Category Archives: Packer

Deploying vSphere Templates with Packer & Azure DevOps

Ever since starting out on my learning journey with Packer and writing my ‘Getting Started‘ blog series, I have not stopped learning and developing my templates. I have also learnt a lot from other members of the tech community, such as @mpoore, as well as discovering this repository – vmware-samples. I really wish I had found this sooner than I did, as its a great resource! It was especially useful to me for Linux examples. That said, its been great taking my own learning journey.

Since writing the series, I have made numerous changes to my template code, structure and added additional functionality and OS’s. But I have also spent some time working with Azure DevOps Pipelines for another piece of work. This got me thinking…

In this blog post I want to show you something that I have put together using Azure DevOps Pipelines and Packer.


This solution makes use of Azure DevOps Pipelines, Azure Key Vault and HashiCorp Packer to schedule and orchestrate the building of new virtual machine templates in VMware vSphere.

Azure Pipelines will be used to orchestrate the secure retrieval of secrets from Azure Key Vault using the native integration and executing the Packer commands to build the required template. By using these together, we can ensure all secrets are securely utilised within the build.

I will be using a Self Hosted DevOps agent as part of this to allow communication between Azure DevOps and the private networks in my on-premises lab. This is instead of using a Microsoft Hosted DevOps agent which is in a public shared address space.

As mentioned, Azure Key Vault is going to be used to store the secret values for things like service accounts for vSphere access and administrator passwords for the Guest OS etc. These can retrieved within a pipeline, which is granted access to these secrets and made available as variables to be consumed.

Each template will have its own pipeline. This means individual templates can be called via API allowing for some other interesting use cases and automation.

As is the case in the blog series, all templates are uploaded to the vSphere Content Library which can then be subscribed too from other vCenter Servers.


  • GitHub Repository (Packer Code)
  • DevOps Project
  • DevOps Pipeline
  • On-Prem DevOps Agent (Virtual Machine)


  • GitHub Repository with your Packer code (Example here)
  • A functioning vSphere environment
  • An Azure & DevOps Subscription / Account
  • An Azure Key Vault (With appropriate networking configured)
  • A Virtual Machine (Windows 2022 Core in this example)
  • AD User Account (To run the DevOps Agent as a service) *can use the system local account if you wish.

Packer Code

If you aren’t familiar with Packer, I would suggest taking a look at my blog series on Packer here, but I will briefly go through some key differences of the newer code that you can find here, which the blog is based on. At the time of writing I have only added Server 2019 & 2022 but I will be adding to this over time.

Firstly the file structure is now a little different. This was inspired by the vmware-samples repository linked earlier, and some of my own preferences from actively using Packer.

  • Shared answer file templates with parameters for all Windows Operating Systems to reduce repeating files.
  • Single .pkrvars.hcl for each Operating System which includes both Standard & Datacenter Editions as well as Core and Desktop options.
  • The Build file includes a dynamic creation of the answer file based on variables from a template file. (this is great!)
  • Cleaner variable naming.
  • The Windows Update Provisioner is now controlled using the required plugin parameters.

Another key difference is how sensitive values such as usernames, passwords and keys are now passed into the configuration. These are now retrieved from Azure Key Vault by a Pipeline task and passed into environment variables (PowerShell) which are then consumed as any other variable would be. The key benefit is that the secrets are securely stored and accessed by the pipeline!

Check out the Azure Key Vault section later in the post for more information on secrets and their consumption.

DevOps Project

First lets create a DevOps Project by heading over to and clicking on New Project.

Provide a name for the project and select the Private option.

Now time to create the first pipeline. As mentioned earlier, we will be using a pipeline per operating system.

Select Create Pipeline.

You will then be asked to select the location of your code. I will be selecting GitHub as that is where I keep my code.

Followed by the repository that contains your Packer Code.

Next you need to provide and approve access for Azure Pipelines to the repository you selected.

Now to create the first pipeline YAML file. Select Starter Pipeline.

First of all rename the file to the name of the template you are going to build. In this example lets call it ‘windows-server-2022-standard-core.yml’. You can do this by clinking the existing name.

Now you want to add the code for this template build. You can use the examples from here.

You could of course take the examples from my GitHub and select ‘Existing Azure Pipelines YAML file’ rather than ‘Starter pipeline’ if you wish.

Lets now briefly explain what this code is doing:

    - repository: ps-code-snippets
      type: github
      endpoint: smctighevcp
      name: smctighevcp/ps-code-snippets
      ref: main

Here we start by referencing a different central repository which contains reusable code. A good resource to understand this bit is linked here.

- checkout: self
- checkout: ps-code-snippets

We also have a ‘checkout’ references. These instruct the pipeline to checkout not only the source repository, but also the additional one that contains reusable code.

   - cron: "0 0 15 * *"
     always: true
     displayName: Monthly Schedule
          -  main

This section is setting a schedule to run at midnight every 15th of the month. This can be adjusted to suit your needs. More information about setting cron schedules are here.

 pr: none
 trigger: none

As we want to run the Pipelines either on a schedule or manually, we want to disable the CI/CD integration. We do this by setting the pull request (pr) and trigger options to ‘none’.

   - job: Windows_Server_2022_Core
      name: On-Prem Agent Pool
     timeoutInMinutes: 120
      - group: Notifications

This section defines a couple of parameters for the job. Firstly the name of the job as well as the name of the On-Prem DevOps agent pool we will be using (See the next section). Finally a timeout value. By default this is 60 minutes for self hosted agent jobs which isn’t quite long enough for the Desktop Edition of the OS in my lab. There is also a reference to a variable group. These are groups of variables that can be consumed by any Pipeline within the DevOps Project.

