7.7 EC2 Image Builder: Automated AMI Pipelines
Right, so you’ve graduated from manually right-clicking an instance and praying to the AWS gods that your “Create Image” request works. Good for you. That manual process is fine for a one-off, but it’s brittle, unrepeatable, and about as auditable as a secret society. You and I both know that if you can’t version it, test it, and reproduce it with a single command, it doesn’t really exist in production. Enter EC2 Image Builder. This is AWS’s answer to building machine images without the manual headache, and honestly, it’s pretty solid, even if the name is about as imaginative as a beige wall.
Think of Image Builder as your own automated, compliant, slightly pedantic factory line for AMIs. You define a recipe (what goes on the AMI), toss in some components (the scripts that do the installing and configuring), and tell it when to run. It then spins up a temporary instance, runs your stuff, creates the image, tests it, and then shuts everything down, leaving you with a pristine, versioned AMI. No more forgetting to install cloud-init or leaving a stray SSH key on the base image.
Why You Should Bother With This Rube Goldberg Machine
“Why not just a bash script and the AWS CLI?” I hear you mutter. You absolutely could. But then you’re on the hook for the logging, the cleanup of temporary resources, the test phase, and distributing that image across regions. Image Builder bakes all that tedious, critical plumbing right in. The killer feature? Infrastructure as Code (IaC) for your images. Your entire OS hardening process, application installation, and configuration management can be defined in a version-controlled JSON or YAML document. This means you can finally answer the security team’s “how was this image built?” question with a link to a Git commit instead of a shoulder shrug.
The Moving Parts: Recipes, Components, and Pipelines
Image Builder has a few key concepts. A Component is a discrete build or test step, written in YAML. AWS provides a library of managed components for things like installing security patches or the .NET runtime, but you’ll write your own for anything custom. A Recipe is the document that combines a base image (e.g., “Amazon Linux 2 x86_64”) and a list of components to run, in order. Finally, a Pipeline ties it all together, linking a recipe to an infrastructure configuration (instance type to build on, VPC settings) and a schedule. The pipeline is what actually kicks off the build process.
Here’s a bare-bones example of a component document that installs NGINX and does a simplistic test. Save this as nginx-install.yml.
name: ExampleNginxInstallation
description: 'Installs and enables NGINX'
schemaVersion: 1.0
phases:
- name: build
steps:
- name: InstallNginx
action: ExecuteBash
inputs:
commands:
- 'sudo yum install -y nginx'
- 'sudo systemctl enable nginx'
- name: test
steps:
- name: CheckInstall
action: ExecuteBash
inputs:
commands:
- 'nginx -v'
You can’t just use that file directly; you have to import it into Image Builder. The AWS CLI makes this straightforward:
aws imagebuilder create-component \
--name "nginx-install-example" \
--semantic-version "1.0.0" \
--platform "Linux" \
--description "My custom NGINX component" \
--change-description "First version" \
--cli-input-json file://nginx-install.yml
The Gotchas: Where This Thing Will Bite You
It’s not all rainbows and automated goodness. Here are the sharp edges I’ve bled on:
- State is a Lie: The build instance is ephemeral. Any file you create in
/tmpor any state you change during the build phase is gone once the image is created. Your components must install everything to the correct persistent locations (/usr/local/bin,/opt, etc.). This trips up everyone once. - The VPC Matters: Your pipeline defines which VPC and subnet the temporary builder instance uses. If it’s a private subnet with no NAT Gateway, your instance can’t reach the internet to
yum installanything. You’ll sit there wondering why your build is hung for an hour before timing out. Always double-check your infrastructure settings. - Versioning is Mandatory: Every time you change a component or recipe, you must bump the semantic version number. You can’t just update in-place. This is annoying until you need to roll back to a known good version, at which point it becomes brilliant.
- Debugging is… Indirect: You can’t SSH into the build instance while it’s running. Your only window into the process is the logs CloudWatch Logs. Make your bash commands verbose (
set -xis your friend) and get comfortable digging through log streams.
Putting It All Together: A Simple Pipeline
Let’s create a pipeline that uses our component. We’ll do it via CLI for clarity, though in practice you’d Terraform or CloudFormation this.
First, create a recipe document my-recipe.json that references an AWS-managed update component and our custom one.
{
"name": "my-ami-recipe",
"description": "A basic recipe with updates and NGINX",
"version": "1.0.0",
"components": [
{
"componentArn": "arn:aws:imagebuilder:us-east-1:aws:component/update-linux/version/1.0.0"
},
{
"componentArn": "arn:aws:imagebuilder:us-east-1:123456789012:component/nginx-install-example/1.0.0"
}
],
"parentImage": "arn:aws:imagebuilder:us-east-1:aws:image/amazon-linux-2-x86/x.x.x",
"workingDirectory": "/tmp"
}
Register the recipe and create the pipeline:
# Create the recipe
aws imagebuilder create-image-recipe --cli-input-json file://my-recipe.json
# Create an infrastructure configuration (adjust subnet & security group)
aws imagebuilder create-infrastructure-configuration \
--name "my-infra-config" \
--description "Basic build infrastructure" \
--instance-types "m5.large" \
--subnet-id "subnet-12345abc" \
--security-group-ids "sg-12345abc"
# Finally, create the pipeline itself
aws imagebuilder create-image-pipeline \
--name "my-weekly-ami-pipeline" \
--description "Builds my AMI weekly" \
--image-recipe-arn "arn:aws:imagebuilder:us-east-1:123456789012:image-recipe/my-ami-recipe/1.0.0" \
--infrastructure-configuration-arn "arn:aws:imagebuilder:us-east-1:123456789012:infrastructure-configuration/my-infra-config" \
--schedule scheduleExpression="cron(0 0 ? * SUN *)" \
--status "ENABLED"
And there you have it. Every Sunday at midnight UTC, this thing will wake up, build you a fresh, patched, NGINX-equipped AMI, and store it in your account. It’s one less thing to remember, and more importantly, it’s a process you can actually trust. Now go and delete those manually created AMIs. You’re better than that.