references/creating-images.md
# Creating images and pipelines
Contents: [Parameters](#1-gather-parameters-once) · [Base image](#2-resolve-the-base-image) · [Build role](#3-create-the-build-role) · [Components](#4-choose-components) · [Recipe](#5-create-the-image-recipe) · [Infrastructure](#6-create-the-infrastructure-configuration) · [One-off build](#7a-one-off-build) · [Pipeline](#7b-recurring-pipeline) · [Verify](#8-verify) · [Patterns](#9-patterns-golden-ami-patching-and-pipeline-chaining)
## 1. Gather parameters once
Ask for everything in one message so the build can run without stalling. (Commands in this skill use `${var}` placeholders — substitute the actual values into the command text, including inside quoted JSON; `${account_id}` is the account ID from `aws sts get-caller-identity --query Account --output text`.)
- **region** (required)
- **architecture**: x86_64 or arm64 (default x86_64). This drives the base image, component compatibility, and instance types together.
- **software to install** (their own software, plus anything an Amazon-managed component covers)
- **one-off or recurring** (see the SKILL.md decision table); if recurring, the rebuild cadence
- **distribution needs**: same Region only (default), other Regions, launch template to update, SSM parameter to publish (see [distribution-options.md](references/distribution-options.md))
- Everything else is overridable when the user asks (resource names, instance types, subnet/security group, an S3 bucket for build-log archival) — otherwise take the defaults below.
## 2. Resolve the base image
Default to Amazon Linux 2023 via the Amazon-managed Image Builder image, with the `x.x.x` wildcard so the recipe always tracks the latest version (this is also how scheduled rebuilds detect base-image updates — step 7b):
```
arn:aws:imagebuilder:${region}:aws:image/amazon-linux-2023-x86/x.x.x
arn:aws:imagebuilder:${region}:aws:image/amazon-linux-2023-arm64/x.x.x
```
Browse the Amazon-managed catalog with `aws imagebuilder list-images --owner Amazon --region ${region}`; if a create call rejects one of the AL2023 ARNs above, confirm the image name there. For an OS Image Builder doesn't publish as an Amazon-managed image, fall back to a public SSM parameter (e.g. `ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64`). A specific AMI ID is a last resort for one-off builds only — it never picks up new releases or patches, so it's wrong for recurring pipelines.
Valid `--parent-image` forms (the API's term for the base image): an Image Builder image ARN, an `ssm:` parameter reference (name or ARN), an AMI ID, or a Marketplace product ID. An EC2-style image ARN (`arn:aws:ec2:...:image/ami-...`) is rejected.
Note: AL2023 ships with AWS CLI v2 preinstalled — don't add a CLI install component to an AL2023 image.
## 3. Create the build role
The build role (the IAM role on the build instance's instance profile) needs exactly two managed policies for AMI builds. Don't confuse it with the build's optional `executionRole`, the role Image Builder itself assumes to run the build's workflows — see [custom-workflows.md](references/custom-workflows.md); the flows here don't need one.
```bash
aws iam create-role --role-name ${name}-role --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole","Condition":{"StringEquals":{"aws:SourceAccount":"${account_id}"}}}]}'
aws iam attach-role-policy --role-name ${name}-role --policy-arn arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilder
aws iam attach-role-policy --role-name ${name}-role --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
aws iam create-instance-profile --instance-profile-name ${name}-role
aws iam add-role-to-instance-profile --instance-profile-name ${name}-role --role-name ${name}-role
# Only when using S3 build logging (step 6) - without this grant, S3 logs silently fail to deliver:
aws iam put-role-policy --role-name ${name}-role --policy-name s3-build-logs \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"arn:aws:s3:::${log_bucket}/imagebuilder/*"}]}'
```
- The `aws:SourceAccount` condition scopes the role to instances launched in this account (confused-deputy protection) — EC2 denies credential vending when the condition doesn't match.
- Do NOT attach `EC2InstanceProfileForImageBuilderECRContainerBuilds` — that is for container builds only and grants unnecessary ECR access.
- Wait 10–15 seconds after creating the instance profile before using it (IAM is eventually consistent).
- If the log bucket uses SSE-KMS with a customer-managed key, the S3 logging policy above also needs `{"Effect":"Allow","Action":"kms:GenerateDataKey","Resource":"${log_bucket_kms_key_arn}"}` — without it `PutObject` is denied and log delivery fails silently.
## 4. Choose components
**Check Amazon-managed components first**: `aws imagebuilder list-components --owner Amazon --region ${region}`. The registry covers common needs — `update-linux` (OS patching), `aws-cli-version-2-linux`, `amazon-cloudwatch-agent-linux`, `stig-build-linux` hardening, language runtimes, and more (confirm current names in the list-components output). Reference them by ARN; the `x.x.x` wildcard version is valid in recipes and tracks the latest version. Check the AWSTOE UpdateOS action-module documentation for the OSes `update-linux` covers before adding it — it doesn't cover AL2023, which patches via base-image refreshes instead (see step 9).
Write a custom component only for the user's own software:
```bash
aws imagebuilder create-component --name ${name} --semantic-version 1.0.0 --platform Linux --data '<yaml>' --region ${region}
```
- Pass the YAML via `--data` (inline or `file://`). `--uri` requires a prior S3 upload. To validate a document without creating anything, add `--dry-run`: a valid document returns `DryRunOperationException` ("Request would have succeeded"), an invalid one returns the actual validation error. Minimal component document:
```yaml
name: ${component_name}
description: Installs ${software}
schemaVersion: 1.0
phases:
- name: build
steps:
- name: Install
action: ExecuteBash
inputs:
commands:
- <install commands>
- name: validate
steps:
- name: Verify
action: ExecuteBash
inputs:
commands:
- <a command that fails if the install is broken>
```
- Make download URLs and binaries match the target architecture.
- Never put secrets (API keys, license tokens, passwords) in component YAML or echo them in commands: component documents are retrievable via `get-component`, and every command's stdout/stderr streams to CloudWatch and the S3 log bucket. Have instances fetch secrets at run time from AWS Secrets Manager or SSM Parameter Store (SecureString).
- To reboot mid-build (kernel updates, agent installs), exit a build step with code `194`: the instance reboots and the build re-runs that step from the top, so guard it with a marker file to avoid a reboot loop. A plain `reboot` command fails the step instead of resuming. The Amazon-managed `update-linux` component handles its own reboots.
- Versioning behavior: re-creating the same name+version with **identical** content returns `ResourceAlreadyExistsException` — the document already exists, reuse the ARN from the error or from `list-components`. Re-creating with **changed** content silently creates a new build version (`/1.0.0/2`), and a recipe that references the version without a build suffix (`.../my-component/1.0.0`) resolves to the latest build version at each build. Bump the semantic version only when you want a new pinnable version. Never delete a component to "fix" this.
- Capture `componentBuildVersionArn` from each response (create responses also include `latestVersionReferences` with ready-made wildcard ARNs).
- To test the finished image, give a component a `test` phase: it runs in the TEST stage on a fresh instance launched from the new AMI, and the image only distributes if tests pass (Amazon-managed test components exist too, e.g. `reboot-test-linux`). Skip the test stage with `--image-tests-configuration imageTestsEnabled=false` on the build if speed matters more.
- For the full component document schema (action modules beyond ExecuteBash, step chaining, test phases), see the [AWSTOE component documentation](https://docs.aws.amazon.com/imagebuilder/latest/userguide/toe-use-documents.html).
## 5. Create the image recipe
```bash
aws imagebuilder create-image-recipe --name ${name}-recipe --semantic-version 1.0.0 \
--parent-image "${parent_image}" \
--components "componentArn=${component_arn_1}" "componentArn=${component_arn_2}" \
--block-device-mappings '[{"deviceName":"/dev/xvda","ebs":{"encrypted":true,"deleteOnTermination":true}}]' \
--region ${region}
```
- The example encrypts the root volume with the account's default EBS key; add `"kmsKeyId":"${kms_key_arn}"` for a customer-managed key (required if the AMI will be shared cross-account — the default EBS key can't be). Block device mappings are also where root volume size and type are set. Distribution-side `kmsKeyId` only encrypts copies made to other Regions or accounts.
- Recipes are immutable. To pick up changed components or settings, create a new recipe version — pass either an explicit next version (`1.0.1`) or a wildcard patch (`--semantic-version 1.0.x`), which the create call auto-assigns to the next free patch (wildcards auto-assign in create requests and resolve to the latest version in ARN references — see the Image Builder semantic versioning documentation). A pipeline that references the recipe with the `x.x.x` wildcard (`arn:...:image-recipe/${name}-recipe/x.x.x`) builds from the latest recipe version automatically; one pinned to a specific version needs repointing with `update-image-pipeline`.
- `update-image-pipeline` is a **full replacement**: re-pass every field. Omitting `--status` resets a DISABLED pipeline to ENABLED; omitting `--schedule` makes the pipeline manual.
## 6. Create the infrastructure configuration
```bash
aws imagebuilder create-infrastructure-configuration --name ${name}-infra \
--instance-profile-name ${name}-role \
--instance-types ${type_a} ${type_b} \
--instance-metadata-options httpTokens=required \
--logging "s3Logs={s3BucketName=${log_bucket},s3KeyPrefix=imagebuilder}" \
--region ${region}
```
Omit the `--logging` line unless the user wants the detailed on-instance AWSTOE logs delivered to an S3 bucket they own — these are more granular than the CloudWatch stream (which is on by default regardless) and useful for audits and deep debugging.
- Pass **two or more instance types** matching the architecture — pick current-generation general-purpose types from `aws ec2 describe-instance-types --filters Name=current-generation,Values=true Name=processor-info.supported-architecture,Values=${architecture}`. Image Builder picks by availability, which avoids capacity failures.
- `httpTokens=required` enforces IMDSv2 on build instances (AWS security best practice).
- S3 logging requires the logging grants from step 3. Logs contain the full output of every build command — use a bucket with Block Public Access enabled, server-side encryption enabled (SSE-KMS recommended), a bucket policy that denies requests where `aws:SecureTransport` is `false` to enforce TLS in transit, and S3 server access logging where log access must be auditable.
- For the CloudWatch log group, a customer-managed key is recommended for the same reason. To have it encrypted from the first build, pre-create the group and associate the key up front (the build writes into a pre-existing group): `aws logs create-log-group --log-group-name /aws/imagebuilder/${name}-recipe` then `aws logs associate-kms-key --log-group-name /aws/imagebuilder/${name}-recipe --kms-key-id ${kms_key_arn}`. Otherwise the group is created automatically with default at-rest encryption and the key can be associated after. The key's policy must grant the `logs.${region}.amazonaws.com` service principal use of the key, scoped with the `kms:EncryptionContext:aws:logs:arn` condition to this account's log groups — the CloudWatch Logs encryption documentation has the exact statement.
- The build instance must reach Systems Manager. With a default VPC this works out of the box. Otherwise pass `--subnet-id` and `--security-group-ids` for a subnet with a route to SSM — a private subnet with VPC endpoints for ssm, ssmmessages, ec2messages, and imagebuilder, plus S3 (add logs for CloudWatch logging) is the preferred production setup since the build instance then needs no internet path; a NAT gateway or a public IP also works. The security group needs no inbound rules — outbound HTTPS (443) is sufficient.
- Optional: `--sns-topic-arn` for build-completion notifications. Notification payloads include resource names, so use an encrypted topic with a restricted policy, and confirm the topic's existing subscribers are authorized recipients before attaching it (`aws sns list-subscriptions-by-topic --topic-arn ${topic_arn}`). Check encryption with `aws sns get-topic-attributes --topic-arn ${topic_arn} --query Attributes.KmsMasterKeyId`; when creating a topic, always pass `--attributes KmsMasterKeyId=${kms_key_id}` — prefer a customer-managed key for production (key-policy control and auditable key use), with `alias/aws/sns` as the fallback minimum. With an SSE-KMS topic, grant Image Builder's service-linked role access in the key policy — otherwise publishes fail silently.
- `--terminate-instance-on-failure` defaults to true (see [troubleshooting.md](references/troubleshooting.md) for when to set false).
If distribution is needed (launch template updates, SSM parameter, other Regions), create the distribution configuration now per [distribution-options.md](references/distribution-options.md) and capture `${dist_arn}`.
## 7a. One-off build
No pipeline needed:
```bash
aws imagebuilder create-image --image-recipe-arn ${recipe_arn} \
--infrastructure-configuration-arn ${infra_arn} \
--region ${region}
```
Add `--distribution-configuration-arn ${dist_arn}` if distribution was configured in step 6. This is `aws imagebuilder create-image` — not `aws ec2 create-image`, which snapshots a running instance. Capture `imageBuildVersionArn` from the response.
## 7b. Recurring pipeline
```bash
aws imagebuilder create-image-pipeline --name ${name} \
--image-recipe-arn ${recipe_arn} \
--infrastructure-configuration-arn ${infra_arn} \
--schedule 'scheduleExpression="cron(0 9 ? * mon *)",pipelineExecutionStartCondition=EXPRESSION_MATCH_AND_DEPENDENCY_UPDATES_AVAILABLE' \
--status ENABLED --region ${region}
aws imagebuilder start-image-pipeline-execution --image-pipeline-arn ${pipeline_arn} --region ${region}
```
Add `--distribution-configuration-arn ${dist_arn}` if distribution was configured in step 6. Capture `${pipeline_arn}` from the create response; `start-image-pipeline-execution` returns `imageBuildVersionArn` — capture it as the build ARN for step 8.
**Schedules.** Pipelines only run at their cron times — a dependency update never starts a build by itself. The start condition decides what happens at each tick:
- `EXPRESSION_MATCH_AND_DEPENDENCY_UPDATES_AVAILABLE` (the default) runs the scheduled build only if a dependency updated since the last build, and skips it otherwise.
- Counts as an update: a new version behind an `x.x.x` wildcard reference (the default Amazon-managed parent image and Amazon-managed components), or a changed `ssm:` parent-image value — provided Image Builder can read the parameter at schedule time (public `/aws/service/*` parameters and parameters under `/imagebuilder/` by default; other names need an execution role on the pipeline that can read the parameter).
- Never counts: a recipe pinned to an AMI ID or to full component build versions, or an `ssm:` parameter the pipeline can't read — such a pipeline never sees an update and never runs under this condition.
- `EXPRESSION_MATCH_ONLY` runs the build on every cron tick — use it for a guaranteed cadence regardless of updates.
- Scheduled pipelines auto-disable after consecutive failed scheduled builds (5 if unset) — see [troubleshooting.md](references/troubleshooting.md). To change a schedule later, see the `update-image-pipeline` note in step 5 (full replacement).
**Vulnerability scanning** (optional, also valid on `create-image` for one-off builds): add `--image-scanning-configuration imageScanningEnabled=true`. Two things to tell the user first: Amazon Inspector must already be activated for EC2 in the account (a paid, account-level decision — creation fails with a dependency error otherwise), and scan results are a findings snapshot in Inspector — scanning does not gate or fail the build (to get alerted on findings, use Inspector's own EventBridge events; see the Inspector documentation).
## 8. Verify
```bash
aws imagebuilder get-image --image-build-version-arn ${build_arn} --region ${region}
```
Confirm `image.state.status` is an in-flight state: `PENDING`, `CREATING`, `BUILDING`, `TESTING`, `DISTRIBUTING`, or `INTEGRATING`. Do not wait for completion (builds typically take 15–45 minutes); give the user this command to check later, and note the output AMI appears under `image.outputResources.amis` when `AVAILABLE`. If the status is `FAILED`, go to [troubleshooting.md](references/troubleshooting.md).
## 9. Patterns: golden-AMI patching and pipeline chaining
**Automatic OS patching.** Combine the 7b schedule with launch-template distribution (see [distribution-options.md](references/distribution-options.md)) and patching runs itself: each new base-image release behind the wildcard parent is picked up at the next scheduled run, and Auto Scaling groups roll onto each new AMI. That base-image refresh is the patching mechanism for the default AL2023 base, which `update-linux` doesn't cover (see step 4). For distros that `update-linux` covers, add it to the recipe as well, to pull the latest security updates between base-image releases. Note: every scheduled rebuild leaves the previous AMI and its snapshot behind — retiring old AMIs is out of scope here (see the Related skills table in SKILL.md).
**Pipeline chaining (golden-base → app image).** The direct form: the downstream recipe uses the upstream pipeline's output image as its parent, with the `x.x.x` wildcard (`arn:aws:imagebuilder:${region}:${account_id}:image/${upstream_image_name}/x.x.x`); downstream pipelines with the dependency-updates start condition pick up each new upstream build at their next scheduled run. When consumers live outside Image Builder, publish to SSM instead — an `ssm:/imagebuilder/${name}/latest` parent also registers updates (see [distribution-options.md](references/distribution-options.md)).
references/custom-workflows.md
# Advanced builds: custom image workflows and execution roles
Image Builder's build and test stages are themselves workflows. Unless custom workflows are attached, every AMI build uses the Amazon-managed defaults — `build-image` (BUILD) and `test-image` (TEST); container builds use `build-container`/`test-container`. Most users never need to change them: customize only when the default stages can't express the build (stage-level steps beyond components, custom orchestration around instance launch, skipping or reordering stage behavior).
## The execution role comes first
Custom workflows always require an execution role — the role Image Builder itself assumes to run the workflows. Without one, `create-image`/`create-image-pipeline` rejects the request: `An execution role is required to provide workflows`. The `EC2ImageBuilderExecutionPolicy` managed policy covers the baseline permissions:
```bash
execution_role_arn=$(aws iam create-role --role-name ${name}-execution \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"imagebuilder.amazonaws.com"},"Action":"sts:AssumeRole","Condition":{"StringEquals":{"aws:SourceAccount":"${account_id}"}}}]}' \
--query Role.Arn --output text)
aws iam attach-role-policy --role-name ${name}-execution --policy-arn arn:aws:iam::aws:policy/EC2ImageBuilderExecutionPolicy
```
This is not the build role from creating-images.md step 3 (that one lives on the build instance); the execution role is assumed by the Image Builder service.
## Define and attach workflows
Start from an Amazon-managed workflow document rather than writing one from scratch — `aws imagebuilder get-workflow` returns the YAML in the `data` field. The document shape (steps chain via `$.stepOutputs`):
```yaml
name: ${workflow_name}
description: Custom build workflow
schemaVersion: 1.0
steps:
- name: LaunchBuildInstance
action: LaunchInstance
onFailure: Abort
inputs:
waitFor: "ssmAgent"
- name: ApplyBuildComponents
action: ExecuteComponents
onFailure: Abort
inputs:
instanceId.$: "$.stepOutputs.LaunchBuildInstance.instanceId"
- name: CreateOutputAMI
action: CreateImage
onFailure: Abort
inputs:
instanceId.$: "$.stepOutputs.LaunchBuildInstance.instanceId"
- name: TerminateBuildInstance
action: TerminateInstance
onFailure: Continue
inputs:
instanceId.$: "$.stepOutputs.LaunchBuildInstance.instanceId"
outputs:
- name: ImageId
value: "$.stepOutputs.CreateOutputAMI.imageId"
```
Like components, `create-workflow` accepts `--dry-run` to validate the document without creating anything — a valid document returns `DryRunOperationException` ("Request would have succeeded"), an invalid one returns the actual validation error. Create your version, then attach it to the build along with the execution role:
```bash
custom_workflow_arn=$(aws imagebuilder create-workflow --name ${name}-build --semantic-version 1.0.0 --type BUILD \
--data file://workflow.yaml --region ${region} \
--query workflowBuildVersionArn --output text)
aws imagebuilder create-image --image-recipe-arn ${recipe_arn} \
--infrastructure-configuration-arn ${infra_arn} \
--workflows "workflowArn=${custom_workflow_arn}" "workflowArn=arn:aws:imagebuilder:${region}:aws:workflow/test/test-image/x.x.x" \
--execution-role ${execution_role_arn} --region ${region}
```
- Workflow types are `BUILD`, `TEST`, and `DISTRIBUTION`. A distribution workflow is an optional override — without one, the attached distribution configuration still runs; the Amazon-managed `distribute-image` and `distribute-container` are the starting points. The Amazon-managed defaults are `arn:aws:imagebuilder:${region}:aws:workflow/build/build-image/x.x.x` and `.../workflow/test/test-image/x.x.x`, and Amazon-managed variants exist too (e.g. `build-image-with-update-ssm-agent`) — browse with `aws imagebuilder list-workflows --owner Amazon`.
- The same `--workflows` + `--execution-role` pair works on `create-image-pipeline`.
- Workflows can pause for approval or external automation with a `WaitForAction` step, which does its own wiring: it publishes an `EC2 Image Builder Workflow Step Waiting` event to the default EventBridge bus, and can invoke a Lambda function asynchronously (`lambdaFunctionName`, with an optional JSON `payload`). The invocation carries the waiting step's execution id, so the function can run its checks and act directly; whatever responds — automation or an operator — resumes or stops the build with `send-workflow-step-action --action RESUME|STOP` (pause default 3 days, maximum 7; the call's reason surfaces as the step's `reason` output). Lambda invocation needs `lambda:InvokeFunction` on the execution role, scoped to the invoked function's ARN.
- An `ExecuteStateMachine` step starts an AWS Step Functions state machine (`stateMachineArn`, optional JSON `input`) and waits for it to complete — the direct integration for compliance validation or certification flows. The execution role needs `states:StartExecution` and `states:DescribeExecution`, scoped to the state machine and its executions.
- A `RunCommand` step runs an SSM command document against the build instance (`documentName`, `parameters`, and `instanceId.$` chained from the launch step) — the hook for hardening, scanning, or setup logic that lives in SSM documents rather than components.
- If a custom-workflow build finishes AVAILABLE but the "output" AMI is just the parent AMI (possibly owned by another account): the build likely attached only a TEST workflow — without a BUILD workflow, no new AMI is created.
- Each step action's inputs, outputs, timeouts, and IAM requirements are in the [supported step actions documentation](https://docs.aws.amazon.com/imagebuilder/latest/userguide/wfdoc-step-actions.html); the broader document schema and workflow management are in the [image workflows documentation](https://docs.aws.amazon.com/imagebuilder/latest/userguide/manage-image-workflows.html).
references/distribution-options.md
# Distribution options
How the output AMI gets where it's needed. All of these live in a distribution configuration attached to the pipeline or one-off build. Default with no distribution configuration: the AMI lands in the build Region only, which is right for most users — treat everything below as opt-in. Note: the output AMI's encryption comes from the recipe's block device mappings, not from the distribution configuration — see [Cross-account sharing and encryption](#cross-account-sharing-and-encryption).
## Launch templates (Auto Scaling groups pick up new AMIs automatically)
This is the default recommendation for Auto Scaling groups: put the launch template in the distribution configuration. Never create launch template versions manually after each build:
```bash
aws imagebuilder create-distribution-configuration --name ${name}-dist \
--distributions '[{"region":"${region}","amiDistributionConfiguration":{"name":"${name}-{{imagebuilder:buildDate}}"},"launchTemplateConfigurations":[{"launchTemplateId":"${lt_id}","setDefaultVersion":true}]}]' \
--region ${region}
```
After each successful build, Image Builder's service-linked role creates a new launch template version pointing at the new AMI and sets it as the default. An Auto Scaling group referencing `$Default` (or `$Latest`) picks it up with no manual steps.
- Verify which version the ASG uses: `aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names ${asg} --query "AutoScalingGroups[0].LaunchTemplate"`. If it pins a numbered version, switch it: `aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${asg} --launch-template "LaunchTemplateId=${lt_id},Version=\$Default"`.
- This means each new AMI reaches whatever uses that launch template with no approval step — confirm the user wants automatic rollout, and point the distribution at a staging launch template if they don't.
- Alternative: `resolve:ssm` in the template's ImageId pointing at a parameter this pipeline publishes — use it when the launch template is managed through IaC.
## SSM parameters (publish the AMI ID for downstream automation)
```json
"ssmParameterConfigurations": [{"parameterName": "/imagebuilder/${name}/latest", "dataType": "aws:ec2:image"}]
```
- `dataType: aws:ec2:image` is recommended — SSM validates the value is a real AMI ID.
- **Permissions gotcha (fails after a successful build):** Image Builder's service-linked role can only write parameters under the `/imagebuilder/` prefix.
- Simplest fix: name the parameter under `/imagebuilder/` (zero IAM changes). For any other name, create an Image Builder execution role with the `EC2ImageBuilderExecutionPolicy` managed policy plus an `ssm:PutParameter` statement scoped to that parameter's ARN (and `ec2:DescribeImages` for the `aws:ec2:image` data type), and pass it as the build's execution role ([custom-workflows.md](references/custom-workflows.md) shows the create-role command and trust policy).
- If the permission is missing, the build and tests succeed and the image then goes `FAILED` at the integration stage with an `ssm:PutParameter` AccessDenied in `image.state.reason` — and the AMI still exists despite the FAILED state. After fixing the permissions, publish the parameter yourself from the existing AMI (`aws ssm put-parameter --name ${param} --type String --data-type aws:ec2:image --value ${ami_id}`) or start a new build. Note `retry-image` does not cover this case — it only retries test- and distribution-stage failures, not the integration stage.
- The `/imagebuilder/` prefix matters twice: it's what the service-linked role can publish to AND read at schedule time — so a published parameter can also feed a downstream pipeline's parent image (pipeline chaining — see creating-images.md, Patterns).
## Other Regions
Add more entries to `distributions`, one per Region, each with its own `amiDistributionConfiguration`; the first entry is the build Region. Note the cost: every Region stores its own snapshot copy. Only add Regions the user actually launches in. Tag output AMIs for governance/cost allocation with `"amiTags": {"team": "${team}"}` inside `amiDistributionConfiguration`.
## Cross-account sharing and encryption
Cross-account distribution of an encrypted AMI requires a customer-managed key in the recipe's block device mappings, planned before the first build — the default AWS-managed EBS key can't be shared ([creating-images.md](references/creating-images.md) step 5). Two forms, both in `amiDistributionConfiguration`:
- `launchPermission` shares the AMI in place. Always list explicit account IDs — `"launchPermission": {"userIds": ["${consumer_account_id}"]}` inside `amiDistributionConfiguration` — a launch permission opened to all accounts makes the AMI public, which is never the default.
- `targetAccountIds` copies the AMI into each target account. The copy requires the `EC2ImageBuilderDistributionCrossAccountRole` role (with `Ec2ImageBuilderCrossAccountDistributionAccess`) to exist in every target account first, created per the AWS cross-account distribution documentation — its trust policy is scoped to the source account, so only that account can assume it — without it the build succeeds and distribution then fails with AccessDenied in `image.state.reason` (fix the role, then `retry-image`). An EventBridge rule on `EC2 Image Builder Image State Change` events surfaces these failures promptly (see SKILL.md, Security considerations).
`kmsKeyId` in this file only encrypts copies made to other Regions or accounts — the output AMI's own encryption comes from the recipe, per the requirement above. A distribution failure quoting `Snapshots encrypted with the AWS Managed CMK can't be shared` is this constraint surfacing — re-bake with a customer-managed key in the recipe. KMS-encrypted copies and License Manager association otherwise follow the AWS documentation.
references/other-image-types.md
# Windows, macOS, and container images
The [creating-images.md](references/creating-images.md) flow (role → components → recipe → infrastructure → build/pipeline) applies to all of these — this file covers only what changes per image type.
## Windows AMIs
- **Base image:** Amazon-managed Windows Server images cover multiple releases (Core and Full, plus SQL and hardened variants). Resolve the image name with `aws imagebuilder list-images --owner Amazon --filters "name=platform,values=Windows"` and take the name segment from the output rather than assuming a naming pattern, then reference it with the `x.x.x` wildcard (e.g. `arn:aws:imagebuilder:${region}:aws:image/windows-server-${release}-english-full-base-x86/x.x.x`).
- **Components:** use `--platform Windows`; steps use `ExecutePowerShell` (or `ExecuteBinary`), and the Amazon-managed `update-windows` component covers OS patching.
- **Reboots:** exit code `3010` is the Windows equivalent of Linux's 194 — the instance reboots and the build re-runs that step from the top, so guard it with a marker file. `update-windows` manages its own reboots.
- **Expectations:** Windows build time varies widely with the patch load — `update-windows` can dominate the run, and Image Builder runs Sysprep automatically before snapshotting. Windows license cost is part of the instance/AMI, nothing extra to configure.
## macOS AMIs
- **Hard prerequisite:** macOS builds run only on EC2 Mac **Dedicated Hosts** — without an allocated host and `tenancy=host` placement, macOS builds cannot launch an instance.
- Allocate a host: `aws ec2 allocate-hosts --instance-type ${mac_instance_type} ...`. List current Mac families with `aws ec2 describe-instance-types --filters "Name=instance-type,Values=mac*"`; the family must match the target architecture. Dedicated Host minimum-allocation billing applies — see the EC2 Dedicated Hosts documentation for billing terms.
- Set the infrastructure configuration's `placement` to tenancy `host` and target the host. Default to a specific `hostId` (simplest); use a **host resource group** (`hostResourceGroupArn` — a License Manager / Resource Groups setup, follow the AWS docs for that part) when License Manager governs the fleet, or host auto-placement when multiple hosts are pooled.
- **Base image:** Amazon-managed macOS images — browse with `aws imagebuilder list-images --owner Amazon --filters "name=platform,values=macOS"` and match the host's architecture.
- Components use `--platform macOS` with `ExecuteBash`. Everything else follows the standard flow; budget for long builds and the dedicated-host cost.
## Container images (Docker to ECR)
- **Recipe:** use a container recipe instead of an image recipe. Default the parent to an Amazon-managed Image Builder container image (browse with `aws imagebuilder list-images --owner Amazon --filters "name=type,values=DOCKER"`); a DockerHub or ECR image is acceptable when the user explicitly chooses one. the output goes to an ECR repository you create first (`create-container-recipe` validates the repository exists and fails with `ResourceNotFoundException` otherwise) — `aws ecr create-repository --repository-name ${ecr_repo} --encryption-configuration encryptionType=KMS,kmsKey=${kms_key_arn} --region ${region}`. A customer-managed key is the production recommendation (auditability and key-policy control); drop `,kmsKey=${kms_key_arn}` to fall back to the AWS-managed `aws/ecr` key. ECR uses AES-256 if the encryption configuration is omitted entirely:
```bash
aws imagebuilder create-container-recipe --name ${name}-recipe --semantic-version 1.0.0 \
--container-type DOCKER --parent-image "arn:aws:imagebuilder:${region}:aws:image/amazon-linux-2023-x86-latest/x.x.x" \
--components "componentArn=${component_arn}" \
--instance-configuration '{"blockDeviceMappings":[{"deviceName":"/dev/xvda","ebs":{"encrypted":true,"deleteOnTermination":true}}]}' \
--dockerfile-template-data 'FROM {{{ imagebuilder:parentImage }}}
{{{ imagebuilder:environments }}}
{{{ imagebuilder:components }}}' \
--target-repository "service=ECR,repositoryName=${ecr_repo}" --region ${region}
```
- **Build instance storage:** the `--instance-configuration` block device mapping in the example encrypts the build instance's working storage — the container counterpart of the AMI recipe's encrypted root volume in creating-images.md step 5.
- **Build role:** container builds are the one case that needs the third managed policy — attach `EC2InstanceProfileForImageBuilderECRContainerBuilds` to the build role (the same policy [creating-images.md](references/creating-images.md) step 3 says to leave off for AMI-only builds).
- **Build and pipeline:** pass `--container-recipe-arn` instead of `--image-recipe-arn` on `create-image`/`create-image-pipeline`; infrastructure configuration and schedules are unchanged. When verifying, the output appears under `outputResources.containers` rather than `.amis`.
- **Distribution:** use `containerDistributionConfiguration` (target ECR repositories per Region) in the distribution configuration instead of the AMI settings in [distribution-options.md](references/distribution-options.md).
references/troubleshooting.md
# Troubleshooting an Image Builder AMI build
(Applies to any Image Builder build, not only ones created through this skill; exit-code examples are Linux — Windows conventions are in [other-image-types.md](references/other-image-types.md).)
The single most important fact: **where the real error lives depends on which stage failed.** The approach:
1. Read `image.state.status` and `image.state.reason` (command below) — which stage failed decides everything else.
2. Match `state.reason` against the failure map. When it only names a component, go straight to the CloudWatch logs section — the APIs won't have more.
3. No matching row: localize by stage and check the pipeline's build history (the paragraph after the map), and let `list-workflow-step-executions` name the failing step.
4. Root cause found: fix it, then `retry-image` for test/distribution-stage failures or a new build otherwise. Root cause unclear: retry with the build instance kept alive (Live debugging below) and inspect it directly.
```bash
aws imagebuilder get-image --image-build-version-arn ${build_arn} --region ${region} --query "image.{state:state,name:name}"
```
## Failure map: stage → where the truth is
| What you see | Where the root cause actually is |
|---|---|
| `state.reason` says `Document arn:...:component/... failed!` (build/test stage, ApplyBuildComponents) | **CloudWatch Logs only.** The state reason names the failing component but not why. The workflow and step-execution APIs (`list-workflow-executions`, `list-workflow-step-executions`) return null messages for this class. Go to the log group below. |
| `state.reason` contains a full error message (e.g. an IAM AccessDenied during distribution/integration) | **Read `state.reason` directly** — post-build stages carry the real error there. Note the output AMI may already exist even though the image shows FAILED. |
| Build runs ~35–40 minutes, then fails with `An error occurred (InvalidInstanceId) when calling the SendCommand operation: Instances [[i-...]] not in a valid state`, or `Step <name> timed out` | The build instance can't reach SSM. See the SSM section below. |
When `state.reason` matches none of these, localize by stage — launch instance → SSM agent connect → build components → test components → create image (snapshot/Sysprep) → distribute → integrate — and check history: `list-image-pipeline-images` on the image's `sourcePipelineArn` shows whether the pipeline ever succeeded. A first-build failure points at configuration; a regression on a scheduled build points at a new base-image version or a changed component.
## CloudWatch logs (the build's full record)
Image Builder streams the build's full execution record to CloudWatch by default — the service's own workflow activity (launching the instance, polling, creating the output image) alongside every command's stdout/stderr from AWSTOE (the on-instance component runner):
```bash
aws logs tail "/aws/imagebuilder/${image_name}" --region ${region} --since 1h
```
The log group is named after the **image name** from the get-image output above (it matches the recipe name for pipeline builds; don't derive it from the build ARN — the ARN lowercases the name while the log group preserves its case). Within the group, each build writes to a stream named `<image version>/<build version>` (e.g. `1.0.0/1`) — the same two segments that end the build ARN — so when a pipeline's group holds many builds, read the stream for the failing build version. Pipeline-level events are in a separate group, `/aws/imagebuilder/pipeline/${pipeline_name}`, with date-named streams (`YYYY/MM/DD`).
A build can fail at any workflow step — launching the instance, running a component (TOE document), creating the output image — and each shape reads differently in the stream, so start from the first error and work outwards rather than hunting for one marker:
- **A failed command step** (components using the `ExecuteBash`/`ExecutePowerShell`/`ExecuteBinary` action modules): searching `ExitCode` jumps to it. **`ExitCode 126`** ("cannot execute") is usually an architecture mismatch — an x86 binary running on an arm64 instance or vice versa (exec format error) — or a file that isn't executable; for a mismatch, fix the component's download URLs or the parent image/instance types so all three agree. For other nonzero codes, read the surrounding stdout/stderr — it is the full output of the failing command.
- **Everything else carries no exit code**: components built on the other AWSTOE action modules (downloads, file operations, reboots, assertions, OS updates) each fail with their own error message, and the same goes for TOE document validation, bootstrap problems, and service-side steps failing (launch, image creation) — all surface as error text in the stream. The Common errors list below carries the recurring signatures (`Unable to bootstrap TOE`, `GetComponent ... dial tcp`, a step restarting mid-run), and `list-workflow-step-executions` names the failing step when the stream alone doesn't make it obvious.
- The logs also reference the SSM RunCommand invocations that drove each stage (`command id: <uuid>`) — `aws ssm get-command-invocation --command-id ${command_id} --instance-id ${build_instance_id}` returns that command's status and output, a useful second view when the log stream cuts off.
If S3 logging was configured on the infrastructure configuration, the bucket holds the detailed on-instance AWSTOE logs — more granular than the CloudWatch stream. Both destinations carry everything build commands print — a secret echoed by a step lands in the logs, which is why secrets are fetched at run time from AWS Secrets Manager or SSM Parameter Store, never embedded in components ([creating-images.md](references/creating-images.md) step 4).
## Live debugging on the build instance
By default Image Builder terminates the build instance on failure, so logs are all you get. To keep the instance running for SSH/SSM inspection, update the infrastructure configuration before retrying. `update-infrastructure-configuration` is a full replacement: read the current settings back with `aws imagebuilder get-infrastructure-configuration --infrastructure-configuration-arn ${infra_arn}` and re-pass every set field (instance profile, instance types, metadata options, logging, subnet/security groups) or they are dropped. If the configuration doesn't use S3 logging, drop the `--logging` line below; add `--subnet-id`/`--security-group-ids` if the configuration sets them:
```bash
aws imagebuilder update-infrastructure-configuration \
--infrastructure-configuration-arn ${infra_arn} \
--instance-profile-name ${name}-role \
--instance-types ${type_a} ${type_b} \
--instance-metadata-options httpTokens=required \
--logging "s3Logs={s3BucketName=${log_bucket},s3KeyPrefix=imagebuilder}" \
--no-terminate-instance-on-failure --region ${region}
```
Instance charges apply until you terminate it manually, and the kept instance still vends the build role's credentials via IMDS — terminate it as soon as inspection is done, then revert with `--terminate-instance-on-failure`.
## Common errors
- **Build fails at launch because the parent AMI can't be found or isn't available — and re-runs keep failing the same way**: the recipe is likely pinned to a hardcoded AMI ID that has since been deregistered or unshared. Repoint the recipe's parent image at an `ssm:` parameter or an Image Builder image ARN with `x.x.x` ([creating-images.md](references/creating-images.md) step 2).
- **SSM unreachable (`InvalidInstanceId ... not in a valid state` / step timeout)**: the instance has no path to Systems Manager. Causes: account has no default VPC (builds need `--subnet-id`/`--security-group-ids`), private subnet without VPC endpoints (needs ssm, ssmmessages, ec2messages, and imagebuilder, plus S3; add logs for CloudWatch logging), or the role is missing `AmazonSSMManagedInstanceCore`. A component-download failure in the logs (`GetComponent ... dial tcp ... i/o timeout`) usually points at the same connectivity gap — check the imagebuilder VPC endpoint in particular.
- **`Unable to bootstrap TOE`**: two common causes to check — the instance can't fetch the AWSTOE bootstrap from the regional `ec2imagebuilder-toe-<region>-prod` S3 bucket (S3 endpoint policy), or a hardened (CIS/STIG) base image mounts `/tmp` `noexec`; setting the recipe's `workingDirectory` somewhere executable is the usual fix for the latter.
- **`ssm:ListCommandInvocations returned terminal state Failed` at ApplyBuildComponents**: often a component rebooted the instance outside the supported reboot protocol (Windows updates are the usual suspect) — check the logs for a step that restarted mid-run, and use the exit-code protocol (194/3010) or the Reboot action module instead ([other-image-types.md](references/other-image-types.md) has the Windows specifics).
- **The `aws-cli-version-2-linux` component fails during its install steps**: the base image may already ship the AWS CLI, and the preinstalled copy collides with the component's install. Check whether the base includes it (`which aws` on the base image, or its documentation) — if so, remove the component; it's redundant there.
- **`inspector-test-linux`/`-windows` component fails with an Inspector `CreateResourceGroup` AccessDenied**: that component targets the retired Inspector Classic — remove it and use the build's native scanning instead (`--image-scanning-configuration imageScanningEnabled=true`, [creating-images.md](references/creating-images.md) step 7b).
- **Pipeline is ENABLED but scheduled dependency-update runs never happen**: compare `dateLastRun`/`dateNextRun` from `get-image-pipeline` against the cron, then check the recipe's references are update-detectable — a pinned AMI ID or full component build version never registers an update, and an `ssm:` parent image only counts if Image Builder can read the parameter at schedule time ([creating-images.md](references/creating-images.md) step 7b has the trigger requirements).
- **`You are not authorized to use the provided image` on `create-image-recipe`**: usually the CALLING role, not the build role — it needs `ec2:DescribeImages` to validate the parent image.
- **Scheduled pipeline silently stopped building**: pipelines auto-disable after consecutive failed scheduled builds (5 if unset; the pipeline's `autoDisablePolicy` sets the threshold — manual runs don't count toward it, and any success or pipeline update resets the counter). Check `get-image-pipeline` for `status: DISABLED`, fix the underlying failure, then re-enable with `update-image-pipeline` (a full replacement — re-pass every field).
- **`InsufficientInstanceCapacity`**: the infrastructure configuration accepts a list of instance types — pass 2–3 so Image Builder can pick an available one.
- **`InvalidParameterValueException` — "The provided instance profile does not exist"** on `create-infrastructure-configuration` right after creating the role: IAM propagation. Wait 10–15 seconds and retry.
- **`ResourceAlreadyExistsException` on `create-component`**: the identical document already exists — reuse it (the existing ARN is in the error). Do not delete components to resolve this.
- **`InvalidParameterValueException`**: almost always a malformed identifier. Use the exact ARN a create call returned; check the parent-image form (AMI ID, `ssm:` parameter, Image Builder ARN, or Marketplace ID — an `arn:aws:ec2:...:image/...` ARN is rejected).
- **Image FAILED after build and tests succeeded, `ssm:PutParameter` AccessDenied in `state.reason`**: the SSM-parameter distribution permissions gotcha — the service-linked role can only write under `/imagebuilder/`. Fixes in [distribution-options.md](references/distribution-options.md) (rename the parameter under `/imagebuilder/`, or use an execution role); the output AMI already exists, so publish the parameter manually or start a new build (`retry-image` doesn't cover integration-stage failures).
- **Container build FAILED after components succeeded, ECR AccessDenied/`denied` in logs**: the build role is missing `EC2InstanceProfileForImageBuilderECRContainerBuilds`, or the target ECR repository doesn't exist in that Region (see [other-image-types.md](references/other-image-types.md)).
- **Build stage succeeds but the TEST stage fails at LaunchInstance or times out**: something the build changed broke the output AMI's connectivity — commonly networking changes or hardening that blocks the SSM agent on the fresh test instance. Compare what the components changed against the SSM requirements above.
- **macOS build never launches an instance**: no capacity on the Mac dedicated host, missing Dedicated Host placement (tenancy `host` plus a host target) in the infrastructure configuration, or host family vs image architecture mismatch — the launch error appears in `image.state.reason`.
- **Windows build looks hung**: `update-windows` and Sysprep legitimately run a long time — check the CloudWatch log group is still progressing before declaring it stuck. A step restarting repeatedly in the logs is an unguarded exit-3010 reboot loop (see [other-image-types.md](references/other-image-types.md)).
- **Stuck or unwanted in-flight build**: `aws imagebuilder cancel-image-creation --image-build-version-arn ${build_arn}`. For test- or distribution-stage failures fixed in place (flaky tests, permissions, target roles), `retry-image` retries from the failed stage without rebuilding the image.
- **CLI `ParamValidation` errors on `--filters`**: quote filter values containing spaces: `--filters "name=name,values=Amazon Linux 2023 x86"`.
## A note on baked-AMI oddities
If an instance launched from the output AMI "behaves oddly" (SSM agent missing, SSH host keys regenerated, machine-id reset): that's Image Builder's post-build cleanup, which sanitizes the image before snapshotting. The SSM agent uninstall is controlled by `systemsManagerAgent.uninstallAfterBuild` in the recipe.
SKILL.md
---
name: amazon-ec2-image-builder
description: Creates and automates custom image builds with EC2 Image Builder - Linux, Windows, and macOS AMIs, and container images to ECR. Covers the build IAM role, Amazon-managed and custom components, image recipes, infrastructure and distribution configuration (launch templates, SSM parameters, other Regions), one-off builds, recurring scheduled pipelines for golden AMI automation and OS patching, custom image workflows, and diagnosing failed builds. Applies when creating, automating, or scheduling AMI or container image builds with Image Builder, or when debugging a failed build. Not for launching instances from existing AMIs, AMI lifecycle/retirement, or general EC2 fleet management.
version: 1
---
# Amazon EC2 Image Builder
## Overview
Domain expertise for building custom AMIs and container images with EC2 Image Builder — from the build IAM role through recipes, pipelines, distribution, and troubleshooting.
**Works best with** the [AWS MCP server](https://docs.aws.amazon.com/aws-mcp/) — recommended for sandboxed execution and audit logging. All guidance also works with standard AWS CLI access.
## Guardrail — where this skill's own files live (MCP vs local install)
This skill can be loaded two ways, and they resolve the skill's own bundled
files from different places. Determine how the skill was loaded before reading
a reference or running a script:
- **Loaded through the AWS MCP `retrieve_skill` tool:** The skill is not
installed on the local filesystem. You MUST fetch each reference or script
via `retrieve_skill` with the `file` parameter (e.g.
`file="references/creating-images.md"`), and use the returned content.
Do NOT `file_read` these paths locally — they do not exist on disk.
- **Installed locally** (e.g. `.kiro/skills/amazon-ec2-image-builder/` or
`~/.claude/skills/amazon-ec2-image-builder/`): Read files from the local
skill directory using relative paths.
This distinction applies only to the skill's own packaged files. User data and
session artifacts are always read from and written to the user's working
directory. Never fetch or write user data through `retrieve_skill`.
## First decision: one-off image or recurring pipeline
Ask this before creating anything — it changes what you build.
| The user wants | Do this |
|---|---|
| One custom AMI, once | Follow [creating-images.md](references/creating-images.md) through step 7a: `create-image` with a recipe and infrastructure configuration — no pipeline needed. |
| A golden AMI that stays current (scheduled rebuilds that pick up base-image updates and patches) | An image pipeline: follow [creating-images.md](references/creating-images.md) — the schedule is part of the create-image-pipeline call (step 7b). |
## Related skills — route there instead
| Use this skill | When the request is about |
|---|---|
| **launching-ec2-instance-with-best-practices** | Launching instances from an AMI the user already has |
| **setting-up-ec2-instance-profiles** | Instance profiles in general (not the build IAM role this skill creates) |
| **aws-compute** | AMI sharing, retiring, and lifecycle management; general EC2 fleet questions |
**Not covered here:** AMI lifecycle/retirement (route via the table above) and VM/ISO image import and export (follow the AWS documentation directly).
## Routing (references in this skill)
Read the matching reference before answering. The exact commands, failure fixes, and platform requirements live in the references — answering Image Builder questions from general knowledge is how agents get the details subtly wrong.
| User need | Read |
|---|---|
| Create an image or pipeline end to end: role, components, recipe, infrastructure, schedules, patching, scanning, chaining | [creating-images.md](references/creating-images.md) |
| Get the output AMI where it's needed: launch templates, SSM parameters (the service-linked role writes only under `/imagebuilder/`), other Regions | [distribution-options.md](references/distribution-options.md) |
| A build failed, hangs, or an Image Builder API call errors | [troubleshooting.md](references/troubleshooting.md) |
| Windows (exit-3010 reboots), macOS (Mac Dedicated Hosts required), container images to ECR (extra build-role policy) | [other-image-types.md](references/other-image-types.md) |
| Custom image workflows (advanced — always require an execution role) | [custom-workflows.md](references/custom-workflows.md) |
Reference files carry specific ARNs, Amazon-managed resource names, and service defaults — when precision matters, confirm against the AWS documentation.
## Guardrails (every workflow)
- Quote CLI filter values that contain spaces: `--filters "name=name,values=Amazon Linux 2023 x86"`. Unquoted spaces are a CLI parse error.
- Use the exact ARN each create call returns — never construct ARNs by hand.
- For a "latest" base image use an Amazon-managed image ARN with the `x.x.x` wildcard, or an `ssm:` parameter reference where no managed image exists. Never list versions and sort them as strings — the list is not semver-ordered.
- Keep architecture consistent across the base image, every component's binaries, and the infrastructure instance types. Image Builder performs no create-time validation of this; a mismatch only fails mid-build when the component runs.
- For component failures, the root cause lives in CloudWatch log group `/aws/imagebuilder/<image-name>` (on by default; also in the S3 logs if configured) — never in the API state. See [troubleshooting.md](references/troubleshooting.md).
- To reboot mid-build, exit the step with code `194` (Linux) or `3010` (Windows). The build re-runs that same step after the reboot — not the next step — so guard it with a marker file. A plain reboot command fails the step.
- If a resource the user describes isn't visible to `get-image`/`get-image-pipeline`, say you can't find it and check the Region and credentials in use — then keep troubleshooting from the user's description; a failed lookup is not proof the resource doesn't exist.
- Distribution handles launch templates and SSM publishing natively (`launchTemplateConfigurations`, `ssmParameterConfigurations`) — never add Lambda glue or manual launch-template versions for AMI propagation.
- Default to: Amazon Linux 2023 base, IMDSv2 required (`instanceMetadataOptions httpTokens=required`), and at least two instance types in the infrastructure configuration. S3 build logging is opt-in — CloudWatch logging is on regardless.
- Check Amazon-managed components (`aws imagebuilder list-components --owner Amazon`) before writing component YAML. Common needs (AWS CLI, OS updates, CloudWatch agent, STIG hardening) are already covered.
## Security considerations
The defaults above are the security posture: IMDSv2 required on build instances, no inbound security-group rules, least-privilege build IAM role (two managed policies for AMI builds plus only the scoped grants a workflow needs), no secrets in components or logs, and log buckets with Block Public Access. Build logs capture full command output that can carry sensitive material; CloudWatch Logs encrypts them at rest by default, and associating a customer-managed KMS key with each `/aws/imagebuilder/...` log group (`aws logs associate-kms-key`) is recommended. For auditing and operational visibility, enable CloudTrail in the account so Image Builder API calls are recorded, and configure EventBridge rules or CloudWatch alarms on build failures (source `aws.imagebuilder`, detail-type `EC2 Image Builder Image State Change`) so misconfigurations and unauthorized changes surface promptly. Per-build notifications are covered by the SNS topic option (creating-images.md step 6) — prefer a customer-managed key on that topic too. Deviations from these should be explicit user decisions. Reference: [EC2 Image Builder security best practices](https://docs.aws.amazon.com/imagebuilder/latest/userguide/security-best-practices.html).