- task: AzureKeyVault@2
            azureSubscription: 'identity-management-connectivity(a949****-****-****-****-************)'
            KeyVaultName: 'kv-******-main'
            SecretsFilter: 'vSphereAdmin, vSphereAdminUser, GuestLocalAdmin, GuestLocalAdminUser, PipelineNotificationsTenantID, PipelineNotificationsAppID, PipelineNotificationsAppSecret'
            RunAsPreJob: false

Next we are using a built in Pipeline task to retrieve secrets from an Azure Key Vault. I am then filtering it to the specific secrets required. You could replace this with ‘*’ if you don’t wish to filter them. Access to these are secured using RBAC later.

#change to Packer working directory
                Set-Location $(System.DefaultWorkingDirectory)\pipeline-templates\

                #Specify the Build
                $BuildVersion = "$(BuildVersion)"
                $BuildDate = get-date -f yyyyMMdd-HHmm

                #Enable and set logging location
                $env:PACKER_LOG = 1
                $env:PACKER_LOG_PATH = "E:\Packer-Logs\packerlog-$($BuildVersion)-$BuildDate.txt"

                #Initialise template!
                packer init build.pkr.hcl

                #Build template!
                packer build --only -var-file="vars/windows-server-2022.pkrvars.hcl" .

               #Build Email Content
              $out = select-string -path $LogPath -pattern 'ui:'
              $EmailContent = $out.line -replace '(?<Date>[0-9]{4}\/[0-9]{2}\/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})','__${Date}' -replace'__',"<br />" -replace "[ ]+ui:[ ]+","`t" -replace " ", "" -replace "==> ", "   "

Now we move onto the more familiar Packer and PowerShell code (if you are already a user of Packer). This sets a couple of variables to use in the log files, enables logging and initiates the build. It then begins to populate a variable that has taken the information from the log file and cleaned it up to consume in an email notification in the final steps.

Something you may need to adjust is the Set-Location path. Its using a built in variable which is the root of the GitHub Repository: $(System.DefaultWorkingDirectory). Make sure you adjust the remain path to match to location of your Packer configuration.

                $EmailBody = ('<HTML><H1><font color = "#286334"> Notification from The Small Human Cloud - Packer Virtual Machine Templates</font></H1><BODY><p><H3><font color = "#286334">Build Name:</H3></font></p><p><b>$(BuildVersion)</b></p><p><H3><font color = "#286334">Pipeline Status:</H3></font></p><p><b>Build Reason:<b> $(Build.Reason)</p><p><b>Build Status:<b> $(Agent.JobStatus)</p><p><H3><font color = "#286334">Packer Log:</H3></font></p><p>Please Review the logfile below for the build and take appropriate action if required.</p>')+("<p>$EmailContent</p>")
                Set-Location $(System.DefaultWorkingDirectory)
                . '.\ps-code-snippets\Send-Email.ps1'
                Send-Email -TenantId "$(PipelineNotificationsTenantID)" -AppId "$(PipelineNotificationsAppID)" -AppSecret "$(PipelineNotificationsAppSecret)" -From "$(From)" -Recipient "$(Recipient)" -Subject "$(Subject)" -EmailBody $EmailBody

This final section makes use of a PowerShell function based on the Azure Graph API that you can find details on here, to send an email notification via O365. It is taking the content of the function from a separate repository and loading it into the session to then run.

Now select the drop down next to ‘Save and run’ and click Save.

We want to rename the actual Pipeline to the template name. Head back to the Pipelines menu, click the 3 dots and select ‘Rename/Move’. Give it the same name as your YAML file for consistency.

Variable Groups and Pipeline Variables

We mentioned earlier the reference to a variable group. These are configured per DevOps Project and can be used by multiple Pipelines. I am using one specifically for the values used for email notifications. They are a great way to reduce duplicating variable declarations.

You can set these by heading to Pipelines > Library and then clicking ‘+ Variable group’. You can see my group called ‘Notifications’ already created.

      - group: Notifications

We then need to grant the Pipeline permissions to this variable group. You will need to add any Pipeline you want to have access to these variables.

There is another way of providing variables to a Pipeline and that is a Pipeline Variable. These are configured per Pipeline and are not available to other Pipelines. I am using this to create a ‘Build Version’ variable that is used for the log file name.

Azure DevOps Agent

We need to build our self hosted DevOps Agent that we referenced in the ‘pool’ parameter in our configuration earlier. This is going to be a virtual machine on my on-premises vSphere environment. I will be using a Windows Server 2022 Standard Core VM called ‘vm-devops-02’ that I have already built on a dedicated VLAN.

To start the config, we need to create an Agent Pool. From the Project page, select ‘Project Settings’ in the bottom left.

From the tree on the left under Pipelines, select Agent Pools.

Now, select Add Pool, and complete the required field as below, editing the name as desired, but you will need to match it when you reference the pool in your YAML.

Now to add the agent to our on-premises VM. Select ‘New Agent’

Download the agent using the Download button and then copy the ZIP file to the VM to a directory of choice. You can use PowerShell for this:

$SESSION = New-PSSession -ComputerName -Credential (Get-Credential)
Copy-Item -Path "C:\temp\" -Destination "C:\" -ToSession $SESSION

Then using the commands from the agent configuration window, run the command to extract the files adjusting the file paths as required:

Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\temp\", "$PWD")

You are then left with something like this:

Now before we run the configuration file, we need to create a PAT (Personal Access Token) for use during the install only, it doesn’t need to persist past the install.

You will then need to make a note of this token for use later!

Now run the configuration script:


You will then be presented with a set of configuration questions (Detailed instructions here):

You will need your DevOps Organisation URL, PAT Token and an AD (Active Directory) account to run the Agent service under. As mentioned, you can use the NETWORK SERVICE if you wish.

