Part 1 – https://verticalagetechnologies.com/index.php/2023/06/14/azure-packer-powershell-creating-citrix-golden-images-part-1

Part 2 – This post.

Part 3 – https://verticalagetechnologies.com/index.php/2023/06/28/azure-packer-powershell-creating-citrix-golden-images-part-3

Welcome back to the second blog post on creating golden Citrix images with Packer, PowerShell, and Azure. In Part 1 we talked about the staging grounds for getting started and kicking everything off. Now that we have the main variables and access configured, we can go over the HCL/JSON file.

In this case we’ll be working with https://github.com/StevenNoel/Packer-Azure-Powershell/blob/main/Packer-CTXAZPK-VDA-Citrix.pkr.hcl. This was a json file that was converted over to the newer HCL format via the convert command – Here. Really no editing was needed. I’m not going to go through this line by line, but I’ll go over some of the pertinent parts. Majority of the commands/syntax in here can be found at this location: https://developer.hashicorp.com/packer/plugins/builders/azure/arm

variable "AZURE_CLIENT_ID" {
  type      = string
  default   = "${env("AZURE_CLIENT_ID")}"
  sensitive = true
}

variable "AZURE_CLIENT_SECRET" {
  type      = string
  default   = "${env("AZURE_CLIENT_SECRET")}"
  sensitive = true
}

variable "AZURE_MDT" {
  type      = string
  default   = "${env("AZURE_MDT")}"
  sensitive = true
}

variable "AZURE_LOCALUSER" {
  type      = string
  default   = "${env("AZURE_LOCALUSER")}"
  sensitive = true
}

variable "managed_image_name" {
  type    = string
  default = "citrix-packer-image"
}

variable "subscription_id" {
  type    = string
  default = "<azure subscription id>"
}

variable "temp_compute_name" {
  type    = string
  default = "CTXAZPK16LOB001"
}
  • This first section is taking the environment variables that we set in Part1 and assigning them so they can be used as values in other sections.
source "azure-arm" "autogenerated_1" {
  azure_tags = {
    application = "citrix"
  }
  #Azure Info
  subscription_id                        = "${var.subscription_id}"
  client_id                              = "${var.AZURE_CLIENT_ID}"
  client_secret                          = "${var.AZURE_CLIENT_SECRET}"
  cloud_environment_name                 = "AzureUSGovernmentCloud"  
  
  #Packer Azure
  build_resource_group_name              = "<azure resource grouop>"
  custom_resource_build_prefix           = "ctxpk"
  managed_image_name                     = "${var.managed_image_name}-${formatdate("YYYY-MM-DD-hhmm-0700",timestamp())}"
  managed_image_resource_group_name      = "<azure resource group>"
  managed_image_storage_account_type     = "Premium_LRS"
  
  #Azure Marketplace Sku
  os_type                                = "Windows"
  image_offer                            = "WindowsServer"
  image_publisher                        = "MicrosoftWindowsServer"
  image_sku                              = "2016-datacenter-gensecond"
  image_version                          = "latest"

  #VM details
  private_virtual_network_with_public_ip = false
  #temp_compute_name                      = "${var.temp_compute_name}${legacy_isotime("06010203")}"
  temp_compute_name                      = "${var.temp_compute_name}"
  virtual_network_name                   = "<azure virtual network>"
  virtual_network_resource_group_name    = "<azure resource group>"
  virtual_network_subnet_name            = "<azure subnet name>"
  vm_size                                = "Standard_B4ms"
  
  #WinRM
  communicator                           = "winrm"
  winrm_insecure                         = "true"
  winrm_timeout                          = "5m"
  winrm_use_ssl                          = "true"
  winrm_username                         = "packer"
}
  • This section is pretty straight forward and should look something similar for builds in Azure.
  • The “Azure Info” section contains the necessary authentication that packer uses to connect to your azure tenant
  • The “Packer Azure” section contains already created already pre-created resource/virtual/subnet groups. If you don’t have these created Packer can create them. Also, if you want to use different resource groups, you can use “temp_resource_group_name“.
  • The “VM Details” section provides resource requirements for what type of VM build to make, the VM name, and Network details.
  • The “WinRM” section contains standard canned WinRM information. In this scenario Packer will communicate to the the Azure VM via SSL Cert that it generates in a Vault. However, you can specify a password here or another method.
  • Make sure to check https://developer.hashicorp.com/packer/plugins/builders/azure/arm for further definitions and caveats for the parameters.
