An Amazon Machine Image (AMI) is a supported and maintained image provided by AWS that provides the information required to launch an instance. You must specify an AMI when you launch an instance. You can launch multiple instances from a single AMI when you require multiple instances with the same configuration. You can use different AMIs to launch instances when you require instances with different configurations.
An AMI provides the information required to launch an instance, which may include Base Operating system, application dependencies, and other runtime libraries required.
An AMI includes the following:
- One or more Amazon Elastic Block Store (Amazon 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.
Related content:
- How to use terraform to Launch an AWS EC2 Instance
- How to use Terraform AWS EC2 user_data – aws_instance
- Terraform AWS VPC with Public and Private subnets with NAT
- How to Create AWS VPC Peering in same account/region using Terraform
Packer
Packer is a free and open source tool for creating golden images for multiple platforms from a single source configuration. It is lightweight, runs on every major operating system, and is highly performant, creating machine images for multiple platforms in parallel. It is made by Hashicorp to create identical machine images for multiple platforms from a single JSON config file. It gives you the flexibility of building your custom AMI for use in AWS EC2 platform.
Packer provisioners
Provisioners are components of Packer that install and configure software within a running machine prior to that machine being turned into a static image. They perform the major work of making the image contain useful software. Example provisioners include shell scripts, ansible, Chef, Puppet, etc.
Step 1 – Ensure packer is installed
Since we are using packer, we have to make sure that it is installed before proceeding.
If you are an ubuntu user, use these commands to install packer:
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install packer
If you are using rhel based OS like Rocky Linux or Alma linux:
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install packer
If you are using mac or homebrew package manager, use this command to install:
brew tap hashicorp/tap
brew install hashicorp/tap/packer
For other operating systems, checkout packer downloads page here.
Step 2 – Create packer project
Create a project directory and switch to it:
mkdir packer
cd packer
Under the project, create a folder called scripts that we will use for our provisioner.
mkdir scripts
This is my current directory structure
➜ tree packer
packer
└── scripts
1 directory, 0 files
Step 3 – Creating Packer templates
Packer reads its configuration either from a json or hcl file. In this guide, we are going to create our configuration template in json. We are going to define variables, builders and provisioners.
Let us create an example nginx web server. Open a webserver.json
template file using your favourite text editor, I am using vim in my case:
vim webserver.json
Add this content to the file:
{
"variables": {
"name_prefix": "webserver",
"aws_region": "{{env `AWS_REGION`}}",
"subnet_id": "subnet-xxxxx",
"vpc_id": "vpc-xxxxx"
},
"builders": [{
"type": "amazon-ebs",
"region": "{{user `aws_region`}}",
"instance_type": "t2.micro",
"ssh_username": "rocky",
"subnet_id": "{{user `subnet_id`}}",
"vpc_id": "{{user `vpc_id`}}",
"ami_name": "{{user `name_prefix`}}-v{{isotime \"200601021504\"}}",
"ami_description": "Citizix Web Server Image",
"associate_public_ip_address": "true",
"ami_block_device_mappings" : [
{
"device_name" : "/dev/sda1",
"volume_size" : "8",
"delete_on_termination" : true
}
],
"source_ami_filter": {
"filters": {
"name": "Rocky-8-*.x86_64-*"
},
"owners": ["aws-marketplace"],
"most_recent": true
}
}],
"provisioners": [
{
"type": "shell",
"scripts": [
"scripts/webserver.sh"
]
}
]
}
Under variables key section, set required variables. In my case I am setting the image name, aws region which is obtained from the env variable AWS_REGION
, subnet id and vpc id.
Under builders key section, set the aws properties for the source image and the name of the image to build. The source_ami_filter will filter the latest rocky instance to use for the build. Consult the AMI Builder documentation for more details.
On provisioners section, provide the paths to your scripts to be executed during build. In my case, I am defining a script in scripts/webserver.sh
.
Step 4 – Create provisioners scripts
Finally, let us define the script that will be executed when the ami is being build. In our case, since we want to set up nginx to serve basic content, we will install nginx and create a hello world file to be served.
Open the script file with your text editor:
sudo vim <span style="font-size: calc(11px + 0.2em);">scripts/webserver.sh</span>
Add this content to the file:
#!/bin/bash -xe
sudo dnf -y update
sudo setenforce 0
sudo sed -i s/^SELINUX=.*$/SELINUX=permissive/ /etc/selinux/config
sudo dnf install -y epel-release
sudo dnf install -y vim wget curl telnet htop
sudo dnf install -y nginx
sudo bash -c "cat > /usr/share/nginx/html/hello.html <<EOC
Hello world from Citizix.
EOC"
sudo systemctl start --now nginx
Step 5 – Run the packer build
First ensure that you are logged in to aws. You I have a profile called citizix
where I have added my credentials. The commands below will set the AWS region and citizix profile to be active.
export AWS_REGION=eu-west-1
export AWS_PROFILE=citizix
Next let is build our ami. We can save the build log to build-artifact.log
so we can refer to it in future.
packer build -machine-readable webserver.json | tee build-artifact.log
Once done with provisioning, packer will Stop and destroy temporary instance used, then create an AMI. AMI ID is printed at the end.
Step 5: Testing AMI Created
In this section, I’ll use Terraform to provision a new instance with created AMI. The same can be done from AWS console. We are going to create an AWS instance using the image we build. We will use terraform to achieve this.
Before proceeding, ensure that you have terraform installed. confirm with this command:
➜ terraform --version
Terraform v1.2.0
on darwin_arm64
Create terraform projects directory.
mkdir terraform
We are querying the latest ami matching the webserver ami we created then using it. Add these content to main.tf
.
provider "aws" {
region = "eu-west-1"
}
data "aws_ami" "web" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["webserver-*"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
module "ec2-instance" {
source = "terraform-aws-modules/ec2-instance/aws"
version = "~> 4.0"
name = "test-webserver-instance"
ami = data.aws_ami.web.id
associate_public_ip_address = true
disable_api_termination = false
instance_type = "t3.small"
key_name = "id_citizix"
monitoring = true
subnet_id = "subnet-xxxxxx"
vpc_security_group_ids = [
aws_security_group.ec2-instance-sg.id
]
root_block_device = [
{
volume_size = 30
volume_type = "gp2"
delete_on_termination = true
},
]
}
resource "aws_security_group" "ec2-instance-sg" {
name = "test-webserver-instance-sg"
description = "Test webserver instance SG "
vpc_id = "vpc-xxxxxxx"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
output "public-ip" {
value = module.ec2-instance.public_ip
}
The next section is to create the resources usig terraform. Initialize terraform using this command:
terraform init
Show an execution plan.
terraform plan
Finally apply the changes. You will be shown the execution plan then prompted to confirm the changes by typing yes.
terraform apply
The new instance will be created and its public IP will be shown as part of the outputs. You can also see it in AWS console.
To confirm that out provisioner is working, visit http://server_ip/hello.html
.
Once you are done with the test, you should delete the resources to avoid incurring costs. To destroy your test infrastructure, run this command:
terraform destroy
Conclusion
In this guide we learnt how to use packer to package an AWS ami with the help of a script provisioner.[][1]
[1]: data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221024%22 height=%22194%22%3E%3C/svg%3E