Skip to content
Menu
Tech Automation Blog
  • About Author
  • Contact
Tech Automation Blog

Create EC2 Linux AMI with Packer and Powershell

Posted on February 19, 2020February 27, 2022

An Amazon Machine Image (AMI) is an AWS resource we use to deploy Amazon EC2(Elastic Compute Cloud) instances. Ideally, an AMI contains the Operating System(Windows, Linux etc.) and a list of customization made on it to meet specific requirements. It helps to quickly bootstrap one or more EC2 instances with similar configuration.

As per Amazon documentation, An AMI includes the following:

  • One or more EBS snapshots, or, for instance-store-backed AMIs, a template for the root volume of the instance (for example, an operating system, an application server, and applications).
  • Launch permissions that control which AWS accounts can use the AMI to launch instances.
  • A block device mapping that specifies the volumes to attach to the instance when it’s launched.

In my previous few posts I went through the process of creating Azure VM images for Windows and Linux platform. You may refer to those here:

  • Create Azure Linux VM Image with Packer and Powershell
  • Create Azure Windows VM Image with Packer and Powershell

In this post I will share the process of creating a custom private AMI from an Amazon Linux AMI using HashiCorp Packer with set of customization that I need in my baseline image for new EC2 instances. Note, I will share only a few customization to keep it simple. Feel free to add your list of changes either as Inline script in the provisioner section of the template JSON file or create a separate bash script and use it there. You can also add script blocks in the user data section.

This process requires a set of credential(Access Key and Secret Access Key) with required role assignments to perform the Packer builder tasks. So, make sure you have those ready. Refer IAM role for EC2 document while setting up your permission.

You also, need to install and import AWSPowershell Module on your system. You can skip this if you already have it setup.

Next important task is to decide how you are going to refer those access key and secret key in your script. I am running this script from a Windows system and set up Environment variables for those secrets. I will refer them in the script. You can follow the same process or you may use a file with secrets stored or AWS profile or some alternate method. It is absolutely on your convenience and security practice that your organization allows.

So, to set those secrets as persistent environment variables run following commands on Powershell console :

