Using vSphere Guest Customization Attributes with Windows and Terraform

3 minute read

VMware has capability to leverage GuestInfo attributes which allow passing of Metadata and Userdata blobs that can be used for Cloud-init. In very simple terms, once the machine boots it looks up these these values via VM tools and executes the declared YAML file using Cloud-Init. This post covers leveraging these attributes for Windows to bootstrap the operating system with a custom PowerShell script.

main

If you haven’t started using Cloud-Init on your Linux templates with vSphere now is the time. Unfortunately, this leaves Windows out in the cold since Cloud-Init is Linux only. While investigating a solution with Windows I did find Cloudbase-init which looked promising at first, but soon found it didn’t have any native capability of joining a Windows domain and had a lot of strange timing issues. I decided to dump Cloudbase-init and figure something else out.

Current Process

If you ever tried to use the run-once portion of a Windows customization you know it’s extremely limited in what you can run. Most of the time you can run a few basic commands or maybe a PowerShell script that is already included within the image but doesn’t offer a lot. Since these commands are run from the Sysprep process the commands become stored in the unattend.xml file stored in C:\Windows\Panther which could contain secrets being passed into any external command (e.g. domain join credentials).

Solution

To prevent the need for an external script or handling of the unattend.xml file I decided to leverage the metadata attribute to store my PowerShell script. The script gets rendered via Terraform to allow variable substitution then converted to base64 and finally being set in the attribute. The main components of the process:

Code is available here: https://github.com/ryancbutler/terraform-vsphere-windows-userdata-vm

PowerShell Template

The bootstrap.ps1 script uses variable substitution to render the script with any needed data for the VM such as passwords and domain info. Anything with {var} gets replaced via Terraform.

For example, the bootstrap.ps1 template contains the following code:

#Add to domain
write-host "Add to domain"
$domain = "${addomain}"
$password = "${adpass}" | ConvertTo-SecureString -asPlainText -Force
$username = "$domain\${aduser}" 
$credential = New-Object System.Management.Automation.PSCredential($username, $password)
Add-Computer -DomainName $domain -OUPath "${adou}" -Credential $credential

Then Terraform renders the file with the data block within main.tf:

data "template_file" "init" {
  template = file("${path.module}/templates/bootstrap.ps1")
  vars = {
    adpass   = var.ADPass
    adou     = var.ADOU
    aduser   = var.ADUser
    addomain = var.ADDomain
  }
}

Setting Attribute

Now that the script is rendered the VM attribute needs to be set.

In the Terraform code it’s done with the extra_config block in the vsphere_virtual_machine resource within main.tf:

extra_config = {
    "guestinfo.userdata"          = base64encode(data.template_file.init.rendered)
    "guestinfo.userdata.encoding" = "base64"
  }

Running the code

With the attribute set, we need to execute it on boot. To do this I’m using the run-once command from the Windows customization spec to pull down the attribute blob using the rpctool.exe that comes with VMware tools (no extra install needed), convert the base64 to PowerShell then execute the script which is being set in the vsphere_virtual_machine resource as you can see here in the run_once_command_list within main.tf:

clone {
    template_uuid = data.vsphere_virtual_machine.template.id
    linked_clone  = false
    customize {
      windows_options {
        computer_name    = var.vm_name
        admin_password   = var.vm_password == "" ? random_password.password.result : var.vm_password
        workgroup        = "WORKGROUP"
        auto_logon       = true
        auto_logon_count = 1
        time_zone        = var.vm_timezone
        run_once_command_list = [
          "powershell \"cd \"$env:ProgramFiles\\VMware\\VMware~1\";[System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($(.\\rpctool.exe \\\"info-get guestinfo.userdata\\\")))|out-file C:\\bootstrap.ps1\"",
          "cmd.exe /C Powershell.exe -ExecutionPolicy Bypass -File C:\\bootstrap.ps1"
        ]

      }

      network_interface {
        ipv4_address    = var.vm_ip
        ipv4_netmask    = var.vm_netmask
        dns_server_list = var.vm_dns_servers
        dns_domain      = var.vm_dns_domain
      }

      ipv4_gateway = var.vm_gateway
    }
  }

It’s important to have auto_logon_count set to 1 so the VM logs in after the Sysprep process to kickoff the command.

Cleaning up

In order not to leak secrets in either the attribute or script I do the following as part of my bootstrap process in the PowerShell script.

  • Clear out the metadata attribute with rpctool so the base64 blob is deleted (notice two spaces at end)
  • Delete the created PS1 file
#Clear userdata
write-host "Clear userdata"
set-location "$env:ProgramFiles\VMware\VMware~1\"
.\rpctool.exe "info-set guestinfo.userdata  "

#Remove Script
write-host "Remove Script"
Remove-Item -Path "C:\bootstrap.ps1" -Force

Leave a comment