Ansible provides a more flexible way to fine-tune Hashicorp Packer images compared to cloud-init. Playbooks can be executed once the guest image building is ready and boots up for the first time. This allows to create different types of Packer images based on different playbooks.
In this article, Packer images will created for Azure using azure-arm build type. The images will use an Ubuntu image available on Azure, as base image.
Let’s consider the following head of a Packer template file:
{ "builders": [{ "type": "azure-arm", "use_azure_cli_auth": true, "subscription_id": "{{ user `subscription_id` }}", "managed_image_resource_group_name": "{{user `resource_group`}}", "managed_image_name": "{{user `managed_image_name`}}", "os_type": "{{user `basevm_os_type`}}", "image_publisher": "{{user `basevm_publisher`}}", "image_offer": "{{user `basevm_offer`}}", "image_sku": "{{user `basevm_sku`}}", "virtual_network_name": "{{user `virtual_network_name`}}", "virtual_network_resource_group_name": "{{user `virtual_network_resource_group_name`}}", "virtual_network_subnet_name": "{{user `virtual_network_subnet_name`}}", "azure_tags": { "dept": "DevOps", "task": "My custom base image" }, "location": "{{user `location`}}", "vm_size": "{{user `vm_size`}}", "os_disk_size_gb": "{{user `os_disk_size_gb`}}", "shared_image_gallery_destination": { "subscription": "{{ user `subscription_id` }}", "resource_group": "{{user `resource_group`}}", "gallery_name": "{{user `gallery_name`}}", "image_name": "{{user `base_image`}}", "image_version": "{{user `base_image_version`}}", "replication_regions": "{{user `replication_regions`}}" }, "shared_image_gallery_timeout": "2h1m1s" }],
All the variables surrounded by {{user `` }}
are template variables, and must be configured in a separate file, example my_base_image_vars.json
, which will be included later-on when initiating the Packer build command. The final image will be stored in Azure, in a Shared Image Gallery.
Packer however allows to configure extra provisioners, which will be executed once the initial virtual machine has been created and before the final image will be created.
One of those provisioner types is the ansible-local type. This provisioner allows to execute Ansible playbooks (and roles) once the virtual machine is booted, directly on the guest machine.
"provisioners": [ { "type": "shell", "inline_shebang": "/bin/sh -x", "execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'", "script": "presetup.sh" }, { "type": "ansible-local", "playbook_file": "./ansible/default_setup.yml", "playbook_dir": "./ansible", "role_paths": ["./ansible/defaults"], "clean_staging_directory": true, "staging_directory": "/tmp/packer-provisioner-ansible-local", "extra_arguments" : [ "--extra-vars", "ansible_python_interpreter=/usr/bin/python3" ] },
The first provisioner called, is the shell provisioner. This provisioner will upload and execute the script presetup.sh
. This script could for instance ensure that the Ansible package is already installed before the next provisioner is called.
The second provisioner is the ansible-local type. The provisioner requires some parameters that must be configured:
playbook_file
: the Ansible playbook which will be executedplaybook_dir
: the base directory of the Ansible playbooks, roles, static_files, …role_paths
: the path of the role which will be called in playbook_file (when required)
The above example will upload all files in the ./ansible
directory (parameter playbook_dir
) to the new guest virtual machine, to the directory configured in staging_directory
, and will call the Ansible playbook default_setup.yml
.
--- - name: Default post installation hosts: all connection: local become: yes tasks: - name: Import the Microsoft signing key into apt apt_key: url: "https://packages.microsoft.com/keys/microsoft.asc" state: present - name: Add the Azure CLI software repository apt_repository: repo: "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ {{ansible_distribution_release}} main" filename: azure-cli state: present roles: - defaults
The above Ansible playbook use the parameter connection: local
. This is very important and must be included in each playbook executed by the ansible-local provisioner.
At first the playbook will execute two tasks which will ensure that the Microsoft Azure-CLI APT repository is installed and properly set up.
Afterwards, it will call the defaults
Ansbile role. This role is set up as a typical Ansible role:
ansible/defaults: tasks templates vars ansible/defaults/tasks: main.yml ansible/defaults/templates: modprobe_blacklist.j2 policy-rc.d.conf.j2 policy-rc.d.j2 securetty.j2 sysctl_base.j2 ansible/defaults/vars: main.yml
The main.yml
in the tasks
sub-directory is used to configure the required post installation steps.
Example:
--- - name: Ensure default packages apt: name: "{{ default_packages }}" state: latest - name: Ensure latest version of all packages apt: upgrade: dist force_apt_get: yes dpkg_options: 'force-confold,force-confdef' autoremove: yes autoclean: yes - name: Update python alternatives (for Log Analytics) shell: | update-alternatives --remove-all python update-alternatives --install /usr/bin/python python /usr/bin/python2 1 - name: Forward Syslog-NG logs to Log Analytics ansible.builtin.copy: src: static_files/syslog-ng-lad.conf dest: /etc/syslog-ng/conf.d/syslog-ng-lad.conf owner: root group: root mode: '0644' ...