Skip to content

Create an Amazon Machine Image (AMI) using EC2 Image Builder and AWS CLI

All the scripts in this article are bash scripts

Setup for macOS

Make sure you do this setup first:

  1. Setup macOS for AWS Cloud DevOps
  2. AWS Authentication

Set variables

Set variables needed for next steps:

account_id=$(aws sts get-caller-identity --query Account --output text)

# change this to your desired region
region="us-west-2"  

# EC2 Image Builder Instance Role
ib_instance_role="ec2-ib-instance-role"

# EC2 Image Builder Instance Profile
ib_instance_profile="ec2-ib-instance-profile"

# EC2 Image Builder Security Group
ib_security_group="ec2-ib-instance-sg"

Create instance profile

This is needed for the EC2 Image Builder to launch instances during image creation.

Create instance role

echo "Creating IAM role..."
aws iam create-role \
    --role-name $ib_instance_role \
    --assume-role-policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {
                    "Service": "ec2.amazonaws.com"
                },
                "Action": "sts:AssumeRole"
            }
        ]
    }'

echo "Attaching policies to the role..."
aws iam attach-role-policy \
    --role-name $ib_instance_role \
    --policy-arn arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilder

aws iam attach-role-policy \
    --role-name $ib_instance_role \
    --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

Create instance profile

echo "Creating instance profile..."
aws iam create-instance-profile \
    --instance-profile-name $ib_instance_profile

echo "Adding role to instance profile..."
aws iam add-role-to-instance-profile \
    --instance-profile-name $ib_instance_profile \
    --role-name $ib_instance_role

echo "Instance profile created: $ib_instance_profile"

Create security group

This is needed for the EC2 Image Builder to launch instances during image creation.

echo "Creating security group..."
sg_id=$(aws ec2 create-security-group \
    --group-name $ib_security_group \
    --description "Security group for Image Builder" \
    --query 'GroupId' \
    --output text)

echo "Security group created with ID: $sg_id"

echo "Adding inbound rule for SSH..."
aws ec2 authorize-security-group-ingress \
    --group-id "$sg_id" \
    --protocol tcp \
    --port 22 \
    --cidr "0.0.0.0/0"

echo "Adding inbound rule for HTTP..."
aws ec2 authorize-security-group-ingress \
    --group-id "$sg_id" \
    --protocol tcp \
    --port 80 \
    --cidr "0.0.0.0/0"

echo "Security group configured."

Create Recipe

Create temp working directory

mkdir -p ~/tmp/ec2-ami-image-builder
cd ~/tmp/ec2-ami-image-builder

Add a component for installing Nginx

Create nginx installation component YAML:

cat << EOF > nginx-component.yaml
name: InstallNginx
description: Installs and starts nginx
schemaVersion: 1.0
phases:
  - name: build
    steps:
      - name: InstallNginx
        action: ExecuteBash
        inputs:
          commands:
            - sudo yum update -y
            - sudo yum install nginx -y
            - sudo systemctl start nginx
            - sudo systemctl enable nginx
EOF

Create the component

component_arn=$(aws imagebuilder create-component \
    --name "nginx-install-component" \
    --semantic-version "1.0.0" \
    --platform "Linux" \
    --data "file://nginx-component.yaml" \
    --region $region \
    --query 'componentBuildVersionArn' \
    --output text)

echo "Component ARN: $component_arn"

To list your own components:

aws imagebuilder list-components --owner Self

To list the components available from Amazon:

list_amazon_components() {
    local component_name="$1"
    component_name=$(echo "$component_name" | tr '[:upper:]' '[:lower:]')
    local next_token=""

    echo "Listing Amazon components with name containing '$1' (case-insensitive) ..."

    while true; do
        if [ -z "$next_token" ]; then
            result=$(aws imagebuilder list-components \
                --owner Amazon \
                --region $region \
                --query 'componentVersionList[*].{Name:name,Version:version,ARN:arn}' \
                --output text)
        else
            result=$(aws imagebuilder list-components \
                --owner Amazon \
                --region $region \
                --next-token "$next_token" \
                --query 'componentVersionList[*].{Name:name,Version:version,ARN:arn}' \
                --output text)
        fi

        echo "$result" | while read -r name version arn; do
            name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]')
            if echo "$name_lower" | grep -q "$component_name"; then
                printf "%-50s %-10s %s\n" "$name" "$version" "$arn"
            fi
        done

        next_token=$(aws imagebuilder list-components \
            --owner Amazon \
            --region $region \
            ${next_token:+--next-token "$next_token"} \
            --query 'nextToken' \
            --output text)

        [ "$next_token" = "None" ] && break
    done
}

list_amazon_components "update-linux"

Create the recipe

List Amazon Linux images (via System Manager):

ami_id=$(aws ssm get-parameters --names /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --query 'Parameters[0].Value' --output text --region $region)

echo "Latest Amazon Linux 2023 AMI ID: $ami_id"

Create a YAML file for the image recipe :

cat << EOF > image-recipe.yaml
name: al2023-nginx-recipe
semanticVersion: 1.0.0
parentImage: $ami_id
components:
  - componentArn: arn:aws:imagebuilder:us-west-2:aws:component/update-linux/1.0.2
  - componentArn: $component_arn
EOF

Create Image Recipe:

recipe_arn=$(aws imagebuilder create-image-recipe \
    --cli-input-yaml "file://image-recipe.yaml" \
    --region $region \
    --query 'imageRecipeArn' \
    --output text)

echo "Recipe ARN: $recipe_arn"

Create infrastructure configuration

infra_config_arn=$(aws imagebuilder create-infrastructure-configuration \
    --name "al2023-nginx-infra-config" \
    --instance-types t2.micro \
    --instance-profile-name $ib_instance_profile \
    --region $region \
    --query 'infrastructureConfigurationArn' \
    --output text)

echo "Infrastructure Configuration ARN: $infra_config_arn"

Create distribution configuration

dist_config_arn=$(aws imagebuilder create-distribution-configuration \
    --name "al2023-nginx-dist-config" \
    --distributions "region=$region,amiDistributionConfiguration={name='al2023-nginx-{{imagebuilder:buildDate}}'}" \
    --region $region \
    --query 'distributionConfigurationArn' \
    --output text)

echo "Distribution Configuration ARN: $dist_config_arn"

Create image pipeline

pipeline_arn=$(aws imagebuilder create-image-pipeline \
    --name "al2023-nginx-pipeline" \
    --image-recipe-arn "$recipe_arn" \
    --infrastructure-configuration-arn "$infra_config_arn" \
    --distribution-configuration-arn "$dist_config_arn" \
    --region $region \
    --query 'imagePipelineArn' \
    --output text)

echo "Image Pipeline ARN: $pipeline_arn"

Start the image pipeline

execution_id=$(aws imagebuilder start-image-pipeline-execution \
    --image-pipeline-arn "$pipeline_arn" \
    --region $region \
    --query 'imageBuildVersionArn' \
    --output text)

echo "Image Pipeline Execution ID: $execution_id"

Monitor build progress

echo "Image Build Version ARN: $execution_id"

# Function to check build status
check_build_status() {
    local status=$(aws imagebuilder get-image \
        --image-build-version-arn "$execution_id" \
        --region $region \
        --query 'image.state.status' \
        --output text)
    echo "$status"
}

# Monitor build progress
echo "Monitoring build progress ..." 
while true; do
    status=$(check_build_status)
    echo "Current status: $status"

    if [[ "$status" == "AVAILABLE" ]]; then
        echo "Build completed successfully!"
        break
    elif [[ "$status" == "FAILED" ]]; then
        echo "Build failed. Check the AWS console for more details."
        break
    elif [[ "$status" == "CANCELLED" ]]; then
        echo "Build was cancelled."
        break
    fi

    sleep 10
done

# If build completed successfully, get the AMI ID
if [[ "$status" == "AVAILABLE" ]]; then
    ami_id=$(aws imagebuilder get-image \
        --image-build-version-arn "$execution_id" \
        --region $region \
        --query 'image.outputResources.amis[0].image' \
        --output text)
    echo "Created AMI ID: $ami_id"
fi

Cleanup

Set variables

image_pipeline_name="al2023-nginx-pipeline"
image_recipe_name="al2023-nginx-recipe"
component_name="nginx-install-component"
infrastructure_config_name="al2023-nginx-infra-config"
distribution_config_name="al2023-nginx-dist-config"

Delete Image Pipeline

echo "Deleting Image Pipeline: $image_pipeline_name"

pipeline_arn=$(aws imagebuilder list-image-pipelines --region $region --query "imagePipelineList[?name=='$image_pipeline_name'].arn | [0]" --output text)

