Deploy Multiple vSAN Witness Appliances Using Terraform
I was involved in a project where we had to replace the vSAN Witness Appliance. I told customer that since we’ve had to do it multiple times, we should automate the process using a tool like Terraform! he was also happy with this decision!
If you have ever deployed vSAN Witness Appliances manually through vCenter, you already know it is a repetitive and time-consuming task that can sometimes lead to human error.
My recommendation is automation—whether it is with Terraform or another tool like PowerShell which is my favorite tool, but since I am expanding my Terraform knowledge, decided to complete this task using Terraform.
In this post, I will show you how to deploy multiple vSAN Witness nodes (OVF) using Terraform — with static IPs, and full OVF customization.
Before you start, make sure you have:
- vCenter access
- The vSAN Witness OVA
- Terraform installed
- vSphere Terraform provider
- A basic understanding of Terraform
I assume you already have a basic understanding of Terraform, but still try to explain each steps as much as possible.
Before you start, you need a folder that contains the following files.
vsan-witness/
├── main.tf
├── variables.tf
├── terraform.tfvars
Here is what your Terraform project structure should look like this one, other files are not required at this step, because Terraform will generate them automatically.

But why do we need to each of these files?
- main.tf: The main configuration file where you define the logic to deploy the vSAN Witness Appliance — such as provider settings, resource definitions (like virtual machines, network interfaces, OVF configurations). Think of this file as what I want to build!
- variables.tf: This file is used to declare input variables for your Terraform configuration including the structure, types, and default values of the variables you will use in main.tf. In fact you define what inputs your infrastructure expects.
- terraform.tfvars: In this file, you assign real values to the variables declared in variables.tf. For example usernames, IPs, passwords, names, IPs and etc.
Since I am learning Terraform, I have understood that it is common practice to separate variable definition and values in two files (variables.tf and terraform.tfvars) to make the code reusable with the same configuration by simply changing the terraform.tfvars file, without modifying the core logic. In my case, I defined those values that change per node in a terraform.tfvars and those that remain the same among all vSAN Witness node, such as DNS, gateway in the variables.tf.
Now I will define OVF configuration and other configurations that are same for the whole Witnesses in variables.tf:
variable "vsphere_user" {}
variable "vsphere_password" {}
variable "vsphere_server" {}
variable "mgmt_ips" {
description = "List of management IP addresses for witness VMs"
type = list(string)
}
variable "vsan_ips" {
description = "List of vSAN network IP addresses for witness VMs"
type = list(string)
}
variable "hostnames" {
description = "List of vSAN network IP addresses for witness VMs"
type = list(string)
}
variable "datacenter" { default = "NRW" }
variable "cluster" { default = "Cluster01" }
variable "host" { default = "esxi01.vmware.local" }
variable "datastore" { default = "DataStore01" }
variable "cpu" { default = "2" }
variable "memory" { default = "32768" }
variable "network1" { default = "VLAN_10" }
variable "network2" { default = "VLAN_20" }
variable "resource_pool" { default = "Resources" }
variable "root_password" {default = "VMware123!" }
variable "mgmt_netmask" {default = "255.255.255.0" }
variable "mgmt_gateway" {default = "10.10.10.1" }
variable "dns_domain" {default = "vmware.local" }
variable "dns_servers" {default = "10.10.10.2, 10.10.10.3" }
variable "ntp_servers" {default = "10.10.10.30, 10.10.10.31" }
variable "vsan_netmask" {default = "255.255.255.0" }
variable "vsan_gateway" {default = "10.10.20.1" }
variable "ovf_path" {
description = "Path to the local vSAN Witness OVF"
default = "C:\\VMware-VirtualSAN-Witness-7.0U3l-21424296.ova"
}
Now let us assign values by modifying the terraform.tfvars, this is where the actual IPs and credentials go:
vsphere_user = "administrator@vsphere.local"
vsphere_password = "P@ssw0rd"
vsphere_server = "vc01.vmware.local"
mgmt_ips = [
"10.10.10.11",
"10.10.10.12"
]
vsan_ips = [
"10.10.20.78",
"10.10.20.79"
]
hostnames = [
"wit01",
"wit02"
]
and finally in the main.tf I define the whole process and state the I am looking to see.
provider "vsphere" {
user = var.vsphere_user
password = var.vsphere_password
vsphere_server = var.vsphere_server
allow_unverified_ssl = true
}
# Get datacenter
data "vsphere_datacenter" "dc" {
name = var.datacenter
}
# Get cluster
data "vsphere_compute_cluster" "cluster" {
name = var.cluster
datacenter_id = data.vsphere_datacenter.dc.id
}
# Get datastore
data "vsphere_datastore" "datastore" {
name = var.datastore
datacenter_id = data.vsphere_datacenter.dc.id
}
# Get Host
data "vsphere_host" "host" {
name = var.host
datacenter_id = data.vsphere_datacenter.dc.id
}
# Get network
data "vsphere_network" "management" {
name = var.network1
datacenter_id = data.vsphere_datacenter.dc.id
}
# Get network
data "vsphere_network" "vsan" {
name = var.network2
datacenter_id = data.vsphere_datacenter.dc.id
}
# Deploy vSAN Witness OVF
resource "vsphere_virtual_machine" "vsan_witness" {
count = 1
name = var.hostnames[count.index]
resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
datastore_id = data.vsphere_datastore.datastore.id
host_system_id = data.vsphere_host.host.id
datacenter_id = data.vsphere_datacenter.dc.id
num_cpus = var.cpu
memory = var.memory
ovf_deploy {
local_ovf_path = var.ovf_path
ip_allocation_policy = "fixed"
ip_protocol = "IPv4"
disk_provisioning = "thin"
enable_hidden_properties = true
deployment_option = "large"
}
network_interface {
network_id = data.vsphere_network.management.id
adapter_type = "vmxnet3"
}
network_interface {
network_id = data.vsphere_network.vsan.id
adapter_type = "vmxnet3"
}
vapp {
properties = {
"guestinfo.passwd" = var.root_password
"guestinfo.vsannetwork" = "Secondary"
"guestinfo.ipaddress0" = var.mgmt_ips[count.index]
"guestinfo.netmask0" = var.mgmt_netmask
"guestinfo.gateway0" = var.mgmt_gateway
"guestinfo.dnsDomain" = var.dns_domain
"guestinfo.hostname" = var.hostnames[count.index]
"guestinfo.dns" = var.dns_servers
"guestinfo.ntp" = var.ntp_servers
"guestinfo.ipaddress1" = var.vsan_ips[count.index]
"guestinfo.netmask1" = var.vsan_netmask
"guestinfo.gateway1" = var.vsan_gateway
}
}
wait_for_guest_net_timeout = 0
wait_for_guest_ip_timeout = 0
}
Using count = 1, I defined how many appliances do I need to deploy.
At first, I didn’t mention CPU and Memory in the main.tf because vSAN Witness OVA includes different deployment options like medium, large, which predefine CPU, memory, and disk sizes. However I encountered a purple screen, and OVF configuration including CPU and Memory was ignored in Terraform.

So I explicitly define num_cpus
and memory
to prevent purple screen and apply right values for them.
Now it is time to deploy it! open the Terminal and navigate to the folder that your Terraform files are and initialize Terraform using commands “terraform init“. This step sets up the Terraform environment, downloads the correct vSphere provider, and prepares your working directory.

If you run this command once, you don’t need to run it again, since the provider has already been downloaded.
Now I have to review the execution plan, it shows me an overview of what Terraform is going to do. I also saved the plan using –out=vsan-witness option to make it easier and safer to apply the plan.
terraform plan -out=vsan-witness
Now you will see the output of the Terraform plan command, which indicate that:
- The 1 VMs it’s about to create (Plan: 1 to add)
- The OVF config it will use
- All the networks and IP addresses assigned

As you can see in the screenshot, next step is to run the plan using:
terraform apply "vsan-witness"
Now the magic happens! I deployed 7 vSAN-Witness appliances in less than a minute!
