EC2 User Data vs. SSM Run Documents vs. Baking an AMI: An Opinionated Guide

If you’ve spent any time provisioning EC2 fleets, you’ve hit this fork in the road: do I throw this into User Data, wrap it in an SSM Run Document, or just bake an AMI and be done with it? All three can get a package installed or a config file written. Only one of them is usually the right tool for the job you actually have.

This isn’t a “here are three AWS features” rundown — plenty of docs already do that. This is the opinionated version: what each one is for, where people misuse them, and the DO/DO NOT lines I’ve settled on after getting burned in a few of the wrong places.

DISCLAIMER Link to heading

The core question isn’t “which one works?” All three will technically work for almost anything. The question is: when does this need to happen, how often, and what’s the blast radius if it goes wrong?


EC2 User Data Link to heading

User Data is bootstrap-time, once-per-launch scripting. It runs as the instance comes up, before anything else is depending on that instance being healthy. Think of it as “get this box to a usable starting state,” not “manage this box over its lifetime.”

DO Link to heading

  • Stand up a basic, usable instance. Install an agent, set a hostname, drop an initial config file, register with a service on first boot — the stuff that only needs to happen once, at launch.
  • Run basic package installs via your distro’s package manager. apt install, yum install, whatever’s native. Low risk, fast, and idempotent enough for a one-shot boot script.

DO NOT Link to heading

  • Perform an OS or dist-upgrade. This is a long-running, failure-prone operation with no good way to recover if it hangs or breaks mid-boot. If your instance needs a newer OS baseline, that’s an AMI problem, not a User Data problem.
  • Make configuration changes to .conf files or systemd units. User Data has no built-in idempotency, no drift detection, and no rollback. The first time you need to change that config later — not just set it once — you’re either re-launching instances or hand-editing fleet-wide, and neither is a good time. Config management belongs somewhere with actual state tracking.

The pattern to notice: User Data is a script that runs once, with no memory of whether it succeeded, no way to re-run cleanly, and no visibility once the instance is up. That’s fine for “get the instance ready.” It’s a liability for “keep the instance correct.”


SSM Run Documents Link to heading

Run Documents are for operations you’ll perform again — against one instance or a thousand — where you want a record of what ran, when, against what, and with what result. This is where User Data’s limitations get solved: you get idempotency (if you write it that way), targeting, execution history, and IAM-gated access to who can run what.

DO Link to heading

  • Make configuration changes that can be scripted. This is the direct upgrade path from “User Data DO NOT” above — if it’s a .conf file or a systemd unit that needs to change, script it as a Run Document instead of baking it into boot.
  • Build and maintain reusable scripts as actual documents, versioned in SSM, rather than snippets scattered across launch templates.
  • Tag instances for runbook association. This is the underrated part — Run Documents pair naturally with resource tags, so you can say “any instance tagged role:cache-node gets this runbook” without touching individual instance IDs.

DO NOT Link to heading

  • Use it to distribute custom, proprietary software across a fleet. You can push a script that pulls your internal build artifact and installs it. You’ll also be re-running that download-and-install dance on every single instance, every single time, and eating the failure modes that come with network dependency at boot- or runtime-adjacent moments. If it’s software your org owns and every instance needs it, that’s a “build it in” problem, not a “run it every time” problem.

The pattern to notice: Run Documents are for actions, not artifacts. If the thing you’re deploying is really just “make this true across the fleet,” Run Documents are exactly right. If the thing you’re deploying is heavy, proprietary, or the same on every single node, you’re using a scripting tool to solve a packaging problem.


Building an AMI Link to heading

An AMI is where “software that should just already be there” lives. It trades boot-time flexibility for boot-time speed and consistency — every instance launched from it starts identical, with zero dependency on scripts succeeding, packages being reachable, or SSM being reachable at the right moment.

