I’ve been fortunate to work closely with VMware and in particular, the UK-based VMware Cloud on AWS team, over the last 18 months or so. Stagecoach were one of the first businesses in the UK to adopt it as their cloud platform of choice alongside AWS and so we have forged a strong relationship with their team, in particular Nico Vibert. I tell you this as some simple promotion for his website, because a good percentage of this code was his. If you’re into Terraform and VMWonAWS then head over to his website.

Goals:

  1. Terraform connectivity to my VMware Cloud on AWS SDDC
  2. Clone existing vSphere template to singular VM
  3. Clone existing vSphere template to multiple VMs
    1. Dynamically name the VMs
    2. Dynamically name the machines in Windows
    3. Assign an sequential IP to each VM
    4. Join to an Active Directory domain

Connecting to my SDDC was achieved by using Terraform 0.12 and the vsphere provider. Links to download below:

Terraform 0.12

vSphere Provider 1.16.1

At the end of the article I’ve provided a zipped up working copy of my code to replicate in your VMWare Cloud on AWS SDDC.

The code

I’ve provided the .tf files at the end of the article (obfuscated where appropriate) that is based on Nico’s code as the base.

Main.tf

provider "vsphere" {
  user                  = var.user
  password              = var.password
  vsphere_server        = var.vsphere_server
  allow_unverified_ssl  = true
}


/*================
Deploy Virtual Machines
=================*/

module "VMs" {
  source = "../VMs"
  data_center           = var.data_center
  cluster               = var.cluster
  workload_datastore    = var.workload_datastore
  compute_pool          = var.compute_pool 
  admin_password        = var.admin_password
  join_domain           = var.join_domain
  domain_admin_user     = var.domain_admin_user
  domain_admin_password = var.domain_admin_password
}

The key takehome here is that the provider is declared as vsphere with variables declared for the inputs. and that the various variables dedicated towards the VM build are declared


variables.tf

variable "data_center"              {}
variable "cluster"                  {}
variable "workload_datastore"       {}
variable "compute_pool"             {}
variable "admin_password"           {}
variable "join_domain"              {}
variable "domain_admin_user"        {}
variable "domain_admin_password"    {}
variable "user"                     {}
variable "password"                 {} 
variable "vsphere_server"           {}

Nothing ground breaking here, just the declaration of variables.


terraform.auto.tfvars

data_center              = "SDDC-Datacenter"
cluster                  = "Cluster-1" 
workload_datastore       = "WorkloadDatastore" 
compute_pool             = "Compute-ResourcePool"
admin_password           = "password"
join_domain              = "domain.com"
domain_admin_user        = "[email protected]"
domain_admin_password    = "password"
user                     = "[email protected]"
password                 = "password"
vsphere_server           = "vcenter.sddc-xx-xx-xx-xxx.vmwarevmc.com" 

This file essentially contains all the sensitive passwords, etc that get passed to the variables declared in the main.tf and vms.tf.


version.tf

terraform {
  required_version = ">= 0.12"
  required_providers {vsphere = "~> 1.16"}
}

This is pretty simple, just declaring which components are required to run.


VMs.tf

variable "data_center" {}
variable "cluster" {}
variable "workload_datastore" {}
variable "compute_pool" {}
variable "admin_password" {}
variable "join_domain" {}
variable "domain_admin_user" {}
variable "domain_admin_password" {}

data "vsphere_datacenter" "dc" {
  name                      = var.data_center
}
data "vsphere_compute_cluster" "cluster" {
  name                      = var.cluster
  datacenter_id             = data.vsphere_datacenter.dc.id
}
data "vsphere_datastore" "datastore" {
  name                      = var.workload_datastore
  datacenter_id             = data.vsphere_datacenter.dc.id
}

data "vsphere_resource_pool" "pool" {
  name                      = var.compute_pool
  datacenter_id             = data.vsphere_datacenter.dc.id
}

data "vsphere_network" "network" {
  name                      = "SDDC-UKB-Core_Network"
  datacenter_id             = data.vsphere_datacenter.dc.id
}
data "vsphere_virtual_machine" "vm_template" {
  name                      = "WinServer-2016-Template"
  datacenter_id             = data.vsphere_datacenter.dc.id
}