Now if we head back over to the Project’s Agent Pool, you will see its active!

I am using service accounts within the Pipeline to access the vSphere environment etc, so I don’t need to give the agent service account any specific permissions. More information can be found here.

Depending on your environment you may need to configure a web proxy or firewall access for the agent to communicate with Azure DevOps.

Finally, you will need to ensure the Packer executable is available on the DevOps agent server. See my past blog for more information.

That’s the Agent setup completed.


Now we need to authorise the DevOps project to access the Key Vault we plan on using. The quickest and easiest way is to do this is to edit the Pipeline and use the Azure Key Vault Task Wizard to authorise, but this isn’t the cleanest way.

You can create the Service Connection manually. This allows for further granularity when you have multiple pipelines within the same project that require different secret access.

You can do this by heading to into the Project Settings and then Service Connections.

When selecting new, choose the Azure Resource Manager type, followed by Service principal (automatic).

You then need to select your Subscription and provide it a name.

Now head over to Azure to match the name of the Service Principal in Azure with the Service Connection from DevOps. To do this select the Service Connection, and then Manage:

You are going to need the Application ID of the service connection to be able to assign permissions to secrets using PowerShell. Grab the Application ID from the Overview tab as well as your subscription ID for use with the New-AzRoleAssignment cmdlet.

Now back over to the DevOps portal, we can give permissions to each template pipeline to use this service connection. First, click on security.

We can then add the pipelines required.

Azure Key Vault Secrets

Adding Secrets

This Packer configuration consumes a number of secrets within the Pipeline. We will be storing the username and password for the vSphere Service account and Guest OS admin accounts for accessing vSphere as well as building and configuring the VM and the autounattend.xml file. I will go into more detail further down, but here is a link describing how to add a secret to a Vault.


To ensure a Pipeline only has access to the secrets it needs, we will be using RBAC permissions per secret using the IAM interface rather than Access Policies.

To configure this, select a secret and then open the IAM interface. Select the ‘Key Vault Secret User’ Role and then click members.

Click’ Select Members’, search for the require service principal and select it, followed by the select option at the bottom.

Now click ‘Review + Assign’

Repeat for all secrets required.

You can also use the PowerShell command ‘New-AzRoleAssignment’ rather than using the portal to assign the permissions.

We are granting the ‘Key Vault Secrets User’ role to the Application ID, for each of the required secrets:

New-AzRoleAssignment -RoleDefinitionName 'Key Vault Secrets User' -ApplicationId <App ID> -Scope /subscriptions/<Subscription ID>/resourcegroups/<Resource Group Name>/providers/Microsoft.KeyVault/vaults/<vault-name>/secrets/<Secret Name>
RoleAssignmentId   : /subscriptions/a9495d90-xxxxx-xxxxx-xxxxx-exxxxxxxxxxx/resourcegroups/rg-identity/providers/Microsoft.KeyVault/vaults/kv-vault-name/secrets/GuestLocalAdmin/providers/Microsoft.Authorization/roleAssignments/1b45f2e4-e85a-4723-a4f4-cfd7da11
Scope              : /subscriptions/a9495d90-xxxxx-xxxxx-xxxxx-xxxxxxxxxxxxx/resourcegroups/rg-identity/providers/Microsoft.KeyVault/vaults/kv-vault-name/secrets/GuestLocalAdmin
DisplayName        : TheSmallHumanCloud-Cloud Management-a9495d90-xxxxx-xxxxx-xxxxx-xxxxxxxxxxxxx
SignInName         :
RoleDefinitionName : Key Vault Secrets User
RoleDefinitionId   : 4633458b-17de-408a-b874-0445c86b69e6
ObjectId           : ca610efd-xxxxx-xxxxx-xxxxx-xxxxxxx1efa26
ObjectType         : ServicePrincipal
CanDelegate        : False
Description        :
ConditionVersion   :
Condition          :

Required Secrets

The Pipeline Azure Key Vault task described earlier lists a number of secrets that we will require. Here is an overview of the purpose of each:

  • vSphereAdminUser – Active Directory Account with the required permissions in vCenter to be able to provision VM’s and upload to a Content Library etc.
  • vSphereAdmin – Password for the vCenter Account above.
  • GuestLocalAdminUser – Local Admin username (Administrator) for consumption in the Operating System answer file.
  • GuestLocalAdmin – Password for the local admin user.

The next three are for the custom email notification section which I will go into more later.

  • PipelineNotificationsTenantID – Azure Tenant ID.
  • PipelineNotificationsAppID – App Registration ID.
  • PipelineNotificationsAppSecret – App Registration Secret.

Email Notification Setup

The Pipeline makes use of a custom email notification PowerShell function which uses the Graph API’s. See my recent blog post on how to set this up.

Running the Pipeline

We are now ready to run the Pipeline! To kick it off manually, hit the ‘Run Pipeline’ button when in the Pipeline.

Increase the playback quality if the auto settings aren’t allowing you to see the detail!

Now you can head over to your content library and you will see your template. Below are both my Windows Server 2022 builds.

You can tell I use Server 2022 Core to test… Version 41!

Notification Email

Here is a snippet of the notification email that was sent on completion.

And there you have it. I personally enjoyed seeing how I could make use of both Packer and Azure DevOps to deliver vSphere templates. I hope it helps you with your templating journey!

As always, thanks for reading!

If you like my content, consider following me on Twitter so you don’t miss out!

Getting Started With Packer to Create vSphere Templates – Part 5 – Bringing it Together!

Here we are, Part 5! If you have stuck with me through this series, thank you for taking the time. If not, you can catchup with Parts 1-4 by searching my blog!

