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 my previous post, I have discussed about automated Windows Image creation process using Packer and Powershell on Azure.
In this post I will share a script to automate image creation process for Linux VMs (specially, CentOS, RHEL, Ubuntu) using powershell and HashiCorp Packer. Packer helps you create an image with customization through it’s Builders and Provisioners.
We need to setup some pre-requisites first. Most of those will be incorporated in the script. However, you need an Azure Subscription with access to create resources in the Subscription and to create Service Principal. Packer is using a Service Principal with Contributor access to the subscription.
You need to import Powershell Az Module. I am skipping this check in the script. 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. We will install Chocolatey before installing packer. 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
}
I will set up a variable to store my provisioner commands I want to execute based on the Linux OS platform:
switch($imageOffer)
{
"UbuntuServer" {
$inlineCmd = "`"apt-get update`",`"apt-get upgrade -y`",`"apt-get -y install nginx`",`"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync`""
}
{"CentOS","RHEL" -contains $_} {
$inlineCmd = "`"yum update -y`",`"yum install -y httpd`",`"yum install -y nginx`",`"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync`""
}
}
Next, we will create a Resource Group to host my Linux 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))
$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. 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 Linux 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. 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": "Linux",
"image_publisher": "$($imagePublisher)",
"image_offer": "$($imageOffer)",
"image_sku": "$($imageSku)",
"azure_tags": {
"CreationDate": "$((Get-Date).ToString("dd-MMM-yyyy"))",
"OS": "$($imageOffer)-$($imageSku)",
"Owner": "Avengers"
},
"location": "$($location)",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"type": "shell",
"inline_shebang": "/bin/sh -x",
"execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",
"inline": [
$($inlineCmd)
]
}]
}
"@
$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 Linux 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]$imagePublisher = "Canonical",
[Parameter(Mandatory=$false)]
[String]$imageOffer = "UbuntuServer",
[Parameter(Mandatory=$false)]
[String]$imageSku = "18.04-LTS"
)
# 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": "Linux",
"image_publisher": "$($imagePublisher)",
"image_offer": "$($imageOffer)",
"image_sku": "$($imageSku)",
"azure_tags": {
"CreationDate": "$((Get-Date).ToString("dd-MMM-yyyy"))",
"OS": "$($imageOffer)-$($imageSku)",
"Owner": "Avengers"
},
"location": "$($location)",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"type": "shell",
"inline_shebang": "/bin/sh -x",
"execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",
"inline": [
$($inlineCmd)
]
}]
}
"@
$input | Out-File $jsonTemplateFile -Encoding ascii
}
Catch
{
Write-Host "ERROR : $_" -Foregroundcolor Red
Exit
}
}
# Function to create Image using Packer
Function Create-Image(){
Try{
powershell packer build $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 $jsonTemplateFile){
Remove-Item -Path $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
}
$jsonTemplateFile = "$($env:Temp)\image-template.json"
if(Test-Path $jsonTemplateFile){Remove-Item -Path $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
}
# Set Provisioner command to execute based on Image Offer
switch($imageOffer)
{
"UbuntuServer" {
$inlineCmd = "`"apt-get update`",`"apt-get upgrade -y`",`"apt-get -y install nginx`",`"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync`""
}
{"CentOS","RHEL" -contains $_} {
$inlineCmd = "`"yum update -y`",`"yum install -y httpd`",`"yum install -y nginx`",`"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync`""
}
}
# 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))
start-sleep -Seconds 20
$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 Linux VM Image with Packer and Powershell […]
[…] Create Azure Linux VM Image with Packer and Powershell […]