Managed images in Azure are the foundation for creating consistent and reproducible virtual machine deployments.
Let’s see how Packer can automate their creation. Imagine you need to spin up a fleet of web servers, all with the same base OS, pre-installed software, and security configurations. Manually setting this up for each VM is a nightmare. Packer, in this context, acts as your automated image factory. It takes a base image (like a Windows Server or Ubuntu VM in Azure), applies your customizations, and then packages it into a managed image that you can use to launch new VMs. This ensures every VM starts from the exact same, known-good state.
Here’s a Packer template for building a managed image in Azure. This example uses the azure-arm builder to create a Windows Server 2019 image with IIS installed.
{
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"tenant_id": "{{env `ARM_TENANT_ID`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}",
"resource_group": "packer-images-rg",
"location": "East US",
"vm_size": "Standard_B1s",
"managed_image_name": "iis-webserver-{{timestamp}}"
},
"builders": [
{
"type": "azure-arm",
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"tenant_id": "{{user `tenant_id`}}",
"subscription_id": "{{user `subscription_id`}}",
"resource_group": "{{user `resource_group`}}",
"location": "{{user `location`}}",
"vm_size": "{{user `vm_size`}}",
"managed_image_name": "{{user `managed_image_name`}}",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2019-datacenter",
"os_type": "Windows",
"ssh_username": "packer",
"communicator": "winrm",
"winrm_username": "{{user `ssh_username`}}",
"winrm_password": "Password123!",
"winrm_timeout": "5m",
"async_mode": true
}
],
"provisioners": [
{
"type": "powershell",
"inline": [
"Install-WindowsFeature -name Web-Server -IncludeManagementTools",
"Set-ItemProperty -Path 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' -Name 'BypassHTTP1.1' -Value 0 -Type DWord -Force",
"Add-Content -Path 'C:\\inetpub\\wwwroot\\iisstart.htm' -Value '<h1>Hello from Packer!</h1>'"
]
}
]
}
The core problem this solves is the repeatability and immutability of your VM deployments. Instead of configuring VMs after they’re launched, you bake your desired state into an image. When you need a new VM, you launch it from this pre-configured image. This drastically reduces configuration drift and makes troubleshooting much simpler because you know every VM started from the same baseline.
The builders block is where the magic for Azure interaction happens. The azure-arm builder is specifically designed for Azure Resource Manager. You provide your Azure credentials (obtained via environment variables or directly in the template, though environment variables are recommended for security). The resource_group, location, and vm_size define where and how the temporary VM for building the image will be provisioned. managed_image_name is what your final image will be called. Crucially, image_publisher, image_offer, and image_sku point to the base Azure Marketplace image you want to start with. communicator and its related winrm settings tell Packer how to connect to the temporary VM to run your provisioning scripts. async_mode: true is important for Azure ARM as it allows Packer to exit while the image creation continues in the background.
The provisioners block is where you define the customizations. In this example, we use the powershell provisioner to run inline commands. We install the IIS web server role, adjust a registry setting for WinRM, and add a simple HTML file to the default web root. Packer will execute these commands on the temporary VM after it’s provisioned and before it’s captured as a managed image.
To run this, you’d first set your Azure environment variables:
export ARM_CLIENT_ID="your-sp-app-id"
export ARM_CLIENT_SECRET="your-sp-secret"
export ARM_TENANT_ID="your-tenant-id"
export ARM_SUBSCRIPTION_ID="your-subscription-id"
Then, execute Packer:
packer init .
packer build azure-image.json
Packer will then:
- Authenticate with Azure.
- Create a resource group (if it doesn’t exist).
- Create a temporary virtual machine using the specified base image.
- Connect to the VM using WinRM.
- Execute the PowerShell scripts.
- Generalize the VM (important for creating reusable images).
- Capture the VM’s OS disk as a managed image.
- Clean up the temporary VM and related resources.
A common point of confusion is the difference between a managed image and a custom VM image. Azure Managed Images are a specific type of custom image stored as a resource within your Azure subscription, optimized for VM creation. They are the recommended way to store custom VM images.
The generalized flag on the VM is critical. When Packer is done provisioning, it runs sysprep (for Windows) or similar generalization commands on the temporary VM. This removes machine-specific information like the computer name, SID, and security identifiers. If you forget to generalize, or if your provisioning scripts interfere with the generalization process, the resulting image will fail to deploy new VMs or will cause significant issues with domain joining and unique identity. Packer handles this automatically for most common OS types, but it’s a step that’s vital to understand.
After your managed image is built, the next challenge will likely be automating the deployment of VMs from that image, often using tools like Terraform or Azure CLI.