build {
  sources = ["source.azure-arm.autogenerated_1"]

  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 }", "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12", "Start-sleep -s 5"]
  }

  provisioner "windows-restart" {
    restart_check_command = "powershell -command \"&amp; {Write-Output 'Machine restarted1.'}\""
  }

  provisioner "powershell" {
    environment_vars = ["AZURE_LOCALUSER=${var.AZURE_LOCALUSER}"]
    scripts          = ["./Azure-Packer-PreReqs.ps1"]
    valid_exit_codes = [0, 1, 3010]
  }

  provisioner "windows-restart" {
    restart_check_command = "powershell -command \"&amp; {Write-Output 'Machine restarted2.'}\""
  }

  provisioner "powershell" {
    environment_vars = ["Azure_MDT=${var.AZURE_MDT}"]
    scripts          = ["./Azure-Packer-Base.ps1"]
    elevated_user  = "Administrator"
    elevated_password = "${var.AZURE_LOCALUSER}"
    valid_exit_codes = [0, 1, 3010]
  }

  provisioner "windows-restart" {
    restart_check_command = "powershell -command \"&amp; {Write-Output 'Machine restarted3.'}\""
    restart_timeout = "20m"
  }

  provisioner "powershell" {
    environment_vars = ["Azure_MDT=${var.AZURE_MDT}"]
    scripts          = ["./Azure-Packer-Citrix-Installs.ps1"]
    elevated_user  = "Administrator"
    elevated_password = "${var.AZURE_LOCALUSER}"
    valid_exit_codes = [0, 1, 3, 3010]
  }

  provisioner "windows-restart" {
    restart_check_command = "powershell -command \"&amp; {Write-Output 'Machine restarted4.'}\""
    restart_timeout = "20m"
  }

  provisioner "powershell" {
    environment_vars = ["Azure_MDT=${var.AZURE_MDT}"]
    scripts          = ["./Azure-Packer-Global-App-Installs-1.ps1"]
    elevated_user  = "Administrator"
    elevated_password = "${var.AZURE_LOCALUSER}"
    valid_exit_codes = [0, 1, 3, 3010]
  }

  provisioner "windows-restart" {
    restart_check_command = "powershell -command \"&amp; {Write-Output 'Machine restarted5.'}\""
    restart_timeout = "20m"
  }

  provisioner "powershell" {
    environment_vars = ["Azure_MDT=${var.AZURE_MDT}"]
    scripts          = ["./Azure-Packer-SecurityAgent-Installs-1.ps1"]
    elevated_user  = "Administrator"
    elevated_password = "${var.AZURE_LOCALUSER}"
    valid_exit_codes = [0, 1, 3, 3010]
  }

  provisioner "windows-restart" {
    restart_check_command = "powershell -command \"&amp; {Write-Output 'Machine restarted6.'}\""
    restart_timeout = "20m"
  }

  provisioner "powershell" {
    environment_vars = ["Azure_MDT=${var.AZURE_MDT}"]
    scripts          = ["./Azure-Packer-Final-Seal-2.ps1"]
    elevated_user  = "Administrator"
    elevated_password = "${var.AZURE_LOCALUSER}"
    valid_exit_codes = [0, 1, 3, 3010]
  }

  provisioner "windows-restart" {
    restart_check_command = "powershell -command \"&amp; {Write-Output 'Machine restarted7.'}\""
    restart_timeout = "20m"
  }

  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 }", "Set-ExecutionPolicy Bypass -Scope Process -Force", "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12", "Start-sleep -s 5", "& $env:SystemRoot\\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 } }"]
  }

  post-processor "manifest" {
    output     = "./manifests/manifest-${legacy_isotime("2006-01-02-0304")}.json"
    strip_path = true
  }
}
  • This is the final part of the HCL file, which is designed by the ‘build’ provisioner. This is similar to an MDT/SCCM task sequence or a ansible playbook, where we tell Packer all the different things to do or scripts to run.
  • I breakdown this into the following section
    • Prereq
      • restart
    • Base
      • restart
    • Citrix Installs
      • restart
    • App Installs
      • restart
    • Security Agent Installs
      • restart
    • Seal
      • restart
  • I’ve uploaded a few of the PS1s to this repo to show you how I’m handling the app installs. In a nutshell, each of the PS1s that get called in this HCL file, pass creds, map drives, and then call other PS1s. For instance the Azure-Packer-Citrix-Installs.ps1 will run elevated, pass the creds into PS1, Map a drive, install the VDA, Citrix Optimizer, Connection Quality Indicator, and Citrix Workspace. You can see an example of this in the Azure-Packer-Base.ps1 that I’ve uploaded.
  • Once this build provisioner finishes, you will be left with an ‘image’ in azure where you can create/deploy VMs from.

That’s all for this post. In Part 3 I’ll talk about some of the gotchas I ran across.

Part 1 – https://verticalagetechnologies.com/index.php/2023/06/14/azure-packer-powershell-creating-citrix-golden-images-part-1

Part 2 – This Post

Part 3 – https://verticalagetechnologies.com/index.php/2023/06/28/azure-packer-powershell-creating-citrix-golden-images-part-3

Leave a Reply

Your email address will not be published. Required fields are marked *