DO Link to heading

  • Bake in custom, proprietary software that needs to be replicated identically across the fleet. If every instance of a given role needs the same internal tooling, and that tooling doesn’t change every deploy, put it in the image. You eliminate an entire category of “why did this one instance fail to bootstrap” incidents.

DO NOT Link to heading

  • Bake in anything that changes frequently. If it changes on a weekly or per-sprint cadence, the build-test-redeploy cycle for a new AMI becomes the bottleneck. That’s a sign the thing belongs in an SSM Run Document, not the image.
  • Use it as a substitute for configuration management. Baking environment-specific values — hostnames, per-environment config — into the image means every environment now needs its own AMI. You lose the “one image, many contexts” benefit that makes AMIs worth maintaining in the first place.
  • Treat it as “done” once it’s built. An AMI still needs a patching cadence for its base layer. Skip that and you’re not avoiding drift — you’re just deferring it until the next incident traces back to an OS-level CVE nobody rebuilt for.

Decision Matrix Link to heading

QuestionUser DataSSM Run DocumentAMI
Runs once, at boot, to get the instance ready?⚠️ possible, not the point❌ overkill
Needs to be re-run or changed after launch?❌ (requires rebake)
Is it a .conf or systemd change?✅ if static long-term
Is it proprietary software every instance needs?⚠️ works, but re-pulled every time
Do you need an audit trail of when/where it ran?N/A (it’s baked in)
Does drift/rollback matter?❌ no tracking✅ built in✅ via image versioning

Where This Gets Opinionated Link to heading

A few callouts worth stating plainly, since the DO/DO NOT lists can read as absolute when reality has edge cases:

Opinion 1: “Scriptable” doesn’t mean “should be scripted every time.” Link to heading

The line between an SSM Run Document and an AMI is really a question of change frequency vs. blast radius. If your proprietary software changes every sprint, baking a new AMI every time might genuinely be slower than your team can tolerate — that’s a legitimate reason to lean on Run Documents longer than this guide suggests. The DO NOT isn’t a hard rule, it’s a default you should have to consciously override.

Opinion 2: AMI sprawl is a real cost you’re deferring, not avoiding. Link to heading

Every “just bake it into the AMI” decision is a decision to own image build pipelines, patching cadence for the base layer, and versioning discipline. If you don’t have that pipeline yet, baking software in isn’t actually simpler — it’s just moving the complexity to a place you’re not looking at yet.


Where This Framework Actually Came From Link to heading

This wasn’t a whiteboard exercise — it’s the order I actually arrived at these opinions on a real program that had me standing up EC2 fleets.

I started the way most people do: everything went into User Data. Package installs, config, the works. It worked fine — until the first time something in that User Data needed to change. Because User Data has no built-in mechanism for re-applying itself, “update the config” meant relaunching instances just to get the new bootstrap script to run. That got time-consuming fast, and it’s the exact thing that pushed me off User Data and onto SSM Run Documents — the same change could now be pushed to instances that were already running, with a record of what ran and where.

Once Run Documents were in place, tagging stopped being just an inventory nicety. I used instance tags as the trigger for EventBridge automation — rules that fired a Run Document automatically based on how an instance was tagged, instead of me manually targeting instance IDs every time. That’s the “tag instances for runbook association” DO above in practice; the EventBridge piece is what made it scale past a handful of boxes.

The last piece was proposing HCP Packer to the team. We had proprietary software that every fresh instance needed, and it was being reinstalled by hand — or via Run Document — on every single launch. Baking it into an AMI with Packer meant that software was just there the moment an instance came up: no install step, no network dependency, no “did the install script actually finish” uncertainty. That proposal is where the AMI section above comes from directly.


The One-Line Version Link to heading

  • User Data — one-shot bootstrap. Get it running.
  • SSM Run Documents — repeatable operations with a record. Keep it correct.
  • AMI — baked-in state. Make it identical, every time, without depending on anything else succeeding.

If you find yourself fighting one of these tools to do something it’s clearly not for — that friction is the signal, not a problem to script around.