Terraform builds VMs and other infrastructure from a text file, so this is to stop the creation of VM from New VM wizard, and it gives the same results every time with no wizard typo’s and it’s in Git, so its version controlled. So, let’s kick off this journey with building something around Terraform first.
Terraform configuration files are text files with a .tf file extension
Commands in Terraform
There are five basic terraform commands: terraform init, terraform plan, terraform apply, and terraform destroy. terraform init initialise the vSphere provider terraform plan is what allows you to see what resources are being created/destroyed based on your configuration file. If everything checks out, terraform apply builds your infrastructure and reports on what was created. terraform destroy, destroys the resources defined in your configuration file and reports on what was destroyed.
You dont need to be a master of this product but just need to understand how this works
Create an Automation User Account
A new user svc-automation@vsphere.local is added n vSphere which will be used for our Ansible, packer and terraform to connect into it and have given it administrator rights…The proper version in production will have bit more striped down version of rights.

Installing the Packages
Packer and Terraform just need to run somewhere that can reach vCenter, so I added them to my existing Ansible host. In production this would more likely be a CI/CD runner or a dedicated automation server.
Install Packer
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install -y packer xorriso
Install Terraform
sudo apt install -y terraform
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following package was automatically installed and is no longer required:Check the versions:
awk@ansible:/etc/ansible/practice$ packer --version
Packer v1.16.2
wk@ansible:/etc/ansible/practice$ terraform version
Terraform v1.17.1
on linux_amd64Create a directory for Terraform
To get started, create a new folder for this configuration
mkdir -p ~/lab-automation/terraform && cd ~/lab-automation/terraform
Getting a template
Before Terraform can build anything, it needs a template to clone so we have a VM deployed in our vCenter and converted into a template.