[System.Environment]::SetEnvironmentVariable('AWS_ACCESS_KEY_ID', 'AKIAMYSAMPLEACCESSKEYEXAMPLE',[System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('AWS_SECRET_ACCESS_KEY', 'myyyyyyyyyyyysecrettttttttttttttttt',[System.EnvironmentVariableTarget]::Machine)

AWS supports a list of standard Environment variables. Refer following document to learn more :

Environment Variables To Configure the AWS CLI

Next important consideration – I am using Amazon provided Linux image as my base image and the ssh user name for it is by default EC2-USER. You can use images provided by other vendors or even your own custom AMI as source image.

As, Packer will deploy a temporary EC2 instance to create image, you need to provide a VPC Id, Subnet Id and Security Group Id. If you are running behind a proxy, update your script to authenticate through it. Your Security Group must allow ssh(port 22) to the EC2 instance as Packer will use ssh to connect the temporary instance to configure it and run scripts to set your customization defined in provisioner.

With that let’s start scripting.

First, as my previous posts, I will use chocolatey to install packer on my Windows system. So, let’s set that :

try{
    $checkChoco = choco --version
}
catch{
    Write-Host "ERROR : $_" -Foregroundcolor Yellow
}

if([String]::IsNullOrEmpty($checkChoco)){
    Set-ExecutionPolicy Bypass -Scope Process -Force
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
    Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}

Similarly, I will check if Packer is already there in my system. If not, will install using choco :

try{
    $checkPacker = packer --version
}
catch{
    Write-Host "ERROR : $_" -Foregroundcolor Yellow
}

try{
    if([String]::IsNullOrEmpty($checkPacker)){
        choco install packer
    }    
}
catch{
    Write-Host "ERROR : $_" -Foregroundcolor Yellow
    Exit
}

Now, I am defining a name for the new AMI and then checking if an AMI with same name already exists in my AWS account. If I already have one, add some extra random characters to make it unique :

$amiName = "$($amiNamePrefix)-CentOs-$((Get-Date).ToString("dd-MMM-yyyy"))"
$awsAccount = Get-STSCallerIdentity | Select-Object Account -ExpandProperty Account
$amiDetails = Get-EC2Image -AccessKey $accessKey -SecretKey $secretKey -Region $region -Filter @{ Name="name"; Values=$amiName} -Owner $awsAccount -ErrorAction SilentlyContinue

if(!([String]::IsNullOrEmpty($amiDetails))){
    $amiName = "$($amiName)-$(-join ((65..90) + (97..122) | Get-Random -Count 6 | ForEach-Object {[char]$_}))"
}

Next, we will generate the Template JSON file with all required information for Packer to create the AMI for us. It includes variables, builders and provisioners. Note, I assigned empty string to access key and secret key variables as those will be user defined variables coming from the build command parameters. You still can assign default values instead of nothing if you want. Rest of items are quite self-explanatory.

As mentioned above, update provisioner inline script with your own stuff or create a separate bash script and refer here. JSON template creation script block is there in the full script. Following script block will call Packer build process with parameters :

    Try{
        powershell packer build -var "access_key=$($accessKey)" -var "secret_key=$($secretKey)" $jsonTemplateFile 
    }
    Catch
    {
        Write-Host "ERROR : $_" -Foregroundcolor Red
        Exit
    }
}

Finally, I am checking if the AMI has been created successfully and then cleaning the JSON file.

    Try{
        Write-Host "Deleting Template JSON file"
        if(Test-Path -Path $jsonTemplateFile){
            Remove-Item -Path $jsonTemplateFile -Force
        }
    }
    Catch{
        Write-Host "ERROR : $_" -Foregroundcolor Red
    }

==================================================
$amiDetails = Get-EC2Image -AccessKey $accessKey -SecretKey $secretKey -Region $region -Filter @{ Name="name"; Values=$amiName} -Owner $awsAccount -ErrorAction SilentlyContinue
if(!([String]::IsNullOrEmpty($amiDetails))){
    Write-Host "New Image $($amiName)($($amiDetails.ImageId)) created successfully!" -ForegroundColor Green
}
else{
    Write-Host "ERROR : Something went wrong! Please try again!" -ForegroundColor Red
}

Here is the complete script with all required statements and functions we discussed above:

Param(
	[Parameter(Mandatory=$true)]
        [String]$amiNamePrefix,
        [Parameter(Mandatory=$false)]
        [ValidateSet("eu-north-1","ap-south-1","eu-west-3","eu-west-2","eu-west-1","ap-northeast-2","ap-northeast-1","sa-east-1","ca-central-1","ap-southeast-1","ap-southeast-2","eu-central-1","us-east-1","us-east-2","us-west-1","us-west-2")]
        [String]$region = "us-east-2",    
        [Parameter(Mandatory=$false)]
        [String]$vpcId,
        [Parameter(Mandatory=$true)]
        [String]$subnetId,
        [Parameter(Mandatory=$true)]
        [String]$securityGroupId
)

# Function to create template definition json file for Packer
Function Create-JsonTemplate()
{
    Try{	
    $input = @"
    {
        "variables": {
            "access_key": "",
            "secret_key": ""
        },
        "builders": [{
            "type": "amazon-ebs",        
            "access_key": "{{user ``access_key``}}",
            "secret_key": "{{user ``secret_key``}}",
            "ami_name": "$($amiName)",
            "vpc_id": "$($vpcId)",
            "subnet_id": "$($subnetId)",
            "security_group_id":"$($securityGroupId)",
            "region": "$($region)",
            "source_ami_filter": {
                "filters": {
                  "virtualization-type": "hvm",
                  "name": "amzn-ami-hvm-*-x86_64-gp2",
                  "root-device-type": "ebs"
                },
                "owners": ["137112412989"],
                "most_recent": true
              },
            "ssh_username": "ec2-user",
            "instance_type": "t2.medium",
            "ami_description": "Linux AMI for Avengers Team",
            "disable_stop_instance": "false",
            "ssh_timeout": "20m",
            "communicator": "ssh",            
            "launch_block_device_mappings": [
               	{
                   	"device_name": "/dev/xvda",
                   	"volume_type": "gp2",
                   	"delete_on_termination": true
               	}
            ],
	        "run_tags": {
                "Name": "Packer Builder",
                "OsType": "Linux",
                "CreationDate": "$((Get-Date).ToString("dd-MMM-yyyy"))",
                "Owner": "Avengers"
            },
            "tags": {
                "CreationDate": "$((Get-Date).ToString("dd-MMM-yyyy"))",
                "OsType": "Linux",
                "Owner": "Avengers",
                "Name": "$($amiName)"
            }
          }],
          "provisioners": [{
            "type": "shell",
            "inline": [
                "sudo yum update -y",
                "sudo yum install -y httpd",
                "sudo service httpd start"
            ]
          }]
    }	
"@ 
    $input | Out-File $jsonTemplateFile -Encoding ascii
    }
    Catch
    {
        Write-Host "ERROR : $_" -Foregroundcolor Red
        Exit
    }
}

# Function to create Image using Packer
Function Create-Image(){
    Try{
        powershell packer build -var "access_key=$($accessKey)" -var "secret_key=$($secretKey)" $jsonTemplateFile 
    }
    Catch
    {
        Write-Host "ERROR : $_" -Foregroundcolor Red
        Exit
    }
}

# Function to remove resources created during the image creation process
Function Cleanup-Resources(){
    Try{
        Write-Host "Deleting Template JSON file"
        if(Test-Path -Path $jsonTemplateFile){
            Remove-Item -Path $jsonTemplateFile -Force
        }
    }
    Catch{
        Write-Host "ERROR : $_" -Foregroundcolor Red
    }
}

# Check AWS Secrets are configured as Environment Variable. If not either set those or use altername method to pass those secrets.
# If you use a different method, like user Profile, File or any other method comment out below script block.
if((Test-Path -Path Env:\AWS_ACCESS_KEY_ID) -and (Test-Path -Path Env:\AWS_SECRET_ACCESS_KEY)){
    $accessKey = $Env:AWS_ACCESS_KEY_ID
    $secretKey = $Env:AWS_SECRET_ACCESS_KEY
}
else{
    Write-Host "ERROR : AWS Credentials are not set as Environment Variables. Either set those or use different method to pass those values.If you use alternate method.Comment this Block" -ForegroundColor Yellow
    Exit
}

# Check if passed VPCId exists in the AWS Account, if not Exit
$vpcInfo = Get-EC2Vpc -VpcId $vpcId -Region $region -AccessKey $accessKey -SecretKey $secretKey
if([String]::IsNullOrEmpty($vpcInfo)){
    Write-Host "ERROR : Incorrect VPC ID Supplied. Exiting.."
    Exit
}

# Check if passed SubnetId exists in the AWS Account, if not Exit
$subnetInfo = Get-EC2Subnet -SubnetId $subnetId -Region $region -AccessKey $accessKey -SecretKey $secretKey
if([String]::IsNullOrEmpty($subnetInfo)){
    Write-Host "ERROR : Incorrect Subnet ID Supplied. Exiting.."
    Exit
}
# Check if passed SecurityGroupId exists in the AWS Account, if not Exit
$sgInfo = Get-EC2SecurityGroup -GroupId $securityGroupId -Region $region -AccessKey $accessKey -SecretKey $secretKey
if([String]::IsNullOrEmpty($sgInfo)){
    Write-Host "ERROR : Incorrect Subnet ID Supplied. Exiting.."
    Exit
}

$jsonTemplateFile = "$($env:Temp)\image-template.json"
if(Test-Path $jsonTemplateFile){Remove-Item -Path $jsonTemplateFile -Force}

# Check if chocolatey is already installed on local system. If not, install.
try{
    $checkChoco = choco --version
}
catch{
    Write-Host "ERROR : $_" -Foregroundcolor Yellow
}

if([String]::IsNullOrEmpty($checkChoco)){
    Set-ExecutionPolicy Bypass -Scope Process -Force
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
    Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}

# Check if Packer is already installed.If not, install using chocolatey
try{
    $checkPacker = packer --version
}
catch{
    Write-Host "ERROR : $_" -Foregroundcolor Yellow
}

try{
    if([String]::IsNullOrEmpty($checkPacker)){
        choco install packer
    }    
}
catch{
    Write-Host "ERROR : $_" -Foregroundcolor Yellow
    Exit
}

# Generate AMI Name. Check if an AMI already exists with that name. If so, suffix name with random letters
$amiName = "$($amiNamePrefix)-CentOs-$((Get-Date).ToString("dd-MMM-yyyy"))"
$awsAccount = Get-STSCallerIdentity | Select-Object Account -ExpandProperty Account
$amiDetails = Get-EC2Image -AccessKey $accessKey -SecretKey $secretKey -Region $region -Filter @{ Name="name"; Values=$amiName} -Owner $awsAccount -ErrorAction SilentlyContinue

if(!([String]::IsNullOrEmpty($amiDetails))){
    $amiName = "$($amiName)-$(-join ((65..90) + (97..122) | Get-Random -Count 6 | ForEach-Object {[char]$_}))"
}

Create-JsonTemplate 
Create-Image

$amiDetails = Get-EC2Image -AccessKey $accessKey -SecretKey $secretKey -Region $region -Filter @{ Name="name"; Values=$amiName} -Owner $awsAccount -ErrorAction SilentlyContinue
if(!([String]::IsNullOrEmpty($amiDetails))){
    Write-Host "New Image $($amiName)($($amiDetails.ImageId)) created successfully!" -ForegroundColor Green
}
else{
    Write-Host "ERROR : Something went wrong! Please try again!" -ForegroundColor Red
}
Cleanup-Resources

Here are some screenshots from my last run:

Packer build process started

Updating the temporary ec2 instance through yum update command :

running yum update

Finally, creating the AMI from the temp ec2 :

Packer makes sure to terminate the temp ec2 after completing its process.

terminating ec2 instances

Here is the resultant AMI :

custom private AMI

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on LinkedIn (Opens in new window) LinkedIn
May 2025
M T W T F S S
 1234
567891011
12131415161718
19202122232425
262728293031  
« May    

Recent Posts

  • Monitor and alert Azure Service Health issues May 5, 2020
  • AWS IAM User access review May 3, 2020
  • Integrate Azure Security Center with Event Hub April 28, 2020
  • Add Tags to Azure Subscription April 24, 2020
  • Automate Azure billing report in Excel March 6, 2020

Categories

©2025 Tech Automation Blog | Powered by SuperbThemes