# ================================================

resource "vsphere_virtual_machine" "vm1" {
  count                     = "2"
  name                      = "terra-testVM-${count.index + 1}"
  resource_pool_id          = data.vsphere_resource_pool.pool.id
  datastore_id              = data.vsphere_datastore.datastore.id
  folder                    = "Workloads"
  num_cpus                  = 2
  memory                    = 4096
  guest_id                  = "windows9Server64Guest"

  scsi_type                 = data.vsphere_virtual_machine.vm_template.scsi_type

  network_interface {
    network_id              = data.vsphere_network.network.id
  }

  disk {
    label                   = "disk0"
    size                    = 80
  }
  disk {
    label                   = "disk1"
    size                    = 80
   unit_number             = 1
  }
  
  clone {
    template_uuid           = data.vsphere_virtual_machine.vm_template.id
        
    customize {
       windows_options{
         computer_name         = "terra-testVM-${count.index + 1}"
         admin_password        = var.admin_password
         join_domain           = var.join_domain
         domain_admin_user     = var.domain_admin_user
         domain_admin_password = var.domain_admin_password
          auto_logon           = "true"
          auto_logon_count     = "5"
          run_once_command_list= [
           #"winrm quickconfig -force",
           #"winrm set winrm/config @{MaxEnvelopeSizekb=\"100000\"}",
           #"winrm set winrm/config/Service @{AllowUnencrypted=\"true\"}",
           #"winrm set winrm/config/Service/Auth @{Basic=\"true\"}",
          ]
    }
    network_interface {
      ipv4_address          = "172.29.0.${80 + count.index}"
      ipv4_netmask          = 24
      dns_server_list       = ["192.168.1.1", " 192.168.1.2"]
    }
    ipv4_gateway            = "172.29.0.1"
    }
  }
}

The Challenges

I had a few issues here with this, which took some time for me to understand. The first is:

scsi_type        = data.vsphere_virtual_machine.vm_template.scsi_type

This essentially ensures that the same scsi type is used in the template that you’re cloning. My clones were all failing without this.


disk {
    label                   = "disk0"
    size                    = 80
  }
  disk {
    label                   = "disk1"
    size                    = 80
    unit_number             = 1
  }

The second part here is down to my Windows template having 2 disks. With Terraform, and additional disks above disk0 need the unit_number declaring sequentially.


count                     = "2"
name                      = "terra-testVM-${count.index + 1}" 

This line essentially declares the number of clones you want from your template, in this case, 2.

The name field is what defines what the VM will be called within vSphere. In this case, there is a count beginning with 1, and that is prefixed with “terra-testVM-“. Essentially, us deploying 2 clones will leave us with 2 VMs with the following names:

terra-testVM-1

terra-testVM-2


computer_name         = "terra-testVM-${count.index + 1}"

This line does a very similar job to the VM name, but names the Windows VM within Windows.


ipv4_address          = "172.29.0.${80 + count.index}"

This essentially sets the IP on the VM, beginning with 172.29.0.80 for the first VM, with the second being 172.29.0.81 and so on.


join_domain           = var.join_domain
domain_admin_user     = var.domain_admin_user
domain_admin_password = var.domain_admin_password

The 3 above lines, alongside the variable declarations in the other files are all that are needed to join your machine to an AD domain

What’s next?

I did attempt to get Terraform to call a powershell script once the VM had booted, you can see evidence of me trying here:

 auto_logon           = "true"
 auto_logon_count     = "5"
 run_once_command_list= [
     #"winrm quickconfig -force",
     #"winrm set winrm/config @{MaxEnvelopeSizekb=\"100000\"}",
     #"winrm set winrm/config/Service @{AllowUnencrypted=\"true\"}",
     #"winrm set winrm/config/Service/Auth @{Basic=\"true\"}",

Essentially, the above code ensures that WinRM is enabled, to allow me to remotely run things on the server. But for what ever reason I wasn’t able to get this working. If anyone can help out with this – drop me a message.

From what I have read though, Terraform is quite limited in comparison to other automation platforms, such as Ansible, for interacting inside Windows.

I hope this is helpful to someone. All the above files are contained in the below zip file.

TerraformVMCvSphere.zip