Create your own Azure Images using Packer

Custom images in Azure for VM deployments

Subscribe to my newsletter and never miss my upcoming articles

Listen to this article

Azure Virtual Machines (VM) are generated using either an image from the Azure Marketplace or an image you created. There are many reasons for creating an image rather than using the Azure Marketplace but the process to create and maintain can be time consuming, this is where Packer comes in. Using Packer, I found managing a custom image as easy as it can be. In this post I will cover the process I took when creating an Ubuntu image and how I included commands to run as part of the imaging process to upgrade existing and install new packages.

You will need the following to continue:

  • Service Principal with permissions to create resources within your subscription (require the Client ID and Secret);
  • Tenant ID where the Service Principal was created;
  • Subscription ID where you'll be creating the image;
  • Resource Group name that the image will be stored once created.

What is Packer

Packer is a HashiCorp tool for creating OS images for platforms like Azure. The tool requires a user to create a json file which includes the configurations needed so it can connect to the designated cloud provider. Once connected, it will start building a VM and any additional resources it requires, then runs any custom scripts specified in the json file and finally captures an image and deletes all resources other than the image file.

Where to start

You need to install Packer locally to your computer before going further with the instructions. Follow the Packer documentation on how to do this for your operating system.

We need to create a file, I am calling mine coreimage.json. Once the file is created, open it using a code editor like Visual Studio Code. First, we are going to initially configure the Builders so Packer knows how to connect to Azure, what resource group to store the image once created, etc… Below is my example code which you can copy and replace the following variables:

CLIENT_ID - Service Principal ID

CLIENT_SECRET - Service Principal Secret

TENANT_ID - The Tenant ID where the Service Principal is created

SUB_ID - The Subscription ID where the image is going to be created

RGP_NAME - Name of the Resource Group where the image will be stored once created

IMG_NAME - The name you want to give the image

{
    "builders": [{
      "type": "azure-arm",

      "client_id": "CLIENT_ID",
      "client_secret": "CLIENT_SECRET",
      "tenant_id": "TENANT_ID",
      "subscription_id": "SUB_ID",

      "managed_image_resource_group_name": "RGP_NAME",
      "managed_image_name": "IMG_NAME",

      "os_type": "Linux",
      "image_publisher": "Canonical",
      "image_offer": "UbuntuServer",
      "image_sku": "18.04-LTS",

      "azure_tags": {
          "dept": "Dev"
      },

      "location": "UK South",
      "vm_size": "Standard_B1ms"
    }],

I am using Ubuntu for this deployment so will be using the Canonical 18.04 image from the Azure Marketplace. I have set the location to UK South as this is the closest Azure region to me, and I've set the vm size to Standard B1ms. All these settings can be changed to suit your needs.

Once Builders is configured to how you want it, we can move onto configuring Provisioners which will allow us to execute commands within the operating system. We will execute apt update to download the latest package information from sources and apt upgrade to install any newer versions of software based on the information that was downloaded. We will also execute the installation of Python and Go as part of this deployment:

    "provisioners": [
      {
      "execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",
      "inline": [
        "apt update",
        "apt upgrade -y",
        "apt install python3.8 -y",
        "apt install golang-go -y"
      ],
      "inline_shebang": "/bin/sh -x",
      "type": "shell"
    },

Now we are ready to input the commands to generalize the VM so it can be imaged (like you would do with sysprep):

    {
      "execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",
      "inline": [
        "/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
      ],
      "inline_shebang": "/bin/sh -x",
      "type": "shell"
    }
  ]
  }

Your file should look something like this (make sure to save):

{
    "builders": [{
      "type": "azure-arm",

      "client_id": "CLIENT_ID",
      "client_secret": "CLIENT_SECRET",
      "tenant_id": "TENANT_ID",
      "subscription_id": "SUB_ID",

      "managed_image_resource_group_name": "RGP_NAME",
      "managed_image_name": "IMG_NAME",

      "os_type": "Linux",
      "image_publisher": "Canonical",
      "image_offer": "UbuntuServer",
      "image_sku": "18.04-LTS",

      "azure_tags": {
          "dept": "Dev"
      },

      "location": "UK South",
      "vm_size": "Standard_B1ms"
    }],
    "provisioners": [
      {
      "execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",
      "inline": [
        "apt update",
        "apt upgrade -y",
        "apt install python3.8 -y",
        "apt install golang-go -y"
      ],
      "inline_shebang": "/bin/sh -x",
      "type": "shell"
    },
    {
      "execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",
      "inline": [
        "/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
      ],
      "inline_shebang": "/bin/sh -x",
      "type": "shell"
    }
  ]
  }

Deploy with Packer

Before deploying, always run a validate to confirm the format is correct and there are no errors. You will need to launch a terminal within the directory where the file is stored and run:

packer validate ./coreimage.json

You can deploy the config file to Azure if you didn't receive any errors. Using the same terminal session, run the following command:

packer build ./coreimage.json

During the build, you will notice a new resource group is created and within it a number of resources, all with random names. For the image to be generated, a virtual machine is created along with required resources for a VM. Once the VM is created it will start executing the commands specified in the configuration file, shutdown the VM and trigger the imaging services where it will store the image in the resource group specified. At the end of the deployment, the temporary resources will automatically be deleted leaving only just the image created.

You can find the above code in my examples GitHub repository.

Interested in reading more such articles from James Cook?

Support the author by donating an amount of your choice.

Recent sponsors

No Comments Yet