An image is required to deploy Azure Virtual Machine on Cloud. You can use a market place image that Azure provides. However, if you want to use a custom image with customization based on your company standards and baselines, you need to create a custom image.
Custom image can be used to bootstrap deployments and ensure consistency across multiple VMs.
In this post I will share a script to automate image creation process for Windows VMs using powershell and HashiCorp Packer. Packer helps you create an image with customization through it’s Builders and Provisioners.
Before we start, we do have some pre-requisites.Most of those will be incorporated in the script so that we can have a seamless automation. However, you need an Azure Subscription with enough access to create resources in the Subscription and rights to create a Service Principal in Azure AD. Packer is using a Service Principal with Contributor access to the subscription. In addition, you need Powershell Az Module installed and imported. I am skipping this check in the script.So, make sure you have those before you start. Refer Microsoft document if you face any issue.
Let’s start the process. I will explain script blocks as it comes.
I will use Chocolatey to download Packer on the Windows system on which you are running this script. So, even before we install Packer, we will install Chocolatey using below script block. I am using installation method provided in Chocolatey documentation.
$checkChoco = choco --version
if([String]::IsNullOrEmpty($checkChoco)){
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
Next, we will use Chocolatey to install Packer with following script block:
$checkPacker = packer --version
if([String]::IsNullOrEmpty($checkPacker)){
choco install packer
}
Make sure, you have logged in to Azure.If you are fine to be prompted during the script run, you can add Connect-AzAccount in your script.
Next, I will set the correct Context to connect to the right Subscriptions in case you have many Subscriptions to manage. Don’t worry about those variables. I will post the complete script and they are all defined there!
$azContext = Set-AzContext -Subscription $subscriptionId
if([String]::IsNullOrEmpty($azContext)){
Write-Host "Error: Azure Context has not been set.Either you don't have permission to the subscription or some other issue!" -Foregroundcolor Red
Exit
}
Next, we will create a Resource Group to host my Windows VM image if it is not already there. I am using a sample name, change it based on your naming standard.
$azResourceGroupName = "azure-image-store-rg"
if(!(Get-AzResourceGroup -Name $azResourceGroupName -ErrorAction SilentlyContinue)){
New-AzResourceGroup -Name $azResourceGroupName -Location $location
}
$rgInfo = Get-AzResourceGroup -Name $azResourceGroupName
Now, we will create a new Service Principal. We will also assign “Contributor” RBAC role on this Subscription to this Service Principal. I am creating this Service Principal with a secret to expire in 3 Hours. You may change it based on your requirements. As mentioned above, Packer uses it create temporary azure resources in the Subscription.It will remove those automatically by the end of the process. I will remove the role assignment and delete SP through this script. We will need Application Client Id , Client Secret, Object Id and Tenant Id in the Packer Template JSON file.
$spInfo = New-AzADServicePrincipal -DisplayName "az-packer-image-create-sp-$((Get-Date).ToString("dd-MMM-yyyy-HHmmss"))" -Scope "/subscriptions/$($subscriptionId)" -EndDate (Get-Date).AddHours(3)
$applicationId = $spInfo.ApplicationId
$objectId = $spInfo.Id
$applicationSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($spInfo.Secret))
start-sleep -Seconds 20
$tenantId = Get-AzTenant | Select-Object -ExpandProperty Id
Next, we will will generate a name for the image. Some random text will be appended as suffix to the original name(Just to make it unique!).
$azImageName = "$($imageNamePrefix)-$($imageSku)-$((Get-Date).ToString("dd-MMM-yyyy"))"
$imgDetails = Get-AzImage -ResourceGroupName $azResourceGroupName -ImageName $azImageName -ErrorAction SilentlyContinue
if(!([String]::IsNullOrEmpty($imgDetails))){
$azImageName = "$($azImageName)-$(-join ((65..90) + (97..122) | Get-Random -Count 6 | ForEach-Object {[char]$_}))"
}
Next, we will perform two very important tasks. First, we will generate a Template JSON file. This file contains all required Builders, Provisioners and some inline commands in the Provisioners to customize the image and finally Sysprep and Generalize it. Here is the script block for it. Just a small disclaimer! I am giving a short sample of commands that I am using to customize my Windows image.Feel free to add your changes accordingly or you can create a different script file and call it in the Provisioner to make all necessary changes. I set the winrm_timeout to 20 minutes as sometimes, it takes time before winrm responds back. You will see it during Packer run. Packer logs every thing in the console. It also, gives you option to store this log in a separate file.
If you are running this script behind Proxy, make necessary enhancements to allow communication through it.
$input = @"
{
"builders": [{
"type": "azure-arm",
"client_id": "$($applicationId)",
"client_secret": "$($applicationSecret)",
"tenant_id": "$($tenantId)",
"subscription_id": "$($subscriptionId)",
"managed_image_resource_group_name": "$($azResourceGroupName)",
"managed_image_name": "$($azImageName)",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "$($imageSku)",
"communicator": "winrm",
"winrm_use_ssl": true,
"winrm_insecure": true,
"winrm_timeout": "20m",
"winrm_username": "packer",
"azure_tags": {
"CreationDate": "$((Get-Date).ToString("dd-MMM-yyyy"))",
"ImageOS": "$($imageSku)",
"Owner": "Avengers"
},
"location": "$($location)",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"type": "powershell",
"inline": [
"Set-ItemProperty -path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa\\Kerberos\\Parameters' -Name MaxTokenSize -value 48000 -type DWord -Force",
"Set-ItemProperty -path 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VisualEffects' -Name VisualFXSetting -value 2 -type DWord -Force",
"Set-ItemProperty -path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa\\Kerberos\\Parameters' -Name MaxTokenSize -value 48000 -type DWord -Force",
"Disable-WindowsErrorReporting",
"& C:\\Windows\\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 } }"
]
}]
}
"@
$input | Out-File $jsonTemplateFile -Encoding ascii
Second, use this JSON file and execute packer build process.
powershell packer build $jsonTemplateFile
Finally, I will clean up resources which are not required any more, including SP and Template JSON file.
Write-Host "Deleting Service Principal used by Packer"
Remove-AzRoleAssignment -ObjectId $objectId -RoleDefinitionName 'Contributor' -Scope "/subscriptions/$subscriptionId"
Remove-AzADServicePrincipal -ApplicationId $applicationId -Force
Write-Host "Deleting Template JSON file"
if(Test-Path -Path $jsonTemplateFile){
Remove-Item -Path $jsonTemplateFile -Force
}
With that, let’s execute the script :
As stated above, if you have already logged in to Azure in this Powershell session, you may comment out this line. Otherwise, you will be prompted to provide your credentials:
Script creates the resource group, Service Principal and assigns role
Now, packer has started actual image creation process. It creates some temporary resources in your Subscription.
Check your Subscription in Portal, you will see something similar to this:
And, finally, we have our image is ready
That’s it! With this script you should be able to automate your Windows Image creation process in Azure. Here is the complete script. Let me know if you face any issue here.
Param(
[Parameter(Mandatory=$true)]
[String]$subscriptionId,
[Parameter(Mandatory=$true)]
[String]$imageNamePrefix,
[Parameter(Mandatory=$false)]
[String]$location = "northcentralus",
[Parameter(Mandatory=$false)]
[String]$imageSku = "2016-Datacenter"
)
# Function to create template definition json file for Packer
Function Create-JsonTemplate()
{
Try{
$input = @"
{
"builders": [{
"type": "azure-arm",
"client_id": "$($applicationId)",
"client_secret": "$($applicationSecret)",
"tenant_id": "$($tenantId)",
"subscription_id": "$($subscriptionId)",
"managed_image_resource_group_name": "$($azResourceGroupName)",
"managed_image_name": "$($azImageName)",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "$($imageSku)",
"communicator": "winrm",
"winrm_use_ssl": true,
"winrm_insecure": true,
"winrm_timeout": "20m",
"winrm_username": "packer",
"azure_tags": {
"CreationDate": "$((Get-Date).ToString("dd-MMM-yyyy"))",
"ImageOS": "$($imageSku)",
"Owner": "Avengers"
},
"location": "$($location)",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"type": "powershell",
"inline": [
"Set-ItemProperty -path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa\\Kerberos\\Parameters' -Name MaxTokenSize -value 48000 -type DWord -Force",
"Set-ItemProperty -path 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VisualEffects' -Name VisualFXSetting -value 2 -type DWord -Force",
"Set-ItemProperty -path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa\\Kerberos\\Parameters' -Name MaxTokenSize -value 48000 -type DWord -Force",
"Disable-WindowsErrorReporting",
"& C:\\Windows\\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 } }"
]
}]
}
"@
$input | Out-File $Global:jsonTemplateFile -Encoding ascii
}
Catch
{
Write-Host "ERROR : $_" -Foregroundcolor Red
Exit
}
}
# Function to create Image using Packer
Function Create-Image(){
Try{
powershell packer build $Global:jsonTemplateFile
}
Catch
{
Write-Host "ERROR : $_" -Foregroundcolor Red
Exit
}
}
# Function to remove resources created during the image creation process
Function Cleanup-Resources(){
Try{
Write-Host "Deleting Service Principal used by Packer"
Remove-AzRoleAssignment -ObjectId $objectId -RoleDefinitionName 'Contributor' -Scope "/subscriptions/$subscriptionId"
Remove-AzADServicePrincipal -ApplicationId $applicationId -Force
Write-Host "Deleting Template JSON file"
if(Test-Path -Path $Global:jsonTemplateFile){
Remove-Item -Path $Global:jsonTemplateFile -Force
}
}
Catch{
Write-Host "ERROR : $_" -Foregroundcolor Red
}
}
if(!((Get-AzLocation | Select-Object -ExpandProperty Location).Contains($location))){
Write-Host "Error: Location $($location) is not a valid Azure Region. Try again with correct Region!" -Foregroundcolor Red
Exit
}
$Global:jsonTemplateFile = "$($env:Temp)\image-template.json"
if(Test-Path $Global:jsonTemplateFile){Remove-Item -Path $Global:jsonTemplateFile -Force}
# Check if chocolatey is already installed on local system. If not, install.
$checkChoco = choco --version
if([String]::IsNullOrEmpty($checkChoco)){
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
# Check if Packer is already installed.If not, install using chocolatey
$checkPacker = packer --version
if([String]::IsNullOrEmpty($checkPacker)){
choco install packer
}
# Login to Azure Account
Connect-AzAccount
$azContext = Set-AzContext -Subscription $subscriptionId
if([String]::IsNullOrEmpty($azContext)){
Write-Host "Error: Azure Context has not been set.Either you don't have permission to the subscription or some other issue!" -Foregroundcolor Red
Exit
}
# Create a Resource Group if it is not already there
$azResourceGroupName = "azure-image-store-rg"
if(!(Get-AzResourceGroup -Name $azResourceGroupName -ErrorAction SilentlyContinue)){
New-AzResourceGroup -Name $azResourceGroupName -Location $location
}
$rgInfo = Get-AzResourceGroup -Name $azResourceGroupName
# Create a Service Principal to be used used by Packer and setting scope to the resource group only
$spInfo = New-AzADServicePrincipal -DisplayName "az-packer-image-create-sp-$((Get-Date).ToString("dd-MMM-yyyy-HHmmss"))" -Scope "/subscriptions/$($subscriptionId)" -EndDate (Get-Date).AddHours(3)
$applicationId = $spInfo.ApplicationId
$objectId = $spInfo.Id
$applicationSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($spInfo.Secret))
$tenantId = Get-AzTenant | Select-Object -ExpandProperty Id
$azImageName = "$($imageNamePrefix)-$($imageSku)-$((Get-Date).ToString("dd-MMM-yyyy"))"
$imgDetails = Get-AzImage -ResourceGroupName $azResourceGroupName -ImageName $azImageName -ErrorAction SilentlyContinue
if(!([String]::IsNullOrEmpty($imgDetails))){
$azImageName = "$($azImageName)-$(-join ((65..90) + (97..122) | Get-Random -Count 6 | ForEach-Object {[char]$_}))"
}
Create-JsonTemplate
Create-Image
$imgDetails = Get-AzImage -ResourceGroupName $azResourceGroupName -ImageName $azImageName -ErrorAction SilentlyContinue
if(!([String]::IsNullOrEmpty($imgDetails))){
Write-Host "New Image $($imgDetails.Name) created successfully!" -ForegroundColor Green
}
else{
Write-Host "Error: Something went wrong! Please try again!" -ForegroundColor Red
}
Cleanup-Resources
[…] Create Azure Windows Image with Packer and Powershell […]
[…] Create Azure Windows VM Image with Packer and Powershell […]
[…] Create Azure Windows VM Image with Packer and Powershell […]