I wanted to end this series with something different to just text, code and images. So I am going to show you the end to end template deployment process with video’s using user defined variables but with a few environment variables in the Linux example.

Lets start with a Windows example – Windows 2019 Core

To give some context to the files being referenced for this build, here is the folder structure I will be working with, all of which is available on the link above.

From the root directory of your configuration, run the following:

packer build --var-file=./win2019core.pkrvar.hcl .

The trailing ‘.’ is important as this tells Packer that it needs to reference all of the .hcl files in its current directory.

And here is the finished article in the content library!

Now lets look at a Linux example that uses a HTTP server to acquire its kickstart configuration file from, rather than it being loaded as removal media. – Centos 8

This example also makes use of environment and user defined variables!

And again, the finished article.

If you have followed this series throughout, thank you. I hope you have found it useful and its inspired you to begin your Packer journey! Feel free to reach out via my socials if you have any questions or just want to chat about Packer!


Getting Started With Packer to Create vSphere Templates – Part 4 – Blocks

Welcome to Part 4 of the Packer Series! In this part we will look at putting together all the block and files we need to deploy a template!

As we have touched upon in earlier parts, we have multiple blocks and files available to us that can be used to make up a complete configuration. We will walk through a complete Source and Build Block here using user defined variables to complete the build. In the final part of this series, I will use a combination of user and environment variables to give you an idea of how you may use this outside of a lab.

Lets start by breaking down a Source Block for a Windows 2019 Core template.

