Using a programmatic approach to creating images is a great way to ensure consistency and standardise your approach. By using a tool like Packer, we can do this reliably, and integrate the build process into existing DevOps Pipelines and deployments. At the current time, there are two Packer Azure Builders; azure-arm and azure-chroot – for this post, we will use the azure-arm builder. A high level overview of the Process and Resources involved is shown below:
✅ In this post, I will run through the process to use Packer with Azure – starting with a simple configuration, we will run through the whole process end to end, in around 30 minutes.
Why Use Packer?
There are a range of use cases for Packer, from creating programmatic builds within your own pipelines, to providing customisation of your images. In my experience, here’s a few key use cases:
- Creating images reliably and consistently using a templated approach.
- Integrating the image build into an existing DevOps approach or pipeline.
- Adding applications, configuration tweaks, patches or updates to marketplace images.
- Enhancing the consistency and security of created images by removing the human aspect, using a Packer template each time.
- Creating templates that can be used by other teams, without the need to understand the Packer process – e.g. Self Service VMs.
Getting Setup
To get started, we need two things installed for the process I will use in this post. Packer, and the Azure CLI. Fortunately both are easy to install – as per the majority of my posts, I will use the awesome Chocolatey package manager to install them both. Run the below in an elevated PowerShell window to complete this process:
#Chocolatey setup and installation of Packer and Azure CLI #Set Execution Policy to allow script to run Set-ExecutionPolicy Bypass -Scope Process -Force #Chocolatey install iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) #Chocolatey apps choco install packer -y choco install azure-cli -y
Once the script completes we have everything installed that we need to use Packer!
Creating a Service Principal
Next up we need to create a Service Principal for Packer to use. The Service Principal defines the credentials and access that Packer will have to our Azure AD Tenant and the Resources within. In this case, we need access to at least a Resource Group where we can create Resources (a build VM and associated components, and an image from that VM).
To create a Service Principal, run the code below:
az ad sp create-for-rbac --name packer --role contributor --scopes /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx --query "{ client_id: appId, client_secret: password, tenant_id: tenant }"
✅ Note: Remember to add your Subscription ID and you can also scope this down further to a single Resource Group should you require by using “–scopes /subscriptions/mySubscriptionID/resourceGroups/myResourceGroupName”
This will then output something similar to the below:
For obvious reasons, the Subscription ID, Client ID, Client Secret, and Tenant ID have been hidden in the above – but you’ll want to make a note of these as we will need these later on.
⚠ Note: Treat Client ID and Client Secret like you would a password!
If you check your Subscription or Resource Group after running the above, you should see the Service Principal in place, with the requested Role too:
Creating a Template
Next up, we need to create a template which will allow us to define the image that packer will create. This template contains things like the OS Type, Image Offer and SKU, and more. Within this template, we will also define any additional configuration that we wish to take place on the Image whilst Packer is running.
Firstly, we need to add the Required elements to the template – these are the elements that cannot be omitted, or Packer will not have the required information to run. For a full list, see here: https://www.packer.io/plugins/builders/azure/arm#required. There are also a number of Optional elements that can be included to customise how Packer will run and control things like outputs/build processes and more. For a full list of the Optional ones, see here: https://www.packer.io/plugins/builders/azure/arm#optional.
Within my demo template, we will include the following elements:
- managed_image_resource_group_name – This defines the Resource Group our Image File will be created in.
- managed_image_name – This defines the name our Managed Image will be given.
- os_type – This defines the OS type.
- image_publisher – This defines the Image Publisher (based on the Marketplace).
- image_offer – This defines the Image Offer (based on the Marketplace).
- image_sku – This defines the Image SKU (based on the Marketplace).
- azure_tags – This adds any tags we required.
- build_resource_group_name – This defines the Resource Group the build will be done within.
- vm_size – This defines the size of the VM that will be used to create our build.
I will also include the following WinRM section, to allow Packer to communicate with the Build VM:
"communicator": "winrm", "winrm_use_ssl": true, "winrm_insecure": true, "winrm_timeout": "5m", "winrm_username": "packer",
✅ If you wish to download a copy of the configuration file I will be using, you can do here: https://github.com/jakewalsh90/Packer-Azure/blob/main/Windows%20Desktop/win11-21h2-avd-choco.json
A full overview of the Packer JSON template I will be running for this deployment is also shown below:
{ "builders": [{ "type": "azure-arm", "client_id": "__client_id__", "client_secret": "__client_secret__", "subscription_id": "__subscription_id__", "managed_image_resource_group_name": "packer-images", "managed_image_name": "packer-win11-21h2-avd", "os_type": "Windows", "image_publisher": "MicrosoftWindowsDesktop", "image_offer": "Windows-11", "image_sku": "win11-21h2-avd", "communicator": "winrm", "winrm_use_ssl": true, "winrm_insecure": true, "winrm_timeout": "5m", "winrm_username": "packer", "azure_tags": { "environment": "demolabv1-packer" }, "build_resource_group_name": "packer-build", "vm_size": "Standard_D2s_v4" }], "provisioners": [{ "type": "powershell", "inline": [ "while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }", "while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }", "Set-ExecutionPolicy Bypass -Scope Process -Force", "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12", "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))", "choco install 7zip -y --force --force-dependencies", "choco install firefox -y --force --force-dependencies", "choco install fslogix -y --force --force-dependencies", "& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit", "while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }" ] }] }
The Provisioners Block
The Provisioners Block is probably the most used element within a Packer JSON file. Within this block we can define actions that we wish to have carried out. Provisioners add functionality to machine images by creating configurations, installing applications/patches/updates, and making other changes as required.
✅ Within my code examples, I have used a PowerShell provisioner to install Chocolatey, install a number of applications, and then sysprep the image, ready for further deployment and use.
You can read more about Provisioners here: https://www.packer.io/docs/provisioners
Running Packer
We are now almost ready to run Packer to create our image. Before we can do this, we need to add the Service Principal details that we created earlier. To do this, take the Client ID and Client Secret we created earlier, along with your Subscription ID, and add them into the section shown below of the JSON file:
"client_id": "__client_id__", "client_secret": "__client_secret__", "subscription_id": "__subscription_id__",
As a final step before we run Packer, we need to create the Resource Groups defined under “managed_image_resource_group_name” and “build_resource_group_name” so that Packer has a location to build within, and store the created image. Use the below Azure CLI command to achieve this:
az group create -n packer-images -l uksouth az group create -n packer-build -l uksouth
Once this is done, we can run Packer! To do this, follow the steps below:
- Open a console session or command prompt, and browse to the folder containing your Packer JSON Template
- Run the following command:
./ packer build filename.json
Once this begins, you will see a process like the below taking place:
As the build Runs, you will see Resources in Azure being created – these are the Resources where our build is being run:
Once the build process completes, you will receive a message like the one below:
You should also see the Image within the Azure Portal. I’ve run all 3 of my templates (available within my GitHub Repo) so I have three images in the Azure Portal:
We now have 3 machine images that are ready for testing. We can import these images into a Compute Gallery, or we can test directly by creating a Virtual Machine from any of these images. For a simple test, I will use the “Create VM” option shown below:
Once we have followed the usual VM creation wizard, we will be able to access our VM and confirm that Packer has run successfully, by checking the Provisioner has installed the requested applications. Below you can see me connected into the VM:
What about the Compute Gallery?
Should you wish to have Packer import the created image into an Image Gallery, you will need to add a few additional elements. Firstly, you will need a Compute Gallery, you can add this using the Azure CLI Command below:
az sig create --resource-group packer-images --gallery-name PackerImages
Once this command runs, you will have a Compute Gallery:
To integrate this gallery into your packer configuration you will need to add details of the image and gallery into your JSON template, using the below references:
- shared_image_gallery_destination
- shared_image_gallery_timeout
- shared_gallery_image_version_end_of_life_date
- shared_image_gallery_replica_count
- shared_gallery_image_version_exclude_from_latest
For an example of this and to read more – see here: https://www.packer.io/plugins/builders/azure/arm#shared_image_gallery_destination
Next Steps and further reading
To continue learning about Packer, I’d recommend testing out integrations into DevOps Pipelines, and working with other Provisioners – perhaps try integrating custom applications or data as part of the Build Process. To help further learning and reading, I’ve put together the following links:
- Using Packer with Azure DevOps – https://jakewalsh.co.uk/azure-devops-using-packer-to-create-images/
- PowerShell: How to use Packer to create virtual machine images in Azure – https://docs.microsoft.com/en-us/azure/virtual-machines/windows/build-image-with-packer
- Creating Linux Images with Packer – https://docs.microsoft.com/en-us/azure/virtual-machines/linux/build-image-with-packer
- Packer Azure Overview – https://www.packer.io/plugins/builders/azure
Until next time – enjoy! ?