aws imagebuilder delete-image-pipeline --image-pipeline-arn $pipeline_arn --region $region

echo "Waiting for Image Pipeline to be deleted..."
while aws imagebuilder list-image-pipelines --region $region --query 'imagePipelineList[].name' --output text | grep -q "$image_pipeline_name"; do
    sleep 5
done

echo "Image Pipeline deleted successfully."

Delete Distribution Configuration

echo "Deleting Distribution Configuration: $distribution_config_name"

dist_config_arn=$(aws imagebuilder list-distribution-configurations --region $region --query "distributionConfigurationSummaryList[?name=='$distribution_config_name'].arn | [0]" --output text)

aws imagebuilder delete-distribution-configuration --distribution-configuration-arn $dist_config_arn --region $region

echo "Waiting for Distribution Configuration to be deleted..."
while aws imagebuilder list-distribution-configurations --region $region --query 'distributionConfigurationSummaryList[].name' --output text | grep -q "$distribution_config_name"; do
    sleep 5
done

echo "Distribution Configuration deleted successfully."

Delete Infrastructure Configuration

echo "Deleting Infrastructure Configuration: $infrastructure_config_name"

infra_config_arn=$(aws imagebuilder list-infrastructure-configurations --region $region --query "infrastructureConfigurationSummaryList[?name=='$infrastructure_config_name'].arn | [0]" --output text)

aws imagebuilder delete-infrastructure-configuration --infrastructure-configuration-arn $infra_config_arn --region $region

echo "Waiting for Infrastructure Configuration to be deleted..."
while aws imagebuilder list-infrastructure-configurations --region $region --query 'infrastructureConfigurationSummaryList[].name' --output text | grep -q "$infrastructure_config_name"; do
    sleep 5
done

echo "Infrastructure Configuration deleted successfully."

Delete Recipe

echo "Deleting Image Recipe: $image_recipe_name"

recipe_arn=$(aws imagebuilder list-image-recipes --region $region --query "imageRecipeSummaryList[?name=='$image_recipe_name'].arn | [0]" --output text)

aws imagebuilder delete-image-recipe --image-recipe-arn $recipe_arn --region $region

echo "Waiting for Image Recipe to be deleted..."
while aws imagebuilder list-image-recipes --region $region --query 'imageRecipeSummaryList[].name' --output text | grep -q "$image_recipe_name"; do
    sleep 5
done

echo "Image Recipe deleted successfully."

Delete Component

echo "Deleting Component: $component_name"

component_arn=$(aws imagebuilder list-components --region $region --query "componentVersionList[?name=='$component_name'].arn | [0]" --output text)

aws imagebuilder delete-component --component-build-version-arn $component_arn/1 --region $region

echo "Waiting for Component to be deleted..."
while aws imagebuilder list-components --region $region --query 'componentVersionList[].name' --output text | grep -q "$component_name"; do
    sleep 5
done

echo "Component deleted successfully."

Delete Instance Profile

echo "Deleting Instance Profile: $ib_instance_profile"

aws iam remove-role-from-instance-profile --instance-profile-name $ib_instance_profile --role-name $ib_instance_role

aws iam delete-instance-profile --instance-profile-name $ib_instance_profile

echo "Instance Profile deleted successfully."

Delete IAM Role

echo "Deleting IAM Role: $ib_instance_role"

aws iam detach-role-policy --role-name $ib_instance_role --policy-arn arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilder

aws iam detach-role-policy --role-name $ib_instance_role --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

aws iam delete-role --role-name $ib_instance_role

echo "IAM Role deleted successfully."

Delete Security Group

echo "Deleting Security Group: $ib_security_group"

sg_id=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=$ib_security_group" --query 'SecurityGroups[0].GroupId' --output text --region $region)

aws ec2 delete-security-group --group-id $sg_id --region $region

echo "Security Group deleted successfully."

Delete AMI

Optionally, delete the AMI if you want to remove it:

snapshot_id=$(aws ec2 describe-images --image-ids $ami_id --region $region --query 'Images[0].BlockDeviceMappings[0].Ebs.SnapshotId' --output text)

aws ec2 deregister-image --image-id $ami_id --region $region
echo "AMI deregistered: $ami_id"

aws ec2 delete-snapshot --snapshot-id $snapshot_id --region $region
echo "Snapshot deleted: $snapshot_id"

Delete temporary YAML files

rm nginx-component.yaml image-recipe.yaml