source "vsphere-iso" "win-2019-std-core" {
  CPUs            = var.CPUs
  RAM             = var.RAM
  RAM_reserve_all = var.ram_reserve_all
  boot_command    = var.boot_command
  boot_order      = var.boot_order
  boot_wait       = var.boot_wait
  cluster         = var.vsphere_compute_cluster
  content_library_destination {
    destroy = var.library_vm_destroy
    library = var.content_library_destination
    name    = var.template_library_Name
    ovf     = var.ovf
  datacenter           = var.vsphere_datacenter
  datastore            = var.vsphere_datastore
  disk_controller_type = var.disk_controller_type
  firmware             = var.firmware
  floppy_files         = var.config_files
  folder               = var.vsphere_folder
  guest_os_type        = var.guest_os_type
  insecure_connection  = var.insecure_connection
  iso_paths = [var.os_iso_path,var.vmtools_iso_path]
  network_adapters {
    network      = var.vsphere_portgroup_name
    network_card = var.network_card
  notes        = var.notes
  password     = var.vsphere_password
  communicator = var.communicator
  winrm_password = var.winrm_password
  winrm_timeout  = var.winrm_timeout
  winrm_username = var.winrm_user
  storage {
    disk_size             = var.disk_size
    disk_thin_provisioned = var.disk_thin_provisioned
  username       = var.vsphere_user
  vcenter_server = var.vsphere_server
  vm_name        = var.vm_name
  vm_version     = var.vm_version

All values are passed in via variables in this example. You can see this by the ‘var.<variable_name>’ entry against every configuration line. All variables in this example are user defined variables in a pkrvar.hcl file.

We have configuration for CPU, Memory and disk sizes for instance, then we also have the WinRM username, password and timeout values used for connecting to the operating system after it’s been installed, for use with provisioners.

You can deploy your template as just a ‘normal’ VM Template in the VM and Templates Inventory by using this line:

convert_to_template        = true

Or a using a variable:

convert_to_template             = var.convert_to_template

Or you can deploy to Content Libraries by either removing the “convert_to_template” option or setting it to false, and replacing it with this:

  content_library_destination {

    library = var.content_library_destination
    name    = var.template_library_Name

If you already use Content Libraries, then you are likely going to want to continue to do so.  Or, if you have multiple vCenter’s, you may want to make use of subscribed libraries so you only have to deploy the template once.

To go further you can automatically destroy the original VM after its been uploaded to the Content Library by adding:

destroy = var.library_vm_destroy

And to take it even further, you can add the following to convert the template to an OVF.  OVF’s can be updated in the content library and therefore will be overwritten when you deploy your template again.  This can’t be done with a standard VM template.

ovf     = var.ovf

To bring that all together it looks like this:

  content_library_destination {
    destroy = var.library_vm_destroy
    library = var.content_library_destination
    name    = var.template_library_Name
    ovf     = var.ovf

A key line to point out in this windows example configuration above, is the ‘floppy_files’ option. This option is used to mount a floppy disk with any configuration files or media that you need to reference during the operating system installation. This includes your unattended.xml file, any scripts and any media or drivers such as VMware Paravirtual drivers for the SCSI controller. Checkout Part 2 for more info.

If we were looking at a Linux build, we would see the WinRM options replaced by SSH, like so:

  ssh_password = var.ssh_password
  ssh_timeout  = var.ssh_timeout
  ssh_username = var.ssh_username

A full list of the different configuration options available can be found here.

Now we have defined our source, we now want to deploy it using a build block.

build {
  name    = "win-2019-std-core"
  sources = [""]

  provisioner "powershell" {
    scripts           = var.script_files
  provisioner "windows-update" {
            search_criteria = "IsInstalled=0"
            filters = [
                      "exclude:$_.Title -like '*Preview*'",
            update_limit = 25
  post-processor "manifest" {
    output = "output/out-win-2019-std-core.json"
    strip_path = false

What’s happening in this block, is that we are referencing the source block that contains our configuration based on the name of the source block that we defined earlier, in this case ‘’.

In this example we also have two provisioners being used once the operating system has been installed. Firstly, the Windows-Update-Provisioner which installs the latest Windows updates based on any filters you include. In this example, its configured to exclude any updates with ‘Preview’ in the title and also to limit it to install up to 25 updates.

Secondly, we are making use of the Manifest post-processor. This produces an output that includes information such as build time each time it is run.

      "name": "win-2019-std-core",
      "builder_type": "vsphere-iso",
      "build_time": 1617185954,
      "files": null,
      "artifact_id": "windows-2019-std-core",
      "packer_run_uuid": "865be1fd-0dec-1688-8c89-9252e48d0818",
      "custom_data": null
  "last_run_uuid": "865be1fd-0dec-1688-8c89-9252e48d0818"

All of the above makes up a complete build file that can be deployed with any media or variables you have referenced. The full set of files for this example can be found here.

To give you an example of a non-windows Provisioner, here is a Shell Provisioner for a Linux template:

provisioner "shell" {
    execute_command = "echo '${"var.ssh_password"}' | sudo -S -E bash '{{ .Path }}'"
    scripts         = var.script_files

This executes all scripts that are referenced in the script.files variables.

Now using environment variables, nothing really changes. Your build file will look the same, the only differences will be you won’t provide a value for your declared variable in your pkrvar.hcl file, instead adding the variable to your terminal session. Check out Part 3 for more info. In the final part of this series, I will show an example of using both user defined and environment variables.

That concludes a short run through of the different files in the examples you can find on my GitHub. By no means have I covered everything in those examples or everything you can do with Packer, but this series along with the examples should help you on your way with discovering Packer! There is so much more that can be done using this product to create templates on vSphere as well as multiple other platforms so do head over to to discover more.

In the final part of this series, I am going to try a different content type, video’s! In these, we will run through two end to end template deployments using default values for variables, user defined and environment variables to show how you could use this as part of a workflow.

If you have gotten this far, thanks for sticking with me and I hope you have enjoyed it and found it useful!


Getting Started With Packer to Create vSphere Templates – Part 3 – Variables

Welcome back to part 3 of my Creating vSphere Templates using Packer series, if you missed part 1 or 2, you can find them here and here. In part 3 we will explore variables!

Why would we use variables? Variables allow you to specify customisations to your templating code without having to edit your actual build files. This can be useful when you are reusing code for multiple templates.

There are multiple types of variables that can be used, but we will talk about 2 types of input variables in this blog. They are what I will refer to as; User defined variables and Environment variables. We will talk about both during the blog post and the use cases for each.

Regardless of whether we use a user defined variable or an environment variable, we still need to declare them. This is done in a variable declaration file, so lets start with that!

Variable Declaration

Following the release of Packer version 1.7 the Hashicorp Configuration Language (HCL) format is now the preferred language over JSON. Everything you will see will be in HCL.

The variable declaration file is a pkr.hcl file used to declare any variables you will be using as part of your configuration, be it user defined or environment variables.

Lets take a look at a few of the variable types you can make use of as well as some of the options you can also set.

Variable Type

Here is a few common variable types, you don’t have to define a type at all, but you could then pass the wrong type of data into your config.

  • String – E.g. The templates name or the name of a datastore.
  • Boolean – E.g. A true or false value for whether you are using thin or thick provisioned disks.
  • List – E.g. A list of DNS server IP addresses.

We will see examples of these later on.

Default Value

You can set default values for variables. These values will be used if no other variable value is found in either your pkrvar.hcl file or as an environment variable. Using default values can help reduce the amount of repeat configuration if you use a shared variable definition file.


Another useful option is to be able to provide a description to a variable. This can be useful if you need to add any additional information about the variable or why a particular default has to be set.


You can also mark variables as sensitive for values such as keys, password or usernames etc, however you can mark any variable as sensitive if you have a need to. When a variable is marked as sensitive, it will not be displayed in any of Packers output.

User Defined Variables

Lets take a look at a few examples of declared variables in the variables.pkr.hcl file as well as any values then set for those variables in the user variables file. You will see a couple of examples of variables that have default, type and sensitive options set to give you an idea of some of the use cases.

Lets start with a basic user defined variable:

Variable Declaration – variables.pkr.hclVariable Defination – template.pkrvar.hcl
variable “vsphere_datastore” {}vsphere_datastore = “ds-vsan-01”
variable “vsphere_portgroup_name” {}vsphere_portgroup_name = “dvPG_Demo_DHCP_149”

So in this example, we are declaring that we are going to use variables called ‘vsphere_datastore’ and ‘vsphere_portgroup_name’. We then have values defined for these variables in our pkrvar.hcl file. This can be any data type for the value, as no type has been defined.

Variable Declaration – variables.pkr.hcl Variable Defination – template.pkrvar.hcl
variable “content_library_destination” {
  type    = string
  default = “Images”
Nothing defined = Default value would be used
content_library_destination = “ISOs”

In this example we have declared a variable with the type ‘String’, and also provided a default value. The configuration will use this default if no other value is defined either via a user variable or environment variable, but will be overridden should a variable value be set.

Variable Declaration – variables.pkr.hcl Variable Defination – template.pkrvar.hcl
variable “vsphere_server” {
  type    = string
  default = “vm-vcsa-01.smt-lab.local”
  description = “vCenter Server FQDN”
Nothing defined = Default value would be used
vsphere_server = “vcsa-02.smt-lab.local”

Here is an example again using a type and default values, but also providing a description to provide some additional information. Like the previous example, not providing a variable value either in the pkrvar.hcl file or in the terminal session as an environment variable, would result in the default value being used.

Variable Declaration – variables.pkr.hcl Variable Defination – template.pkrvar.hcl
variable “vsphere_user” {
  type      = string
  default   = “packer_build@smt-lab.local”
  sensitive = true
Nothing Defined

In this final example we are using the sensitive option. This will stop the value being displayed in any Packer output. Again, it’s using a default value, so you do not need to define a value in the pkrvar.hcl file unless you want to use a different value to this default.

Environmental Variables

Now let’s take a look at environment variables. These are especially useful if you want to use Packer as part of a workflow or automation pipeline, or to pass in secrets (passwords or keys) into the workflow from a secret management tool.

You still declare all your variables in your variables.pkr.hcl file as you would for user defined variables, but instead of providing a value in your pkrvar.hcl file, you create environment variables in your terminal session, in this case, PowerShell.

Packer will look for variables in the session with the prefix of PKR_VAR_. If Packer finds any variables with this prefix, it knows they are for its use.

You do not need to add this prefix anywhere in your configuration as Packer knows to ignore the prefix when matching the variable name.

For example lets set the vSphere connection password in the PowerShell session we are using. This can be done by running the following to set the variable:

$env:PKR_VAR_vsphere_password = "VMware123!"

This example will match up to the variable declaration:

variable "vsphere_password" {}

You do not need to provide a value in your pkrvar.hcl file as Packer will read the value from the ‘PKR_VAR_vsphere_password’ environment variable.

NOTE: If you also provide a user defined variable in pkrvar.hcl, this will take precedence over the environment variable.

You can find HashiCorps documentation on variables here, have a read to discover even more options.

Referencing a Variable from Build Blocks

Now we have taken a brief look at some of the ways to declare and define variables, lets now take a look at how you use them in your source block!

Here are some examples:

  username       = var.vsphere_user
  vcenter_server = var.vsphere_server
  vm_name        = var.vm_name
  vm_version     = var.vm_version

There are two components here. Firstly, ‘var.’ this defines that a variable is being referenced. Secondly, the name of the variable you wish to reference. Each variable referenced will need to exist in variables.pkr.hcl and either a default value specified or a user or environment variable set. It doesn’t matter whether you are using environment or user defined variables, this syntax is the same. Remember that you do not need to include ‘PKR_VAR_’ in the variable name in the source block when you are referencing an environment variable, it’s only needed as a prefix when actually setting the variable.

That concludes my brief overview of user defined and environment variables. Do checkout the link to HashiCorp’s official documentation above and you can also find an example of a variable declaration file here, and a pkrvar.hcl file here on my GitHub.

In Part 4 we will put all the blocks and files together to complete the configuration before moving onto the final part of the series, where we will deploy some templates!

Thanks for reading!

Getting Started With Packer to Create vSphere Templates – Part 2 – Answer Files and Scripts

Welcome to part 2 of my Getting Started with Packer series, if you missed part 1, you can find it here. In part 2, we will take a look through an important part of creating your vSphere templates; Answer Files and scripts.

Firstly, we will be looking at a couple of example scripts that can be used to configure your operating system before its turned into your template. We will then move on to answer files that allow an automated, non user prompting installation of your operating system. These answer files provide configuration details during the operating system installation.

Let’s get started!

Scripts, Drivers and Media

These can be referenced either during the installation of the operating system via the answer file, like VMTools is during the Windows example below. They can also be run by a Provisioner via PowerShell or Shell, after the operating system install has completed. If media is required during the installation of the operating system, such as disk controller drivers or VMware Tools, they need to be made available to the operating system during installation. This can be achieved in multiple ways, Floppy disks, CD_rom, via a HTTP server or a combination. Either way you are going to need them available, more on how to make them available later in the series, but for now lets look at a couple of examples.


Disabling TLS (Windows)

Here is an example script for disabling TLS 1.0 &1.1 on Windows using a PowerShell script. This could be run during the installation via the answer file, or via the PowerShell Provisioner. If running during the installation of the OS, this must be mounted as media during the installation. If it’s being run via the Provisioner, this can be referencing directly from the working directory of the machine you are running Packer from.

#Disable TLS 1.0
new-item -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols" -Name "TLS 1.0"
new-item -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0" -Name "Server"
new-item -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0" -Name "Client"
new-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client" -Name "Enabled" -Value 0
new-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client" -Name "DisabledByDefault" -Value 1
new-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" -Name "Enabled" -Value 0
new-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" -Name "DisabledByDefault" -Value 1
#Disable TLS 1.1
new-item -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols" -Name "TLS 1.1"
new-item -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1" -Name "Server"
new-item -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1" -Name "Client"
new-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client" -Name "Enabled" -Value 0
new-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client" -Name "DisabledByDefault" -Value 1
new-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" -Name "Enabled" -Value 0
new-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" -Name "DisabledByDefault" -Value 1

This is a simple script to create the required registry entries to disable TLS versions 1.0 &1.1.

Updating Installed Packages (Linux)

Now let’s look at a CentOS example. Here is an example Shell script for updating all installed packages, that again can be ran via the answer file (kickstart.cfg) or via the Shell Provisioner.

# Update existing packages
sudo yum update -y


Depending on the type of disk controller you plan on using for your templates and subsequent virtual machines, you may need to make drivers available during the operating system installation. An example of this are drivers for ParaVirtual SCSI (PVSCSI) disk controllers. These drivers aren’t available in during a Windows installation by default and need to be provided.

These can be mounted via floppy or another method. They just need to be available during the operating system installation. I stick to floppy currently as I don’t have to do anything other than reference the folder containing the drivers, along with my answer file and required scripts:

floppy_files         = var.config_files

This is the floppy_files config line referencing the variable ‘config_files’. That variable references the path and file name of each file I wish to make available to the VM.

Here is detail of that variable as an example. It is referencing files in two directories, config and scripts, within my template parent directory.

config_files            = ["config/autounattend.xml","scripts/pvscsi","scripts/install-vm-tools.cmd","scripts/enable-winrm.ps1"]

If you don’t provide drivers where needed, your operating system installation will fail.


Depending on what you intend to install on your templates, you will need to make any install media or install scripts available. Like above, you can either mount any media to the VM using the floppy_files option and run the installs from the answer file, or via the Provisioner referencing your local working directory.

Examples of media or installations could be security products such as Antivirus or Data Loss Preventions agents, Management/Monitoring agents such as System Center Configuration Manager (SCCM) or System Center Operations Manager (SCOM).

There is no right or wrong answer as to what you should include in your templates, this is something you need to decide based upon your needs and environment. Although I would say, keep them as light as possible and use the right tool for the job. Consider using configuration management tools when its the right time too!

Answer Files

As we touched upon above, answer files are used to provide configuration details during the operating system install. In this blog, we will take a look at two types of answer files; A windows autounattended.xml & a CentOS kickstart.cfg.

Lets begin with the Windows answer file. You can create a Windows answer file using the Windows System Image Manager (Windows SIM) which you can find more information on here.

There are multiple sections within this file from the locale settings, disk partition configurations, the edition of Windows and even a section to stop the administrator account from expiring.

Here is a cut down example of a Windows answer file, you can find a complete example on my GitHub:

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="windowsPE">
        <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="" xmlns:xsi="">
        <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="" xmlns:xsi="">
                <Disk wcm:action="add">
                        <CreatePartition wcm:action="add">
                        <CreatePartition wcm:action="add">
    <settings pass="specialize">
        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="" xmlns:xsi="">
                <RunSynchronousCommand wcm:action="add">
                    <Description>Disable Network Discovery</Description>
                    <Path>cmd.exe /c a:\disable-network-discovery.cmd</Path>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="" xmlns:xsi="">
                <SynchronousCommand wcm:action="add">
                    <CommandLine>cmd.exe /c wmic useraccount where "name='Administrator'" set PasswordExpires=FALSE</CommandLine>
                    <Description>Disable password expiration for Administrator user</Description>
                <SynchronousCommand wcm:action="add">
                    <CommandLine>cmd.exe /c a:\install-vm-tools.cmd</CommandLine>
                    <Description>Install VMware Tools</Description>
                <SynchronousCommand wcm:action="add">
                    <CommandLine>cmd.exe /c C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\enable-winrm.ps1</CommandLine>
                    <Description>Enable WinRM</Description>
    <cpi:offlineImage cpi:source="wim:c:/wim/install.wim#Windows Server 2019 SERVERSTANDARDCORE" xmlns:cpi="urn:schemas-microsoft-com:cpi" />

Key parts of this file are the installation of VMTools and the enabling of WinRM:

<SynchronousCommand wcm:action="add">
                    <CommandLine>cmd.exe /c a:\install-vm-tools.cmd</CommandLine>
                    <Description>Install VMware Tools</Description>
<SynchronousCommand wcm:action="add">
                    <CommandLine>cmd.exe /c C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\enable-winrm.ps1</CommandLine>
                    <Description>Enable WinRM</Description>

VMTools is important to make sure that the correct drivers are installed, ensuring you can get a network connection etc, and you need WinRM and the appropriate firewall rules to be enabled, to allow Packer to continue any post OS install configurations to take place via PowerShell Provisioner block. If WinRM isn’t enabled and working, you wont be able to complete any post install configuration actions!

You will notice both these actions are achieved by running a script from a floppy drive. A:\<scriptname>. As touched upon earlier, these can be made available to the virtual machine as floppy disks (other options available) as it is built and subsequently removed when the build is complete.

One other setting to mention, the Administrator password in encrypted, you don’t want to be leaving this in plain text!


Lets now take a look at a Linux Kickstart.cfg file, again cut down but a complete annotated example can be found here:

lang en_GB
keyboard --vckeymap=uk --xlayouts='gb'
network --onboot yes --bootproto=dhcp --activate
rootpw --iscrypted $1$JlSBrxl.$ksXaF7TIE.70iV12//V4R0
firewall --disabled
authconfig –enableshadow –enablemd5
selinux --permissive
timezone --utc Europe/london --isUtc
bootloader --location=mbr --append="crashkernel=auto rhgb quiet" --password=$1$JlSBrxl.$ksXaF7TIE.70iV12//V4R0
autopart --type=lvm
clearpart --linux --initlabel
firstboot --disabled
eula --agreed
services --enabled=NetworkManager,sshd
user --name=linux_user --iscrypted --password=$1$JlSBrxl.$ksXaF7TIE.70iV12//V4R0 --groups=wheel
%packages --ignoremissing --excludedocs
chkconfig ntpd on
chkconfig sshd on
chkconfig ypbind on
chkconfig iptables off
chkconfig ip6tables off
chkconfig yum-updatesd off
chkconfig haldaemon off
chkconfig mcstrans off
chkconfig sysstat off
echo "linux_user        ALL=(ALL)       NOPASSWD: ALL" >> /etc/sudoers.d/linux_user
sed -i "s/^.*requiretty/#Defaults requiretty/" /etc/sudoers
reboot --eject

Although this may look completely different it is still doing similar things as a Windows answerfile.

We are still detailing locale settings and encrypted passwords:

rootpw --iscrypted $1$JlSBrxl.$ksXaF7TIE.70iV12//V4R0

There is also a networking section. In this case I am specifying that I want the operating system to use DHCP:

network --onboot yes --bootproto=dhcp --activate

Also the packages section is quite useful. Here you can specify any packages you want to install during the operating system installation.

%packages --ignoremissing --excludedocs

In Part 3 we will dive into variables in more detail!

Thanks for reading!

Getting Started With Packer to Create vSphere Templates – Part 1

Virtual Machines Templates, why? Templates are a great way to achieve consistent, repeatable and fast virtual machine (VM) deployments, be it an on-premise vSphere environment or cloud based environment. Having up to date VM templates for each of the operating systems you use, is key to being able to deploy infrastructure quickly and easily across multiple platforms.

In this series of blog posts, I will be focusing on deploying virtual machine templates in vSphere, specifically vSphere 7, using a product called Packer by HashiCorp. Packer is an Infrastructure as Code tool specifically for template management.

There is so much that can be done with Packer. I aim to be able to give you enough information to be able to start your journey with Packer.

Throughout this series, I will reference two templates as examples. A Windows (Windows 2019 Core), and a Linux (CentOS 7) template to give you an idea of the differences, and will also give you a basis that you can apply to other operating systems. But to start we of course need to know how to install Packer and understand the components! Let’s get started!

Installing Packer

Firstly you are going to need to download the Packer executable from the Packer website, here. The latest version at the time of writing is 1.7.0. This is an exciting release for many reasons, but specifically that its moved over to HCL (Hashicorp Configuration Language) from JSON! This brings it closer inline with the other Hashicorp products such as Terraform which already use HCL.

You have a choice of downloads for multiple operating systems. Everything in this blog series will be done on a Windows 10 machine.

Now you need to unzip the download and copy ‘packer.exe’ to either an existing PATH directory or create a new one. For simplicity here, I have copied the executable to ‘C:\Windows\System32’.

Another option for installing on Windows is to use Chocolatey by running the following:

choco install packer

All installation options can be found here.

Once done, you can confirm its working by opening a PowerShell Terminal and running the Packer executable:


The Command Line

Packer has a simple command line to build your templates, you will have seen the available options when you ran ‘packer’ to check your install.

Lets take a look at a few of them that we might see during this series:

  • build: Builds the template you have defined.
  • fmt: This is a nice command that will format your code. Anyone who likes their code to look tidy formatting wise, will like this one!
  • hcl2_upgrade: For anyone that has been using Packer with JSON configuration files, this is a great starting point to get your code converted to HCL. Be aware its not perfect in my experience and will need to be manually tweaked, but it gets you on your way.
  • validate: This checks whether your template is valid. It will check to make sure your syntax is correct and has all mandatory values set for any resources you make use of.
  • version: This is a quick easy way to check which version of Packer you are currently using.

As we saw above, you can find brief descriptions for the remaining options by running ‘packer’ from the command line.


There are multiple blocks that can be used to build your virtual machine templates. You can find a complete list here, but lets take a look at some of the ones you will see throughout this series.

Source Blocks

There are two types of source blocks, top level that can be used and reused by multiple builder blocks, and then there are nested source blocks which can be used to inject build specific content.

Build Blocks

Build blocks are used to build your templates, in this case by referencing a source block. It can reference any top level source blocks you have referenced or source blocks nested within your build and merge them to produce a complete configuration.

Provisioner blocks and Post-Processor blocks are also referenced in the build block. More on what they do below…

snippet of a build block referencing a top level source block

Provisioner Blocks

Provisioners are how we interface with your template once the operating system is installed. They will use either SSH or WinRM to communicate with the operating system.

We will be focusing on three provisioners throughout the series; Shell for Linux, PowerShell for Windows, and a community managed provisioner called ‘Windows Update Provisioner’.

Both PowerShell and Shell can be used to run scripts, commands, copy files (you can also use the File Provisioner to do this), install software, basically anything you want. The Windows Update provisioner, is exactly what it sounds like. It’s a way of installing the latest Windows patches. More on that later.

There are multiple HashiCorp supported provisioners available which you can find here.

Post-Processor Blocks

Finally, Post-Processors, these run once the build is complete, but its not mandatory to use them. I haven’t really used them yet apart from producing a manifest file which you will see included later in the series.

Information on the available Post-Processors can be found here.

Folder Structure, Configuration Files and Scripts

There are many ways to set out your configuration files for your templates in which ever directory structure you wish. This is the way I have found logical for me; by separating the configuration out into multiple files (mainly the 3 highlighted in Bold below), it makes it easier to reuse your code.

Folder Structure

--> OperatingSystemName

You can have a set of folders per operating system .

Configuration Files, Scripts and Drivers

All Packer configuration files use the file extension; .pkr.hcl apart from your user defined variables file which uses the .pkrvar.hcl extension. Lets take a look at each file.

Variables Declaration file (example – variables.pkr.hcl): This file is where you declare all the variables you want to reference in your source, build or provisioner blocks. This includes user defined variables and environmental variables.

User Defined Variables file (example – win2019.pkrvar.hcl ): This file is where you will define your user variable values. This could include values for Template Name, CPU, RAM and disk size for instance. These variable values are in plain text, therefore you don’t want to be keeping sensitive values such as passwords in this file in any scenario outside of a lab. These can be handled by environment variables which we will see in later parts of the series.

‘Build’ file (example – win2019.pkr.hcl): This is where you define your template using a Source Block that we mentioned earlier, and build it using a Build Block. In this case we are going to be using the ‘vSphere-ISO’ Source.

Operating System Answer File (example autounattended.xml: This is the answer file needed to complete the installation of your operating system. For Windows this would be an autounattend.xml file and for CentOS, a kickstart.cfg file.

Scripts and Drivers: Finally you will need any scripts, drivers or media ready to reference in either the answer files or for use by a provisioner. The output file is not a prerequisite, as this is generated by the post processor at the end of the build.

In a later part of this series I will break down each of the components and blocks, and explain the content of a Windows and Linux template build in further detail.

So what next? In part 2, we will take a closer look at the operating system answer files and some example scripts & drivers that can be used or are required.

Thanks for reading!