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


Create 3 packer files.
1. The main packer HCL code (amzn_linux.pkr.hcl)
2. Variable defined (variables.pkr.hcl)
3. Variables assigned (var_set.auto.pkrvars.hcl)

In the main packer HCL code define the block that will be the source for your custom AMI

packer {
  required_plugins {
    docker = {
      version = ">= 0.0.7"
      source  = "github.com/hashicorp/docker"
    }
  }
}

source "amazon-ebs" "this" {
  ami_name      = "amzn-linux-silver-{{timestamp}}"
  ami_users     = var.share_with
  instance_type = var.inst_type
  region        = var.region
  #security_group_id           = "sg-0f4abe34913599a2e" #Use this if you don't want to use the Packer created temp SG
  #iam_instance_profile        = var.iam_profile
  associate_public_ip_address = true
  ssh_username = var.ssh_user

  aws_polling {
    delay_seconds = 30
    max_attempts  = 200
  }

  launch_block_device_mappings {
    device_name           = "/dev/xvda"
    volume_size           = var.vol_size
    volume_type           = var.vol_type
    delete_on_termination = true
  }

# To use a specific subnet uncomment the parameter below and comment out the subnet_filter block
#   subnet_id = "subnet-d9205ebc"
    subnet_filter {
      filters = {
            "tag:Name": "public"
      }
      most_free = true
      random    = false
    }

  source_ami = var.image_id #uncomment to use and comment the filter below

  # If you want to use a predefined AMI or existing AMI and you want to filter off specifics, use the below and comment out the above source_ami parameter
  # source_ami_filter {
  #   filters = {
  #     name                = "amzn2-goldimage-*"
  #     root-device-type    = "ebs"
  #     virtualization-type = "hvm"
  #   }
  #   most_recent = true
  #   owners      = ["1234567890"] #accountID sharing the GI
  # }

  tags = {
    Release       = "Latest"
    Name          = "amzn-linux-si-{{timestamp}}"
    Base_AMI_Name = "{{ .SourceAMIName }}"
  }
}

Below the source block define the build block. This will tell Packer what to bake into your AMI using provisioners. There are a variety of provisioners and the below has multiple used for example purposes.

build {
  sources = [
    "source.amazon-ebs.this"
  ]

  provisioner "shell-local" {
    inline = ["sleep 60"]
  }

  provisioner "shell" {
    environment_vars = [
      "FOO=BAR",
    ]
    inline = [
      "sudo yum update -y",
      "sudo yum install wget -y",
      "sudo yum install -y yum-utils",
      "sudo amazon-linux-extras install -y lamp-mariadb10.2-php7.2 php7.2",
      "sleep 5",
      "sudo yum install -y httpd",
      "sleep 5",
      "sudo systemctl start httpd",
      "sudo systemctl enable httpd",
      "sudo amazon-linux-extras install ansible2",
      "sleep 5",
      "echo foo equals $FOO"
    ]
  }
  provisioner "ansible-local" {
    playbook_file = "./scripts/playbook.yml"
  }

  provisioner "shell" {
    script = "scripts/install_bins.sh"
  }


  provisioner "breakpoint" {
    disable = true
  }

  post-processor "checksum" {
    checksum_types = ["sha1", "sha256"]
    output         = "packer_{{.BuildName}}_{{.ChecksumType}}.checksum"
  }
}

The second file is the variable definitions HCL. Define the variables and their type. In the main HCL code other parameters could be made into variables but for example purposes I didn’t go through that process.

# ssh_username could be set in the main HCL like the below:
ssh_username = "ec2_user"

# or you could set it as such
 ssh_username = var.ssh_user

Example above, shows you can set the variable with its expected value, or you can set it to a variable. If you assign the parameter to a variable you have to either define it in the variable files, or when you run Packer to create the build set the variable at the command line.

I prefer to first create my definitions file for variables like the below:

//  variables.pkr.hcl
// For those variables that you don't provide a default for, you must
// set them from the command line, a var-file, or the environment.


variable "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."


  validation {
    # regex(...) fails if it cannot find a match
    condition     = can(regex("^ami-", var.image_id))
    error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
  }
}

variable "share_with" {
  type = list(string)
}

variable "region" {
  type    = string
  default = "us-east-1"
}

variable "ssh_user" {
  type = string
}
variable "inst_type" {
  type = string
}

variable "vol_size" {
  type = number
}

variable "vol_type" {
  type = string
}

variable "iam_profile" {
  type = string
}

Lastly set the variables in the third file

image_id    = "ami-090fa75af13c156b4"
share_with  = [] #account ids of who you share with
inst_type   = "t3.large"
ssh_user    = "ec2-user"
vol_size    = 40
vol_type    = "gp3"
iam_profile = "demo-base-ec2-profile"

After you have your files, keep in mind if you have any scripts that will run and are called via provisioners that those exist within the proper directories to be run or the Packer build will fail.

Begin the build

packer init .
packer build .

After initiating the Packer build, a helper EC2 instance will spin up and be assigned a security group if defined in the main HCL, otherwise one will be created temporarily, as well as keypairs to facilitate the communication to the instance. The provisioners will run (some screenshots of the process are shown below but full output is not).

The code can be found on my github and some small modifications would need to be made to the example to run in other AWS environments.