The Four Terraform files
Terraform has 4 main files and I’ve kept it all under my Ansible repo under etc/ansible/terraform
providers.tf– The platform you are going to manage eg: vSpheredata.tf– finds the datacenter, cluster, datastore, templates and networks in vCenter, plus the OS and VLAN tablesmain.tf– create this VMenv.sh– vCenter credentials
In addition to 4 files, there are few more
variables.tf– define variables in configuration and this is the file we touch daily to add a new VM or a disk for exampleterraform.tfvars– values of variables configuredorganise.tf– for organising VM’s into folders
The most important of this is the terraform.tfvars file which we will editing daily to add or remove VM’s..
Building the dev/prod folder
My vCenter has two clusters on it, prod and dev so create the folders
mkdir -p /etc/ansible/terraform/dev /etc/ansible/terraform/prodConfigure the vSphere Provider
The first part to creating a Terraform configuration file is to define the provider you will interact with. In this example, it will create a virtual machine using the vSphere provider. The vSphere provider has some required fields like the user, password and server name which can be provided here or via an env file
providers.tf
terraform {
required_providers {
vsphere = {
source = "vmware/vsphere"
version = ">= 2.13.0"
}
}
}
provider "vsphere" {
allow_unverified_ssl = true
}data.tf
Names must match vCenter exactly. The values here are
- vsphere_datacenter: give name of datacenter on vSphere here
- vsphere_compute_cluster – give name of our dev cluster here
- vsphere_datastore_cluster – our datastore cluster
- templates – list of all templates now in our vCenter
- networks – this is usually obtained from IPAM but here we added two VLAN 1611 and 1619
data "vsphere_datacenter" "dc" {
name = "DC1"
}
data "vsphere_compute_cluster" "cluster" {
name = "Cluster01"
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_datastore_cluster" "dsc" {
name = "Lab-DatastoreCluster"
datacenter_id = data.vsphere_datacenter.dc.id
}
locals {
templates = {
ubuntu = "tpl-ubuntu-2404"
# centos = "tpl-centos-9" # uncomment once the template exists
# win2022 = "tpl-win2022"
}
networks = {
"1611" = { portgroup = "VM-1611-mgmt", gateway = "172.16.11.1", prefix = 24 }
"1619" = { portgroup = "VM-1619-vmnet", gateway = "172.16.19.1", prefix = 24 }
}
}
data "vsphere_virtual_machine" "template" {
for_each = toset([for vm in var.vms : vm.os])
name = local.templates[each.key]
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_network" "net" {
for_each = toset([for vm in var.vms : tostring(vm.vlan)])
name = local.networks[each.key].portgroup
datacenter_id = data.vsphere_datacenter.dc.id
}main.tf – The VM Recipe
This is where we define how the VM should be provisioned so we can give in sizes we need, type of disk,
locals {
sizes = {
small = { cpus = 1, memory = 2048 }
medium = { cpus = 2, memory = 4096 }
large = { cpus = 4, memory = 8192 }
}
}
resource "vsphere_virtual_machine" "vm" {
for_each = var.vms
name = each.key
resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
datastore_cluster_id = data.vsphere_datastore_cluster.dsc.id
folder = vsphere_folder.dev.path
tags = [vsphere_tag.dev.id]
num_cpus = local.sizes[each.value.size].cpus
memory = local.sizes[each.value.size].memory
guest_id = data.vsphere_virtual_machine.template[each.value.os].guest_id
firmware = data.vsphere_virtual_machine.template[each.value.os].firmware
scsi_type = data.vsphere_virtual_machine.template[each.value.os].scsi_type
network_interface {
network_id = data.vsphere_network.net[tostring(each.value.vlan)].id
adapter_type = data.vsphere_virtual_machine.template[each.value.os].network_interface_types[0]
}
disk {
label = "disk0"
size = data.vsphere_virtual_machine.template[each.value.os].disks[0].size
thin_provisioned = true
}
# extra data disk, only for VMs that ask for one
dynamic "disk" {
for_each = each.value.data_disk_gb > 0 ? [1] : []
content {
label = "disk1"
unit_number = 1
size = each.value.data_disk_gb
thin_provisioned = true
}
}
clone {
template_uuid = data.vsphere_virtual_machine.template[each.value.os].id
customize {
dynamic "linux_options" {
for_each = startswith(each.value.os, "win") ? [] : [1]
content {
host_name = each.key
domain = "lab.internal"
}
}
dynamic "windows_options" {
for_each = startswith(each.value.os, "win") ? [1] : []
content {
computer_name = each.key
admin_password = var.windows_admin_password
workgroup = "WORKGROUP"
}
}
network_interface {
ipv4_address = each.value.ip
ipv4_netmask = local.networks[tostring(each.value.vlan)].prefix
}
ipv4_gateway = local.networks[tostring(each.value.vlan)].gateway
dns_server_list = ["172.16.11.5"]
dns_suffix_list = ["lab.internal"]
}
}
}
output "vm_ips" {
value = { for name, vm in vsphere_virtual_machine.vm : name => vm.default_ip_address }
}variables.tf – What a VM line can contain
variable "vms" {
type = map(object({
ip = string
vlan = optional(number, 1611)
size = optional(string, "small")
os = optional(string, "ubuntu")
data_disk_gb = optional(number, 0)
}))
}
variable "windows_admin_password" {
type = string
sensitive = true
default = null
}organise.tf – A vCenter folder for dev VMs, a tag category, and a Dev tag.
resource "vsphere_folder" "dev" {
path = "Terraform-Dev"
type = "vm"
datacenter_id = data.vsphere_datacenter.dc.id
}
resource "vsphere_tag_category" "env" {
name = "Environment"
cardinality = "SINGLE"y
associable_types = ["VirtualMachine"]
}
resource "vsphere_tag" "dev" {
name = "Dev"
category_id = vsphere_tag_category.env.id
}
terraform.tfvars – Define number of VM’s we need in this file
vms = {
"tf-ubuntu-01" = { ip = "172.16.11.151" }
"tf-ubuntu-02" = { ip = "172.16.11.152" }
"tf-ubuntu-03" = { ip = "172.16.11.153", cpus = 4, memory = 4096 }
}env.sh – vCenter login and password
export VSPHERE_SERVER=vcenter.lab.internal
export VSPHERE_USER=svc-automation@vsphere.local
read -s -p "vSphere password: " VSPHERE_PASSWORD; echo; export VSPHERE_PASSWORDThat’s the whole configuration side, so now how do we work on this daily ?
Apply the plan
Here we’ve defined 3 VM’s to be deployed in the terraform.tfvars in this below spec
vms = {
“tf-ubuntu-01” = { ip = “172.16.11.151” }
“tf-ubuntu-02” = { ip = “172.16.11.152” }
“tf-ubuntu-03” = { ip = “172.16.11.153”, cpus = 4, memory = 4096 }
}
First run this command to check-in our vCenter credentials
awk@ansible:/etc/ansible/terraform$ source env.sh
Password
Now, navigate to dev directory and run terraform init – this downloads the vSphere plugin
terraform init
terraform plan -out=tfplan shows what Terraform is about to do so it’s a Save and it says 3 VM’s are going to be deploye d

