As a follow on from my last post, I wanted to explore using Packer to create images for Azure Lab Services in more detail. Specifically, how images created using Packer could be imported into Azure Lab Services. If you’re new to Azure Lab Services, I’d recommend reading my previous post on it.
Why use Packer with Azure Lab Services?
This is the key question for this post… why would you actually want to use Packer with Azure Lab Services?
In my view, the key here is in how Packer allows us to create images programmatically, and when combined with Azure Lab Services and tooling like Terraform, we can spin up whole lab environments, along with custom images and applications very easily, repeatably, and quickly. It also means we can manage the creation, ongoing changes, and removal of Azure Lab environments, with our custom images, using DevOps toolsets like Azure DevOps, GitHub and more.
✅ In this post I’ll run through the creation of a Packer Image, the storage of that image in an Azure Compute Gallery, and then how we can integrate the Compute Gallery to Azure Lab Services, and therefore use our Custom image.
Step 1 – Creating A Compute Gallery and Packer Image
To start this process, we need to create the basic elements that Packer needs to run in Azure, namely Resource Groups, and an Azure Compute Gallery to store our image. To do this, I’ll use the Azure CLI:
az group create -l uksouth -n rg-uks-packer-build az group create -l uksouth -n rg-uks-packer-images az group create -l uksouth -n rg-uks-AzureLabsImages az sig create --resource-group rg-uks-AzureLabsImages --gallery-name AzureLabsImages az sig image-definition create --resource-group rg-uks-AzureLabsImages --gallery-name AzureLabsImages --gallery-image-definition win11-22h2-pro --publisher AzureLabsImages --offer Labs-Win11-22h2-Pro --sku Labs-Win11 --os-type windows --os-state Generalized --hyper-v-generation V2
This will create the following items:
-
3 Resource Groups, 1 for Packer build items, 1 to store the created image, and 1 for the Azure Compute Gallery.
-
1 Azure Compute Gallery
-
1 Image Definition within the Azure Compute Gallery
It’s worth noting, for this test, I am creating a Windows 11 22h2 Pro Image – if you wish to create something different to this, you’ll need to make changes to the code/templates shared within this post.
Next, we need to setup a Service Principal for Packer – to do this, use the CLI command below (remember to add your Subscription ID):
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 }"
This will output Service Principal details:
Note: These details give Contributor access to the Subscription, and should be treated as sensitive!
Next, we need to add these to our Packer template. Here’s the template file I am using – you’ll need to add your Service Principal details to the below Packer template. (Anything you need to replace is marked with “__replace this__“):
source "azure-arm" "autogenerated_1" { azure_tags = { environment = "packer" } build_resource_group_name = "rg-uks-packer-build" client_id = "__replace this__" client_secret = "__replace this__" communicator = "winrm" image_offer = "Windows-11" image_publisher = "MicrosoftWindowsDesktop" image_sku = "win11-22h2-pro" managed_image_name = "packer-win11-22h2-pro" managed_image_resource_group_name = "rg-uks-packer-images" os_type = "Windows" shared_image_gallery_destination { gallery_name = "AzureLabsImages" image_name = "win11-22h2-pro" image_version = "1.0.0" replication_regions = ["uksouth"] resource_group = "rg-uks-AzureLabsImages" } subscription_id = "__replace this__" vm_size = "Standard_D4as_v5" winrm_insecure = true winrm_timeout = "5m" winrm_use_ssl = true winrm_username = "packer" } build { sources = ["source.azure-arm.autogenerated_1"] provisioner "powershell" { inline = ["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 vscode -y --force --force-dependencies", "choco install firefox -y --force --force-dependencies", "choco install terraform -y --force --force-dependencies"] } provisioner "windows-restart" { } provisioner "powershell" { inline = ["while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }", "while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }", "& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit /mode:vm", "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 } }"] } }
To run the code above, save the file as filename.pkr.hcl and then run “packer build .\filename.pkr.hcl“.
Once the Packer build has been completed, we will see our image imported into the Compute Gallery:
✅ We can now move on to using this image within a Lab!
Step 2 – Enabling our Custom Images in Azure Lab Services
For the next part of this post, you will need a Lab Service Plan already created – if you need guidance on doing this, please refer to my previous post here. Once your Lab Service plan is in place, browse to it in the Portal, and click on “Azure compute gallery“:
Note, if you are attaching an existing Gallery, there are Permissions that must be assigned first – see here. Within the next screen, we can link our Gallery (created earlier in Step 1) to this Lab Service Plan:
Select the previously created Gallery:
The Gallery will then be attached:
Once attached, you’ll see the Gallery and it’s images displayed:
Before we can use our Windows 11 Image (shown above, in our Gallery, as “win11-22h2-pro“), we need to enable this image. To do this, tick the image and select “Enable Image” and then click on “Apply” at the bottom of the screen:
✅ Now that our Image is enabled, we can use this image when creating a new Lab Environment.
Step 3 – Using Custom Images within Azure Lab Services
Now that we have integrated the Compute Gallery and enabled our Custom Image, we can use this Image within Azure Lab Services. Firstly, let’s look at doing this within the Portal. We start by visiting labs.azure.com to create our lab. To start, click on “+ New Lab“:
Once selected, the New lab screen will appear. In order to see our custom image, click on “Virtual machine image” as shown in the screenshot below:
In this dropdown, you should then see the custom image that has been created:
Once our custom image has been selected, we can follow the rest of the Lab creation steps, firstly, with the local administrator details:
Next, it’s the Lab Policies:
Finally, we can select “Use virtual machine image without customization”, as we have already setup this image using Packer:
The Lab will now be deployed using our custom image:
Once this has been done, we need to publish the image – for my test I will just select 1 VM to test with:
This image will then show as Publishing:
Once the image is published, we can add users and access the lab. If you need a guide on this, please refer to my previous post on Azure Lab Services here. When we access the Lab, we need to power on the VM and wait for it to start – this process is outlined below:
Once the VM is started and we have clicked on the connect symbol, we are provided with an RDP file we can use to connect to our VM. Once connected, we can see our VM is running and it has the applications installed using Chocolatey within Packer on it:
✅ We’ve now completed the deployment and testing of Azure Lab Services, using a custom Packer created image with Azure Compute Gallery!
What about Terraform?
Fortunately, adding our Packer created image into a Lab Services deployment using Terraform is a very simple process. If you need a refresher on deploying Azure Lab Services using Terraform, please see my previous post here. To use our Custom Image, we just need to add two things to the deployment; The Compute Gallery to the Lab Service Plan, and our Image into the Lab deployment. Note, if you are attaching an existing Gallery, there are Permissions that must be assigned first – see here.
This is done within Terraform as follows:
Lab Service Plan:
This requires the Compute Gallery Resource ID adding to the Resource Block:
# Lab Service Plan resource "azurerm_lab_service_plan" "plan1" { name = "lsp-${var.labname}-${var.region}" resource_group_name = azurerm_resource_group.rg.name location = var.region allowed_regions = [var.region] shared_gallery_id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-uks-AzureLabsImages/providers/Microsoft.Compute/galleries/AzureLabsImages" support { email = "jake@jakewalsh.co.uk" instructions = "Welcome to your Azure Lab, please raise a support request if you encounter any issues" url = "https://jakewalsh.co.uk" } tags = { Environment = var.labname Function = "AzureLabServices" } }
Enabling our custom image – AzAPI to the Rescue!
I had a slight issue with my Lab whereby I couldn’t enable the Image once the Compute Gallery had been attached to the Lab (using the above code), and despite searching I could not find any way of doing this within the AzureRM provider (at the time of writing). The problem I found was that even though the Gallery had been attached, the Lab creation would fail as the image was not enabled:
Thankfully, the AzAPI Terraform provider was able to let me work around this using the below code:
# AzAPI to enable the Image in the Gallery resource "azapi_resource_action" "enable-image" { type = "Microsoft.LabServices/labPlans/images@2022-08-01" resource_id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-lab1-uksouth/providers/Microsoft.LabServices/labPlans/lsp-lab1-uksouth/images/win11-22h2-pro" action = "" method = "PUT" body = jsonencode({ "properties" : { "enabledState" : "Enabled" } }) depends_on = [ azurerm_lab_service_plan.plan1 ] }
With the above code set, and a depends_on for this set in the Lab code below, the image is enabled:
With this image enabled, the Lab is created without issue.
Lab
Within the Lab Resource we can specific the ID of the image:
# Lab resource "azurerm_lab_service_lab" "lab1" { name = "lab-${var.labname}-${var.region}" resource_group_name = azurerm_resource_group.rg.name location = var.region title = "Lab 1" description = "Virtual Machine for Lab Environment" lab_plan_id = azurerm_lab_service_plan.plan1.id depends_on = [ azapi_resource_action.enable-image ] connection_setting { client_rdp_access = "Public" } security { open_access_enabled = false } virtual_machine { shared_password_enabled = true admin_user { username = var.labusername password = azurerm_key_vault_secret.vmpassword.value } image_reference { id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-uks-AzureLabsImages/providers/Microsoft.Compute/galleries/AzureLabsImages/images/win11-22h2-pro" } sku { name = "Classic_Fsv2_8_16GB_128_S_SSD" capacity = 1 } } tags = { Environment = var.labname Function = "AzureLabServices" } }
✅ This concludes this post – I hope the above has been useful in demonstrating how to use Azure Compute Gallery integration, and Custom Images using Packer within Azure Lab Services.