1. What is a gold image?
I have recently been toying with Packer and Terraform to create a gold 2019 image. Traditionally gold images have been used to keep an image up to date that everyone uses within a company to build from. The image will be up to date with the latest security, feature and hotfixes and this will/should normally happen on/around patch day each month. You will also have various base software installed on the image; this software will/should also be up to date.
The idea is that the software is shipped/baked into the image so when it gets used in a deployment, there’s a bare bones / minimum requirement build already on it, i.e., various proxy tools.
In the traditional sense, this is great but with a few caveats. In my personal experience from being an EUC and using SCCM daily in past roles, producing builds, patching etc – my views are the following:
- Its time consuming and cumbersome, people hate doing it.
- It tends to use older tech such as SCCM, whilst great - it relies upon on-prem resources, lan link data restrictions (time of day etc), BITS etc.
- You can easily download and update images using other means, such as Azure Update Management or tie Windows Update directly into your VMs but limit what they can install.
- No automation, slow and costly compared to most cloud solutions. You have typically drives full of redundant data which is readily available and hosted already in the cloud for free.
Aside from the above, there’s various reasons as to why staying with this ideology fits but if we were starting fresh, you’d probably want to use something like packer if I’m being honest.
2. What is a Packer?
So why use packer?
Packer is essentially a tool from Hashicorp (same as Terraform) used to package images up with, vs the above here’s a few high-level things to note:
- Its fully automated once setup (you can gate things if needed), and free.
- It plugs right into azure and gets the latest images, rather than having to login with an account online and manually download an ISO.
- All your patch data is already in the cloud, no requirement to store it on-prem or in storage accounts in the cloud.
- No need to learn about SCCM, un-attend xml files, WSUS etc - once setup all you really need to know is how to read the code itself.
- Easily install software from various sources, such as a private chocolatey repo or even better MSIX App Attach, or use the apps delivered via Intune instead. So many new options!
- The images can be used both on prem, or in the cloud on Azure, AWS, GCP, Citrix etc and they can also be used for your AVD solution. Meaning, you can use 1 image for everything, instead of replicating the same task again and again separately. The ability to be cloud agnostic is key nowadays, and this is a simple and easy way to achieve that.
- Integrate the solution into Azure DevOps, utilizing a fully automated build pipeline in that your gold image packer process is triggered, this in turn creates a new gold image and all your subsequent ADO builds reference that latest image, zero human interaction is required once it’s all been configured.
- Azure Image Builder is based upon Packer, and although you could reference everything separately (arm, json etc), it makes more sense to integrate Packer into your existing codebase for Terraform and have it all under one roof in one similar format. You will finds things much easier to learn this way, everything looks familiar and it all fits into your existing code stack, plus there’s huge support online for any issues you might find.
3. How do I set this up (high level)?
I’ll run through a very high-level overview of its implementation:
- Create a Service Principal Account within Azure or use a previous one you’ve created if in your own dev environment, if it’s the same tenant/sub etc then it’ll work.
- You can usually just get Azure DevOps to create this when you create your first pipeline.
- Upload your packer pipeline file into ADO, preferably using a pkr.hcl file format. I would suggest going with a decent VM to build from, 4 cores, 8gb and an SSD minimum.
- I used a B2 box, and it took 27 minutes which isn’t slow by any means but that’s a bottom tier spec.
- Setup the Packer Pipeline using your agent with the following steps.
- Install latest packer version
- Run packer version and reference your packer template location
- Run packer init and reference your packer template location
- Run packer validate against your code, set variables to contain atleast: WorkingDirectory=${System_DefaultWorkingDirectory}
- Run packer build as per steps ii-iv.
Example of packer pipeline code:
packer {
required_plugins {
windows-update = {
version = "0.12.0"
source = "github.com/rgl/windows-update"
}
}
}
variable "WorkingDirectory" {
type = string
default = ""
}
variable "client_id" {
type = string
default = "place your own id here"
}
variable "client_secret" {
type = string
default = "place your own secret here"
sensitive = true
}
variable "location" {
type = string
default = "uk south"
}
variable "managed_image_name" {
type = string
default = "fisontech_gld_img"
}
variable "managed_image_resource_group_name" {
type = string
default = "fisontech_gld_img"
}
#### this next couple lines of code you use to determine the image
variable "offer" {
type = string
default = "WindowsServer"
}
variable "publisher" {
type = string
default = "MicrosoftWindowsServer"
}
variable "sku" {
type = string
default = "2016-Datacenter"
}
####
variable "subscription_id" {
type = string
default = "insert your own sub here"
}
variable "tenant_id" {
type = string
default = "insert your own tenant here
variable "vm_size" {
type = string
# could bump this up to a beefier machine
default = "Standard_B2s"
}
source "azure-arm" "windowsvm" {
async_resourcegroup_delete = true
client_id = var.client_id
client_secret = var.client_secret
communicator = "winrm"
image_offer = var.offer
image_publisher = var.publisher
image_sku = var.sku
location = var.location
managed_image_name = var.managed_image_name
managed_image_resource_group_name = var.managed_image_resource_group_name
os_type = "Windows"
private_virtual_network_with_public_ip = "false"
subscription_id = var.subscription_id
tenant_id = var.tenant_id
vm_size = var.vm_size
winrm_insecure = "true"
winrm_timeout = "3m"
winrm_use_ssl = "true"
winrm_username = "packer"
}
# down here you'd start looking at maybe adding software packages in, I would suggest using MSIX AppAttach though or Intune and install post-build.
build {
sources = ["source.azure-arm.windowsvm"]
provisioner "windows-update" {
filters = ["exclude:$_.Title -like '*Preview*'", "include:$true"]
search_criteria = "IsInstalled=0"
update_limit = 25
}
provisioner "windows-restart" {
restart_check_command = "powershell -command \"& {Write-Output 'Machine restarted.'}\""
}
provisioner "powershell" {
inline = ["if( Test-Path $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml ){ rm $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml -Force}", "& $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; Write-Output $imageState.ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Start-Sleep -s 10 } else { break } }"]
}
# Orignal: https://gist.github.com/jackwesleyroper/ceeea0b927008c69722ac0268d1f0643#file-packer_win_update-pkr-hcl
# Also great guide here (as per above github), highly recommend giving it a read: https://faun.pub/creating-vm-images-in-azure-with-packer-hcl-using-azure-devops-pipelines-701347964cb8
The packer build process will perform something along the lines of the following:
- Create a temporary resource group to host your image inside.
- Create a temporary VM and reference your image i.e., 2019.
- Scan windows update, install missing patches.
- You can opt out of certain types of patches such as versions or previews etc.
- You can also set variables such as limits on how many updates.
- Perform a system prep and generalize, and mark down as being OOBE.
- Transfer the image with your gold image name into your image blade within Azure (or any other cloud provider).
4. How do I use the image (high level)?
Once the image is in the image folder, its then free to use within your Terraform builds, for example depending on your azurerm module being used for virtual machines.
You will use one of the following:
- Azurerm_windows_virtual_machine = source_image_id = data.azurerm_image.example.id
- Azurerm_virtual_machine = storage_image_reference id = “${data.azurerm_image.custom.id}"}
The first example uses the newer vm, but splits it out into windows and Linux, the other option splits the vm types within the template itself where you define the os_profile_window_config etc. I personally prefer the newer method as its more declarative but it’s up to you, obviously the older method is no longer being developed so be aware of that – you won’t be getting any new features / updates.
Example here of an all in one code block:
provider "azurerm" {
# note no specific version has been used, in production you might want to change this i.e
# version = "=2.3.0"
features {}
}
# Place this code in your regional variables file for example if you use different regional images
# variable "custom_image_resource_group_name" {
# description = "The name of the Resource Group in which the Custom Image exists."
# default = "fisontech_gld_img"
# }
# Standard custom image reference, I use this in dev but the above would be preferable in production cases
variable "custom_image_name" {
description = "The name of the Custom Image to provision this Virtual Machine from."
default = "fisontech_gld_img"
}
# I.e Keep this code here if using the above values and reference below
# data "azurerm_image" "custom" {
# name = "${var.custom_image_name}"
# resource_group_name = "${var.custom_image_resource_group_name}"
# }
# comment out if wanting to use gallery
# data "azurerm_shared_image" "example" {
data "azurerm_image" "example" {
name = "fisontech_gld_img"
# gallery_name = "my-image-gallery"
resource_group_name = "fisontech_gld_img"
}
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "uksouth"
}
resource "azurerm_virtual_network" "example" {
name = "example-network"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
}
resource "azurerm_subnet" "example" {
name = "internal"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.2.0/24"]
}
resource "azurerm_network_interface" "example" {
name = "example-nic"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.example.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_windows_virtual_machine" "example" {
name = "example-machine"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
size = "Standard_F2"
admin_username = "adminuser"
admin_password = "P@$$w0rd1234!"
network_interface_ids = [
azurerm_network_interface.example.id,
]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_id = data.azurerm_image.example.id
# comment out if wanting to use gallery
# source_image_id = data.azurerm_shared_image.example.id
# Original: https://stackoverflow.com/questions/67178590/deploy-azure-vm-based-on-customize-image-available-on-shared-image-gallery
}
5. Summing things up!
Once you start playing around with the above modules it’ll make sense quick enough, sometimes when writing up blogs like this its better to leave some bits out for a few reasons, mainly things change a lot and it gets you thinking about how things connect, but if you need clarification just give me a shout. In the bottom of the code I have listed its original source should you want to read that also/instead.
The great thing about this is once you’ve got your imaging process all smoothed out, it’s a simple case of switching out a few lines in the terraform code and you’re golden, essentially replace your image references of sku etc with the image you created. All this once in place will just look after itself, if the image name stays static – then again you can use variables and within your modules that you can use to reference different images, so when someone rolls out a Linux VM it always uses x and y images as opposed to using static values.
If you think about the bigger picture, in that most companies have various department that look after each of the images separately in a traditional sense. In this method, its one image for it all and no-one needs to lift a finger once its all in place, it’s worth the work setting it up for the long-term benefits and gains to free your workforce up.
Hope this helps, thanks for reading!