Run terraform apply tfplan to deploy the VM

After few minutes, the VM are live in its folder

awk@ansible:/etc/ansible/terraform$ ansible -i “172.16.11.151,172.16.11.152,172.16.11.153” all -m ping
172.16.11.151 | SUCCESS => {
“ansible_facts”: {
“discovered_interpreter_python”: “/usr/bin/python3.12”
},
“changed”: false,
“ping”: “pong”
}
The VM is reachable

Drift – Extending Data Disk from vSphere
This is pretty cool; So, what happens if I change a disk on one server ask to terraform what it thinks
Run terraform plan again

The disk grown can’t be taken so all we need to do now is just update the terraform config file so back to the terraform.tfvars file

Change the entry from 50 GB to 60 GB

rerun the terraform plan again and now it says the infra matches configuration.

Drift – Extending OS Disk over Template Size from vSphere
This is pretty cool; So, what happens if I we decide to extend the OS disk . I’ve just extended the OS disk on one of my VM

Run terraform plan again and this time it detects disk 0 has a problem.

The disk grown can’t be taken so all we need to do now is just update the terraform config file so back to the terraform.tfvars file

Add an entry for os_disk

Under the variables.tf, we add a new entry os_disk_gb as that was missing previously

Finally, under the main.tf , that line decides how big each VM’s main (OS) disk is which at the moment is set to template size max

So, we replace that allowing the OS disk to outgrow template size

rerun the terraform plan again and now it says the infra matches configuration.

So now we are good.
Drift – Extending OS Disk via Terraform
So, what if we don’t want to extend the disk from vSphere but want Terraform doing it all.
Go back to the terraform.tfvars file and let’s say we want to extend the data disk on dev-ubuntu-03 to 70 GB

Change the 60 GB to 70 GB and save the file

Under the variables.tf, we add a new entry os_disk_gb as that was missing previously
rerun the terraform plan again and now it says the infra matches configuration.

Run terraform plan -out=tfplan to do a save of our plan we are just about to deploy

So now we are good. so just apply it by running terraform apply tfplan

The disk size has changed

This may be a complex one to master but as far the vSphere admin job is concerned most of our work is centered around the terraform.tfvars..
If we wish to delete a VM, remove the VM entry by commenting it as shown
Destroying Just 1 VM
Comment the VM we wish to delete

Run terraform plan -out=tfplan to do a save of our plan we are just about to deploy

Finally run the removal

The 3rd VM is now removed

Another way to delete a VM is by running
terraform destroy -target=’vsphere_virtual_machine.vm[“dev-ubuntu-02”]’

Run the terraform state list just to check state of terraform and it should now show just 1 VM remaining

Creating New VM and organising into folders
I’ve created a number of folders inside my vCenter so what if we want to deploy VM’s straight into these folders

This is actually pretty easy so again all we need to touch is just 1 file terraform.tfvars and we just need to add the new VM’s and just say place it in the folder

Let’s re-run the terraform plan and it shows exactly what Terraform would do, without changing anything.

Run terraform plan -out=tfplan to do a save of our plan we are just about to deploy

Make the changes by running terraform apply tfplan

All the VM’s are initially created to their folders

Then after power up, Terraform moves all moves around to correct folders
Destroying All the VMs
If these are test VMs you may want to remove them after testing is done. To clean up, simply issue:

Protecting VMs from being deleted
Terraform deletes any VM that’s removed from the list as above and this is actually dangerous where one wrong line could remove a whole lot in one shot.
Terraform has a safety catch for this, called lifecycle. Add it inside the VM resource in main.tf,

Let’s now try removing a VM

Test it via terraform plan -out=tfplan

terraform plan refuses with “Instance cannot be destroyed”. Nothing is touched so that’s how you protect the VM.
If we now wish to delete the VM we need to comment line 80 on this one or set it to false and re-run the above steps.

That’s pretty much it to Terraform. This skill may be a complex one to master but as far the vSphere admin job is concerned most of our work is centered around the terraform.tfvars.
References
How to Configure VMware vSphere Provider in Terraform

