Monday, 4 March 2024

HashiCorp Packer



Packer is a tool for creating (golden) images from a configurable template. 

Packer configuration file is a JSON file e.g. config.json.

config.json contains:
  • variables
  • builders
    • type
      •  amazon-ebs
  • provisioners
    • type
      • shell
      • ansible

Example of configuration file used to create AWS AMI for multiple environments, by using Ansible as a provisioner:

config.json:

{
  "variables": {
    "env_name": "{{env `env_name`}}",
    "instance_profile": "",
    "ami_name": "asb-linux-al23-arm-{{timestamp}}",
    "kms_key_id": "{{env `kms_key_id`}}",
    "sg_default": "{{env `SG_DEFAULT`}}",
    "current_user": "{{env `USER`}}"
  },
  "builders": [
    {
      "ami_name": "{{user `ami_name`}}",
      "instance_type": "t4g.medium",
      "region": "us-east-1",
      "source_ami_filter": {
        "filters": {
          "virtualization-type": "hvm",
          "name": "al2023-ami-2023.*-kernel-6.1-arm64",
          "root-device-type": "ebs"
        },
        "owners": "amazon",
        "most_recent": true
      },
      "ssh_username": "ec2-user",
      "ssh_bastion_host": "bastion.mycorp.com",
      "ssh_bastion_username": "{{user `current_user`}}",
      "ssh_bastion_agent_auth": true,
      "ssh_bastion_port": 22,
      "ssh_timeout": "2m",
      "ssh_clear_authorized_keys": "true",
      "iam_instance_profile": "{{user `instance_profile`}}",
      "type": "amazon-ebs",
      "tags": {
        "Name": "{{user `ami_name`}}",
        "Environment": "{{user `env_name`}}"
      },
      "vpc_filter": {
        "filters": {
          "tag:Environment": "{{user `env_name`}}",
          "isDefault": "false"
        }
      },
      "subnet_filter": {
        "filters": {
          "tag:Name": "vpc-{{user `env_name`}}-private-us-east-*"
        },
        "most_free": true
      },
      "security_group_ids": [
        "{{user `sg_default`}}"
      ],
      "launch_block_device_mappings": [
        {
          "device_name": "/dev/xvda",
          "encrypted": true,
          "kms_key_id": "{{user `kms_key_id`}}",
          "delete_on_termination": true,
          "volume_type": "gp3"
        }
      ]
    }
  ],
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "sudo dnf update -y",
        "sudo dnf install -y python3"
      ]
    },
    {
      "type": "ansible",
      "host_alias": "packer",
      "user": "ec2-user",
      "inventory_directory": "ansible/env/aws/{{user `env_name`}}",
      "playbook_file": "ansible/playbooks/playbook.yml",
      "extra_arguments": [
        "-D",
        "--vault-password-file",
        "./aws-{{user `env_name`}}-vault.pw",
        "--scp-extra-args", "'-O'",
        "--extra-vars",
        "'ansible_python_interpreter=/usr/bin/python3'"
      ]
    }
  ]
}


To run Packer:
 
$ packer build [-debug] config.json


AWS AMI gets created in the following way:
  • a temporary SSH keypair is created
  • new temporary EC2 instance is started. This instance is based on source AMI specified in configuration file
  • SSH tunnel is established between local/dev host and remote EC2 instance
  • provisioners are run e.g. Ansible is installing packages etc...
  • once provisioners work is completed ephemeral key is removed from authorized_keys file on temp EC2 instance
  • temp EC2 instance is terminated
  • a snapshot of the root disk of that EC2 instance is created
  • a new AMI is created based on that snapshot
  • tags are added to snapshot
  • tags are added to AMI
  • temp SSH keypair is destroyed

Once AMI is created, it is not possible to find out which AMI was used as a source (base) AMI (amazon web services - Is it possible to find the source AMI for an existing AMI? - Stack Overflow). But soon after AMI is created, we can check properties of temp EC2 instance (which is in Terminated state) and check its AMI. For source AMI filter as above, it could be e.g. al2023-ami-2023.3.20240219.0-kernel-6.1-arm64.

If some of provisioning steps need to access resources that belong to some other AWS account, we can add its profile to ~/.aws/credentials and then refer to that profile in the command e.g.

# Download a file from an S3 bucket
- name: Download file from S3
  command: "aws s3 cp  s3://{{ s3_bucket_name }}/{{ file_name }} /path/to/dest_directory/{{ file_name }} --profile {{ aws_profile }} --region {{ s3_bucket_region }}"
  run_once: True
  delegate_to: localhost
  become: no


When it comes to services, Packer provisioner should only install and enable them but not start. Temporary EC2 instance that Packer creates during the image creation process does not have necessary IAM roles attached (IAM role attached to EC2 instance needs to have CloudWatchAgentServerPolicy  attached to it as that policy allows to push metrics/logs to CloudWatch). Therefore it does not make sense starting the agent on that EC2 instance (as an Ansible provisioning step). But we can make service start on the instance launch/boot by using ansible.builtin.service's enabled property: 

Wrong:

- name: Restart CloudWatch agent service
  ansible.builtin.service:
    name: amazon-cloudwatch-agent
    state: restarted
  become: yes

Correct:

- name: Start CloudWatch agent service on boot
  ansible.builtin.service:
    name: amazon-cloudwatch-agent
    enabled: true
  become: yes


---

No comments: