Using Ansible to finalize Hashicorp Packer images


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 executed
  • playbook_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
    - name: Import the Microsoft signing key into apt
        url: "https://packages.microsoft.com/keys/microsoft.asc"
        state: present

    - name: Add the Azure CLI software repository
        repo: "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ {{ansible_distribution_release}} main"
        filename: azure-cli
        state: present

    - 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:

tasks  templates  vars


modprobe_blacklist.j2  policy-rc.d.conf.j2  policy-rc.d.j2  securetty.j2  sysctl_base.j2


The main.yml in the tasks sub-directory is used to configure the required post installation steps.


- name: Ensure default packages
    name: "{{ default_packages }}"
    state: latest

- name: Ensure latest version of all packages
    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
    src: static_files/syslog-ng-lad.conf
    dest: /etc/syslog-ng/conf.d/syslog-ng-lad.conf
    owner: root
    group: root
    mode: '0644'

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Previous Post

Perl script to monitor the rate of logs

Next Post

Deploy a PostgreSQL database with an initial schema using Ansible

Related Posts