The heaviest module in the vSphere library: a single vsphere_virtual_machine keystone rendered from deeply-nested, repeating provider blocks β disk, network_interface, cdrom, clone β customize β {linux,windows}_options, ovf_deploy, vtpm, and video_card β all driven by dynamic blocks. Create a blank VM, clone from a template, or deploy from OVF/OVA.
Why it matters: A virtual machine is where every other object in the library converges β a resource pool decides where it runs, a datastore decides where it lives, a port group decides how it talks, a template decides what it boots, and a tag decides how it is governed. This module turns that convergence into one auditable, secure-by-default unit. Disks render thick lazy-zeroed, power operations are graceful by default (a stuck guest fails the run rather than getting yanked), Windows customization secrets are redacted in plan output, and every parent object is consumed by MOID β the module performs no ambient vCenter lookups of its own. The caller resolves parents; the module renders the VM. No
terraform applyis ever performed here: everything is plan-only until a human reviews and applies.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
| File | Purpose |
|---|---|
providers.tf |
required_version >= 1.12.0; pins vmware/vsphere ~> 2.0. No provider {} block β the caller configures auth. |
variables.tf |
All inputs as deeply-typed object schemas; secure defaults baked in; validation {} blocks for closed value sets and cross-field invariants. |
main.tf |
Thin total renderer: one vsphere_virtual_machine.this with every nested block emitted via dynamic + try. |
outputs.tf |
id (MOID) first, then name, then computed identity/network attributes. No secret is emitted. |
README.md |
This document. |
SCOPE.md |
The design contract β written before any .tf. README is consistent with it. |
examples/main.tf |
A working, plan-only usage example (clone + Linux customize). |
Rendered via the Mermaid Chart MCP (validated
flowchart). This module is highlighted in#607078. Upstream sibling modules feed inputs by MOID; downstream siblings consume this module's outputs.
graph LR
rp["tf-mod-vsphere-resource-pool"]
cc["tf-mod-vsphere-compute-cluster"]
ds["tf-mod-vsphere-nas-datastore /<br/>tf-mod-vsphere-vmfs-datastore"]
pg["tf-mod-vsphere-distributed-port-group /<br/>distributed_virtual_switch"]
cl["tf-mod-vsphere-content-library"]
gos["tf-mod-vsphere-guest-os-customization"]
sp["tf-mod-vsphere-vm-storage-policy"]
fold["tf-mod-vsphere-folder"]
tag["tf-mod-vsphere-tag"]
vm["tf-mod-vsphere-virtual-machine"]
snap["tf-mod-vsphere-virtual-machine-snapshot"]
vapp["tf-mod-vsphere-vapp"]
rp -->|resource_pool_id| vm
cc -->|resource_pool_id| vm
ds -->|datastore_id| vm
pg -->|network_id| vm
cl -->|template_uuid| vm
gos -->|customization_spec.id| vm
sp -->|storage_policy_id| vm
fold -->|folder| vm
tag -->|tags| vm
vm -->|uuid| snap
vm -->|uuid target_id| vapp
style vm fill:#607078,color:#ffffff,stroke:#3f4a50,stroke-width:2px
Rendered via the Mermaid Chart MCP (validated
flowchart). One keystone resource (#00A1E0) with provider nested blocks (#607078) β there are no separate child resources.
graph TD
this["vsphere_virtual_machine.this<br/>(keystone)"]
disk["disk {}<br/>dynamic over var.disks<br/>(map by label, max 60)"]
nic["network_interface {}<br/>dynamic over var.network_interfaces<br/>(map, max 20)"]
cdrom["cdrom {}<br/>dynamic over var.cdroms (max 2)"]
clone["clone {}<br/>optional singleton"]
ovf["ovf_deploy {}<br/>optional singleton"]
vapp["vapp {}<br/>OVF / vApp properties"]
vtpm["vtpm {}<br/>optional singleton"]
video["video_card {}"]
cspec["customization_spec {}"]
customize["customize {}"]
linux["linux_options {}"]
windows["windows_options {}<br/>(sensitive passwords)"]
cnic["network_interface []<br/>positional per NIC"]
g3d["graphics_3d {}"]
this --> disk
this --> nic
this --> cdrom
this --> clone
this --> ovf
this --> vapp
this --> vtpm
this --> video
clone --> cspec
clone --> customize
customize --> linux
customize --> windows
customize --> cnic
video --> g3d
style this fill:#00A1E0,color:#ffffff,stroke:#0077a8,stroke-width:2px
style disk fill:#607078,color:#ffffff
style nic fill:#607078,color:#ffffff
style cdrom fill:#607078,color:#ffffff
style clone fill:#607078,color:#ffffff
style ovf fill:#607078,color:#ffffff
style vapp fill:#607078,color:#ffffff
style vtpm fill:#607078,color:#ffffff
style video fill:#607078,color:#ffffff
style cspec fill:#7d8c94,color:#ffffff
style customize fill:#7d8c94,color:#ffffff
style linux fill:#7d8c94,color:#ffffff
style windows fill:#7d8c94,color:#ffffff
style cnic fill:#7d8c94,color:#ffffff
style g3d fill:#7d8c94,color:#ffffff
Minimum starting set for the Terraform integration account. Validate via vCenter Check Privileges, and scope each privilege to the specific datastore, network, and pool the VM uses β do not grant datacenter-wide. This is the broadest privilege set in the library.
| Privilege | Why |
|---|---|
VirtualMachine.Inventory.Create |
Create the VM |
VirtualMachine.Config.AddNewDisk |
Add disks (disk {}) |
VirtualMachine.Config.AddRemoveDevice |
Add/remove disks, NICs, CD-ROMs |
VirtualMachine.Config.Memory |
Memory sizing |
VirtualMachine.Config.CPUCount |
vCPU sizing |
VirtualMachine.Provisioning.DeployTemplate / .CloneTemplate |
When clone is used |
VirtualMachine.Provisioning.Customize (and ModifyCustSpecs) |
When customize / customization_spec is used |
Datastore.AllocateSpace |
On each target datastore |
Network.Assign |
On each NIC's network / port group |
Resource.AssignVMToPool |
On the target resource pool |
| (tag + custom-attribute set privileges) | For the universal tail (tags / custom_attributes) |
The vCenter built-in Administrator account must not be used β apply principle of least privilege.
- vCenter Server 7.0 or later (provider
~> 2.0, validated against v2.16.1). - License: vSphere Standard or higher. Some features (vTPM, hardware-version ceilings) require newer ESXi / licensing.
- The resource pool (required), datastore, network/port group, and any clone template must exist before apply β the module consumes them by MOID.
- For clone + customize: VMware Tools / Perl (Linux) or sysprep (Windows) support on the source template.
- For vTPM: a configured key provider in vCenter, plus
firmware = "efi".
The provider block and all
datalookups belong to the caller β the module consumes every parent by MOID. Credentials come from environment variables (VSPHERE_SERVER/VSPHERE_USER/VSPHERE_PASSWORD), never module variables. Plan-only β a human reviews and applies.
provider "vsphere" {
allow_unverified_ssl = false # production: validate the vCenter certificate
}
data "vsphere_datacenter" "dc" { name = "dc-primary" }
data "vsphere_compute_cluster" "cluster" {
name = "cluster-prod-01"
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_datastore" "ds" {
name = "ds-prod-01"
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_network" "app" {
name = "pg-app-tier"
datacenter_id = data.vsphere_datacenter.dc.id
}
module "app_vm" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-app-01"
resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id # REQUIRED
datastore_id = data.vsphere_datastore.ds.id
guest_id = "rhel9_64Guest" # immutable β set correctly at create
num_cpus = 2
memory = 4096
# Disks are keyed by label. Secure default: thick lazy-zeroed (both flags false).
disks = {
"disk0" = { size = 60, unit_number = 0 } # boot disk
}
network_interfaces = {
"00-app" = { network_id = data.vsphere_network.app.id }
}
}One row per Emits entry from SCOPE.md β how this module's outputs feed downstream siblings:
| Output | Type | Wire into |
|---|---|---|
id |
string (MOID) |
entity_id for tf-mod-vsphere-entity-permissions; cross-references |
name |
string |
Logging, tagging, human-facing references |
uuid |
string (computed) |
tf-mod-vsphere-virtual-machine-snapshot.virtual_machine_uuid; tf-mod-vsphere-vapp entity target_id |
moid |
string (computed) |
Inventory cross-reference / vCenter MO lookups |
default_ip_address |
string (computed) |
DNS registration, downstream provisioners (null until VMware Tools reports) |
guest_ip_addresses |
list(string) (computed) |
Monitoring, logging (empty until VMware Tools reports) |
Composite by complexity, not by resource count. There is exactly one resource β vsphere_virtual_machine.this. Disks, NICs, CD-ROMs, clone/customize, OVF deploy, vTPM, and video card are provider nested blocks rendered via dynamic, not separate child resources. "Type is the contract" applies most strongly here.
Parent consumed by ID β the module boundary. Every parent object (resource_pool_id, datastore_id, datastore_cluster_id, datacenter_id, host_system_id, folder, each NIC's network_id, clone.template_uuid, clone.customization_spec.id, storage_policy_id) is a pre-resolved MOID passed by the caller. The module runs no data "vsphere_*" lookups. This keeps it composable and free of ambient vCenter state.
Immutable / force-new fields β set correctly at create:
| Field | Behavior |
|---|---|
guest_id |
Immutable β changing the guest OS type forces destroy/recreate. |
firmware (bios/efi) |
Effectively force-new. EFI is required for efi_secure_boot_enabled and vtpm. |
efi_secure_boot_enabled |
Effectively force-new; requires firmware = "efi". |
hardware_version |
Can be raised but not downgraded. |
disk.thin_provisioned / disk.eagerly_scrub |
Provisioning type is fixed at create β cannot be changed afterward. |
disk label / unit_number |
Disks are keyed by label; changing a disk's label or unit_number can detach/recreate that disk. |
replace_trigger |
Changing the string deliberately forces a full VM replace. |
Ordering constraints. NIC device order follows the sorted map-key order of var.network_interfaces β name keys so the sort order matches the intended order (e.g. "00-mgmt", "01-app"). When clone.customize.network_interface is used, its positional list must align with that same sorted order. Disk order is governed by unit_number, not map order.
Mutually exclusive inputs (enforced by validation {}):
datastore_idxordatastore_cluster_id(set at most one).clone.customization_specxorclone.customize(at most one).clone.customize.linux_optionsxorwindows_options(at most one).ovf_deploy.local_ovf_pathxorremote_ovf_url(exactly one).- Per CD-ROM:
client_device = truexorpath(an ISO). - Per disk: not both
thin_provisionedandeagerly_scrub.
Computed-after-create attributes. uuid, moid, default_ip_address, and guest_ip_addresses are populated by the provider after create. The IP attributes stay null/empty until VMware Tools reports β they are outputs, never inputs.
vSphere-specific gotcha. When wiring a distributed port group, use the DVS uuid (= the DVS id) path for network_id. Custom-attribute and extra_config values land in plan output and state in clear text β never place secrets there.
1 Β· Basic blank VM (minimal)
module "vm_basic" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-blank-01"
resource_pool_id = var.resource_pool_id # REQUIRED
datastore_id = var.datastore_id
guest_id = "otherGuest64"
num_cpus = 2
memory = 4096
disks = {
"disk0" = { size = 40, unit_number = 0 } # at least one disk required (unless ovf_deploy)
}
network_interfaces = {
"00-net" = { network_id = var.network_id }
}
}2 Β· With tags and custom attributes (the universal tail)
module "vm_tagged" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-tagged-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = "rhel9_64Guest"
num_cpus = 2
memory = 4096
disks = { "disk0" = { size = 60, unit_number = 0 } }
network_interfaces = { "00-app" = { network_id = var.network_id } }
# tags = list of vsphere_tag MOIDs; custom_attributes = map of attribute MOID -> value.
tags = [
module.tf_mod_vsphere_tag.tag_ids["environment"],
module.tf_mod_vsphere_tag.tag_ids["cost-center"],
]
custom_attributes = {
(data.vsphere_custom_attribute.owner.id) = "platform-team"
}
}3 Β· CPU/memory sizing with hot-add
module "vm_sized" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-db-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = "rhel9_64Guest"
num_cpus = 8
num_cores_per_socket = 4 # num_cpus must be divisible by this
memory = 32768
cpu_hot_add_enabled = true
memory_hot_add_enabled = true
disks = { "disk0" = { size = 100, unit_number = 0 } }
network_interfaces = { "00-db" = { network_id = var.network_id } }
}4 Β· Multiple disks β provisioning types and per-disk storage policy
module "vm_disks" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-storage-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = "rhel9_64Guest"
num_cpus = 4
memory = 8192
disks = {
"disk0" = { size = 80, unit_number = 0 } # boot: thick lazy-zeroed (secure default)
"data1" = { size = 500, unit_number = 1, thin_provisioned = true } # thin
"logs2" = { size = 200, unit_number = 2, eagerly_scrub = true } # thick eager-zeroed
"fast3" = { # per-disk storage policy
size = 300
unit_number = 3
storage_policy_id = module.tf_mod_vsphere_vm_storage_policy.id
}
}
# Each disk needs a unique (controller_type, unit_number) pair; a disk cannot be both
# thin_provisioned and eagerly_scrub.
network_interfaces = { "00-app" = { network_id = var.network_id } }
}5 Β· Multiple NICs β device ordering and a static MAC
module "vm_multi_nic" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-edge-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = "rhel9_64Guest"
num_cpus = 4
memory = 8192
disks = { "disk0" = { size = 60, unit_number = 0 } }
# Device order follows SORTED MAP-KEY order β prefix keys to pin it.
network_interfaces = {
"00-mgmt" = { network_id = var.mgmt_network_id, adapter_type = "vmxnet3" }
"01-app" = { network_id = var.app_network_id }
"02-dmz" = {
network_id = var.dmz_network_id
use_static_mac = true
mac_address = "00:50:56:01:02:03" # required when use_static_mac = true
}
}
}6 Β· CD-ROMs β ISO from a datastore and a client device
module "vm_cdrom" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-install-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = "ubuntu64Guest"
num_cpus = 2
memory = 4096
disks = { "disk0" = { size = 40, unit_number = 0 } }
network_interfaces = { "00-net" = { network_id = var.network_id } }
# Each cdrom is EITHER an ISO (datastore_id + path) OR a client device β never both. Max 2.
cdroms = {
"iso0" = { datastore_id = var.iso_datastore_id, path = "isos/ubuntu-24.04.iso" }
"client" = { client_device = true }
}
}7 Β· Clone from a template + inline Linux customization
# Caller resolves the template UUID:
data "vsphere_virtual_machine" "template" {
name = "tmpl-rhel9"
datacenter_id = var.datacenter_id
}
module "vm_clone_linux" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-web-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = data.vsphere_virtual_machine.template.guest_id # match template (immutable)
disks = { "disk0" = { size = 100, unit_number = 0 } }
network_interfaces = { "00-app" = { network_id = var.network_id } }
clone = {
template_uuid = data.vsphere_virtual_machine.template.id
customize = {
linux_options = { host_name = "web01", domain = "example.internal" }
# Positional β one entry per NIC, aligned to the sorted network_interfaces key order.
network_interface = [{ ipv4_address = "10.20.0.10", ipv4_netmask = 24 }]
ipv4_gateway = "10.20.0.1"
dns_server_list = ["10.20.0.2", "10.20.0.3"]
}
}
}8 Β· Clone + inline Windows customization (secrets out of band)
variable "win_admin_password" {
type = string
sensitive = true # provision out of band β never hard-code
}
module "vm_clone_windows" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-app-win-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = "windows2022srvNext_64Guest"
firmware = "efi"
disks = { "disk0" = { size = 90, unit_number = 0 } }
network_interfaces = { "00-app" = { network_id = var.network_id } }
clone = {
template_uuid = var.windows_template_uuid
customize = {
windows_options = {
computer_name = "APPWIN01"
admin_password = var.win_admin_password # redacted in plan output via sensitive
workgroup = "WORKGROUP"
time_zone = 35
}
network_interface = [{ ipv4_address = "10.30.0.20", ipv4_netmask = 24 }]
ipv4_gateway = "10.30.0.1"
}
}
}9 Β· Clone + saved customization spec (preferred) / linked clone
module "vm_clone_spec" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-batch-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = "rhel9_64Guest"
disks = { "disk0" = { size = 60, unit_number = 0 } }
network_interfaces = { "00-app" = { network_id = var.network_id } }
clone = {
template_uuid = var.template_uuid
linked_clone = true # fast, space-efficient; the source must be a snapshot-backed template
# Reference a saved spec from tf-mod-vsphere-guest-os-customization (preferred over inline).
# NOTE: set AT MOST ONE of customization_spec or customize.
customization_spec = { id = module.tf_mod_vsphere_guest_os_customization.id }
}
}10 Β· Deploy from OVF/OVA + vApp properties
module "vm_ovf" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-appliance-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
datacenter_id = var.datacenter_id # REQUIRED for OVF/OVA deploy
# When ovf_deploy is set, disks/guest_id come from the appliance template.
network_interfaces = { "00-net" = { network_id = var.network_id } }
ovf_deploy = {
remote_ovf_url = "https://content.example.internal/appliance.ova"
disk_provisioning = "thin"
ovf_network_map = { "VM Network" = var.network_id }
# allow_unverified_ssl_cert defaults false β only relax for trusted internal hosts.
}
vapp_properties = {
"guestinfo.hostname" = "appliance01"
# Avoid secrets here β vApp/extra_config values are stored in state in clear text.
}
}11 Β· Storage-DRS placement (datastore cluster)
module "vm_sdrs" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-sdrs-01"
resource_pool_id = var.resource_pool_id
# Mutually exclusive with datastore_id. Requires Storage DRS enabled on the cluster.
datastore_cluster_id = module.tf_mod_vsphere_datastore_cluster.cluster_id
guest_id = "rhel9_64Guest"
num_cpus = 4
memory = 8192
disks = { "disk0" = { size = 80, unit_number = 0 } } # no per-disk datastore_id under SDRS
network_interfaces = { "00-app" = { network_id = var.network_id } }
}12 Β· Secure-boot EFI VM with a virtual TPM
module "vm_vtpm" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-secure-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = "windows2022srvNext_64Guest"
firmware = "efi" # required for secure boot + vTPM (force-new)
efi_secure_boot_enabled = true
disks = { "disk0" = { size = 90, unit_number = 0 } }
network_interfaces = { "00-app" = { network_id = var.network_id } }
vtpm = { version = "2.0" } # requires a configured key provider in vCenter
}13 Β· Resource allocation, latency tuning, advanced toggles, and a video card
module "vm_tuned" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-latency-01"
resource_pool_id = var.resource_pool_id
datastore_id = var.datastore_id
guest_id = "rhel9_64Guest"
num_cpus = 8
memory = 16384
# Reservations / limits / shares.
cpu_reservation = 4000 # MHz guaranteed
memory_reservation = 16384
memory_reservation_locked_to_max = true
cpu_share_level = "high"
latency_sensitivity = "high" # requires full memory reservation
# Advanced toggles (e.g. enable_disk_uuid for Kubernetes CSI / clustering).
enable_disk_uuid = true
nested_hv_enabled = true
disks = { "disk0" = { size = 80, unit_number = 0 } }
network_interfaces = { "00-app" = { network_id = var.network_id } }
video_card = {
num_displays = 2
total_video_memory = 16384 # KB
graphics_3d = { renderer = "automatic", memory = 262144 }
}
}14 Β· End-to-end composition β wiring sibling module outputs (mandatory)
# Provider + parent lookups belong to the caller. Plan-only β a human applies after review.
provider "vsphere" {
allow_unverified_ssl = false
}
data "vsphere_datacenter" "dc" { name = "dc-primary" }
data "vsphere_datastore" "ds" {
name = "ds-prod-01"
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_virtual_machine" "template" {
name = "tmpl-rhel9"
datacenter_id = data.vsphere_datacenter.dc.id
}
# Tags
module "tf_mod_vsphere_tag" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-tag?ref=v1.0.0"
#... category + tags...
}
# Resource pool
module "tf_mod_vsphere_resource_pool" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-resource-pool?ref=v1.0.0"
#... parent_resource_pool_id, name, reservations...
}
# Port group (DVS)
module "tf_mod_vsphere_distributed_port_group" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-distributed-port-group?ref=v1.0.0"
#... dvs_uuid, vlan_id...
}
# Guest OS customization spec
module "tf_mod_vsphere_guest_os_customization" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-guest-os-customization?ref=v1.0.0"
#... linux_options...
}
# Storage policy
module "tf_mod_vsphere_vm_storage_policy" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-vm-storage-policy?ref=v1.0.0"
#... tag-based rule set...
}
# The VM β every parent arrives as a sibling output (MOID), never a lookup inside this module.
module "tf_mod_vsphere_virtual_machine" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine?ref=v1.0.0"
name = "vm-app-prod-01"
resource_pool_id = module.tf_mod_vsphere_resource_pool.id
datastore_id = data.vsphere_datastore.ds.id
storage_policy_id = module.tf_mod_vsphere_vm_storage_policy.id
guest_id = data.vsphere_virtual_machine.template.guest_id
num_cpus = 4
memory = 8192
disks = {
"disk0" = { size = 100, unit_number = 0 }
"data1" = { size = 500, unit_number = 1, storage_policy_id = module.tf_mod_vsphere_vm_storage_policy.id }
}
network_interfaces = {
"00-app" = { network_id = module.tf_mod_vsphere_distributed_port_group.id }
}
clone = {
template_uuid = data.vsphere_virtual_machine.template.id
customization_spec = { id = module.tf_mod_vsphere_guest_os_customization.id }
}
tags = [module.tf_mod_vsphere_tag.tag_ids["environment"]]
}
# Downstream: snapshot consumes the VM's uuid.
module "tf_mod_vsphere_virtual_machine_snapshot" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-virtual-machine-snapshot?ref=v1.0.0"
virtual_machine_uuid = module.tf_mod_vsphere_virtual_machine.uuid
snapshot_name = "baseline"
}Required: name, resource_pool_id. Everything else is optional with a baked-in default. A field defaulting to null defers to the provider's own default (shown in parentheses).
| Name | Type | Default | Description |
|---|---|---|---|
name |
string |
β (required) | VM display name (β€80 chars). |
resource_pool_id |
string |
β (required) | MOID of the resource pool. Consumed by ID. |
datacenter_id |
string |
null |
Datacenter MOID. Required only for OVF/OVA deploy. |
datastore_id |
string |
null |
Datastore MOID for VM home. Set exactly one of datastore_id / datastore_cluster_id. |
datastore_cluster_id |
string |
null |
Datastore-cluster MOID for SDRS placement. Mutually exclusive with datastore_id. |
host_system_id |
string |
null |
Specific ESXi host MOID. null = let DRS choose. |
folder |
string |
null |
VM inventory folder path. null = datacenter vm root. |
storage_policy_id |
string |
null |
Storage-policy MOID/UUID for the VM home. |
| Name | Type | Default | Description |
|---|---|---|---|
guest_id |
string |
null (otherGuest64) |
Guest OS identifier. Immutable β force-new. |
alternate_guest_name |
string |
null |
Full guest name when guest_id is otherGuest/otherGuest64. |
annotation |
string |
null |
Free-form notes shown in vCenter. |
firmware |
string |
null (bios) |
bios | efi. Effectively force-new. |
efi_secure_boot_enabled |
bool |
null (false) |
UEFI Secure Boot. Requires firmware = "efi". Force-new. |
hardware_version |
number |
null |
VMX version (e.g. 19β21). Cannot be downgraded. |
| Name | Type | Default | Description |
|---|---|---|---|
num_cpus |
number |
null (1) |
Total vCPUs. |
num_cores_per_socket |
number |
null (1) |
Cores per socket. num_cpus must be divisible by this. |
num_cores_per_numa_node |
number |
null |
Cores per vNUMA node. |
memory |
number |
null (1024) |
Memory in MB. |
cpu_hot_add_enabled |
bool |
null (false) |
Allow adding vCPUs while powered on. |
cpu_hot_remove_enabled |
bool |
null (false) |
Allow removing vCPUs while powered on. |
memory_hot_add_enabled |
bool |
null (false) |
Allow adding memory while powered on. |
| Name | Type | Default | Description |
|---|---|---|---|
cpu_reservation |
number |
null (0) |
Guaranteed CPU (MHz). |
cpu_limit |
number |
null (-1) |
CPU limit (MHz). -1 = unlimited. |
cpu_share_level |
string |
null (normal) |
low | normal | high | custom. |
cpu_share_count |
number |
null |
Explicit share count (when custom). |
memory_reservation |
number |
null (0) |
Guaranteed memory (MB). |
memory_reservation_locked_to_max |
bool |
null (false) |
Lock reservation to max (no balloon/swap). |
memory_limit |
number |
null (-1) |
Memory limit (MB). -1 = unlimited. |
memory_share_level |
string |
null (normal) |
low | normal | high | custom. |
memory_share_count |
number |
null |
Explicit share count (when custom). |
latency_sensitivity |
string |
null (normal) |
low | normal | medium | high | custom. high needs full memory reservation. |
| Name | Type | Default | Description |
|---|---|---|---|
scsi_type |
string |
null (pvscsi) |
lsilogic | lsilogic-sas | pvscsi | buslogic. |
scsi_controller_count |
number |
null (1) |
Number of SCSI controllers (1β4). |
scsi_bus_sharing |
string |
null (noSharing) |
physicalSharing | virtualSharing | noSharing. |
sata_controller_count |
number |
null (0) |
Number of SATA controllers. |
ide_controller_count |
number |
null (2) |
Number of IDE controllers (0β2). |
nvme_controller_count |
number |
null (0) |
Number of NVMe controllers. |
| Name | Type | Default | Description |
|---|---|---|---|
boot_delay |
number |
null (0) |
Delay (ms) before boot firmware starts. |
boot_retry_enabled |
bool |
null (false) |
Retry boot after boot_retry_delay. |
boot_retry_delay |
number |
null (10000) |
Delay (ms) before a boot retry. |
| Name | Type | Default | Description |
|---|---|---|---|
force_power_off |
bool |
false π |
Secure default (overrides provider true). false = a stuck guest fails the run rather than being hard powered off. |
shutdown_wait_timeout |
number |
3 |
Minutes to wait for graceful shutdown (1β60). Raise deliberately. |
migrate_wait_timeout |
number |
null (30) |
Minutes to wait for vMotion. |
poweron_timeout |
number |
null (300) |
Seconds to wait for power-on. |
wait_for_guest_net_timeout |
number |
null (5) |
Minutes to wait for a guest IP. 0 disables. |
wait_for_guest_net_routable |
bool |
null (true) |
Require the guest IP to be routable. |
wait_for_guest_ip_timeout |
number |
null (0) |
Legacy IP waiter (minutes). 0 disables. |
ignored_guest_ips |
list(string) |
null |
IPs/CIDRs to ignore for default_ip_address. |
run_tools_scripts_after_power_on |
bool |
false π |
Secure default (overrides provider true). |
run_tools_scripts_after_resume |
bool |
null (true) |
Run Tools scripts after resume. |
run_tools_scripts_before_guest_reboot |
bool |
null (false) |
Run Tools scripts before reboot. |
run_tools_scripts_before_guest_shutdown |
bool |
null (true) |
Run Tools scripts before shutdown. |
run_tools_scripts_before_guest_standby |
bool |
null (true) |
Run Tools scripts before standby. |
tools_upgrade_policy |
string |
null (manual) |
manual | upgradeAtPowerCycle. |
sync_time_with_host |
bool |
null (false) |
Sync guest time with host. |
sync_time_with_host_periodically |
bool |
null (false) |
Periodically sync guest time. |
| Name | Type | Default | Description |
|---|---|---|---|
nested_hv_enabled |
bool |
null (false) |
Expose hardware-assisted virtualization to the guest. |
cpu_performance_counters_enabled |
bool |
null (false) |
Expose CPU performance counters. |
enable_disk_uuid |
bool |
null (false) |
Expose consistent disk UUIDs (k8s CSI / clustering). |
enable_logging |
bool |
null (true) |
Enable VM logging to the VM directory. |
ept_rvi_mode |
string |
null (automatic) |
automatic | on | off. |
hv_mode |
string |
null (hvAuto) |
hvAuto | hvOn | hvOff. |
swap_placement_policy |
string |
null (inherit) |
inherit | hostLocal | vmDirectory. |
vbs_enabled |
bool |
null (false) |
Virtualization-Based Security. Requires EFI + secure boot + nested HV. |
vvtd_enabled |
bool |
null (false) |
I/O MMU (VT-d / AMD-Vi) virtualization. |
pci_device_id |
set(string) |
[] |
PCI passthrough device IDs. |
extra_config |
map(string) |
{} |
Advanced VMX/guestinfo keys. Clear text in state β no secrets. Not for OVF templates. |
extra_config_reboot_required |
bool |
null (true) |
Whether an extra_config change triggers a reboot. |
replace_trigger |
string |
null |
Changing the string forces a full VM replace. |
| Name | Type | Default | Description |
|---|---|---|---|
disks |
map(object) |
{} |
Disk label β config. β₯1 required unless ovf_deploy; max 60. See schema below. |
network_interfaces |
map(object) |
{} |
NIC label β config. Device order = sorted key order; max 20. See schema below. |
cdroms |
map(object) |
{} |
CD-ROM label β config. Max 2; ISO xor client device. See schema below. |
clone |
object |
null |
Clone + customize. Omit for blank/OVF. See schema below. |
ovf_deploy |
object |
null |
OVF/OVA deploy. Requires datacenter_id. See schema below. |
vapp_properties |
map(string) |
null |
vApp property key β value (OVF/cloud-init). Clear text in state. |
vtpm |
object({ version = optional(string) }) |
null |
Virtual TPM 2.0. Requires firmware = "efi" + key provider. |
video_card |
object |
null |
Video card / 3D settings. See schema below. |
disks β map(object) keyed by label
disks = map(object({
size = optional(number, null) # GiB; required unless attach = true
unit_number = optional(number, 0) # unique per controller; boot disk = 0
thin_provisioned = optional(bool, false) # π secure default thick; true = thin
eagerly_scrub = optional(bool, false) # π secure default lazy zero; true = eager
keep_on_remove = optional(bool, false)
controller_type = optional(string, null) # scsi | sata | nvme | ide
datastore_id = optional(string, null)
storage_policy_id = optional(string, null)
disk_mode = optional(string, null)
disk_sharing = optional(string, null) # sharingNone | sharingMultiWriter
write_through = optional(bool, null)
io_limit = optional(number, null)
io_reservation = optional(number, null)
io_share_level = optional(string, null) # low | normal | high | custom
io_share_count = optional(number, null)
path = optional(string, null)
attach = optional(bool, false)
}))Validations: β€60 disks Β· each disk sets size unless attach Β· unique (controller_type, unit_number) Β· not both thin_provisioned and eagerly_scrub.
network_interfaces β map(object) keyed by NIC label
network_interfaces = map(object({
network_id = string # MOID of port group / network (required)
adapter_type = optional(string, null) # e1000 | e1000e | vmxnet2 | vmxnet3 | vmxnet3vrdma | sriov
use_static_mac = optional(bool, false)
mac_address = optional(string, null) # required when use_static_mac = true
bandwidth_limit = optional(number, null)
bandwidth_reservation = optional(number, null)
bandwidth_share_level = optional(string, null) # low | normal | high | custom
bandwidth_share_count = optional(number, null)
ovf_mapping = optional(string, null)
physical_function = optional(string, null)
external_port_id = optional(string, null)
}))Device order follows the sorted map-key order; max 20.
cdroms β map(object) (max 2)
cdroms = map(object({
client_device = optional(bool, false) # EITHER this true...
datastore_id = optional(string, null)
path = optional(string, null) #... OR an ISO path (requires datastore_id)
}))clone β object (omit for a blank or OVF VM)
clone = object({
template_uuid = string # UUID of source template (required)
linked_clone = optional(bool, false)
timeout = optional(number, null)
ovf_network_map = optional(map(string), null)
ovf_storage_map = optional(map(string), null)
# Set AT MOST ONE of customization_spec or customize:
customization_spec = optional(object({
id = string
timeout = optional(number, null)
}), null)
customize = optional(object({
timeout = optional(number, null)
dns_server_list = optional(list(string), null)
dns_suffix_list = optional(list(string), null)
ipv4_gateway = optional(string, null)
ipv6_gateway = optional(string, null)
windows_sysprep_text = optional(string, null)
# Set AT MOST ONE of linux_options or windows_options:
linux_options = optional(object({
host_name = string
domain = string
hw_clock_utc = optional(bool, null)
script_text = optional(string, null)
time_zone = optional(string, null)
}), null)
windows_options = optional(object({
computer_name = string
admin_password = optional(string, null) # π sensitive β redacted in plan
workgroup = optional(string, null)
join_domain = optional(string, null)
domain_admin_user = optional(string, null)
domain_admin_password = optional(string, null) # π sensitive β redacted in plan
domain_ou = optional(string, null)
full_name = optional(string, null)
organization_name = optional(string, null)
product_key = optional(string, null)
run_once_command_list = optional(list(string), null)
auto_logon = optional(bool, null)
auto_logon_count = optional(number, null)
time_zone = optional(number, null)
}), null)
# Positional list β aligns to the sorted network_interfaces key order.
network_interface = optional(list(object({
ipv4_address = optional(string, null)
ipv4_netmask = optional(number, null)
ipv6_address = optional(string, null)
ipv6_netmask = optional(number, null)
dns_server_list = optional(list(string), null)
dns_domain = optional(string, null)
})), [])
}), null)
})ovf_deploy / video_card β object
ovf_deploy = object({
local_ovf_path = optional(string, null) # set EXACTLY ONE of local_ovf_path
remote_ovf_url = optional(string, null) #... or remote_ovf_url
ip_allocation_policy = optional(string, null)
ip_protocol = optional(string, null)
disk_provisioning = optional(string, null)
deployment_option = optional(string, null)
ovf_network_map = optional(map(string), null)
allow_unverified_ssl_cert = optional(bool, false) # π secure default false
enable_hidden_properties = optional(bool, null)
})
video_card = object({
num_displays = number
total_video_memory = number # KB
graphics_3d = optional(object({
renderer = optional(string, null) # automatic | software | hardware
memory = optional(number, null)
}), null)
})| Name | Type | Default | Description |
|---|---|---|---|
tags |
list(string) |
[] |
List of vsphere_tag MOIDs. From tf-mod-vsphere-tag.tag_ids[*]. |
custom_attributes |
map(string) |
{} |
Map of custom-attribute MOID β value string. |
| Output | Type | Sensitive | Description |
|---|---|---|---|
id |
string (MOID) |
no | Primary. MOID of the VM (e.g. vm-1234). entity_id for permissions; cross-references. |
name |
string |
no | Display name. |
uuid |
string (computed) |
no | BIOS UUID β snapshot virtual_machine_uuid, vApp entity target_id. |
moid |
string (computed) |
no | Managed object ID β inventory cross-reference. |
default_ip_address |
string (computed) |
no | Primary IP from VMware Tools. Null until Tools reports. |
guest_ip_addresses |
list(string) (computed) |
no | All guest IPs from VMware Tools. Empty until Tools reports. |
No secret is emitted. Windows customization passwords are inputs only (wrapped with
sensitiveinmain.tf) β never outputs (org policy: secrets out of band).
Every secure default and its explicit opt-out, per this module suite's secure-by-default convention. The empty call produces the safe resource; the caller types extra characters to take a risk.
| Setting | Secure default | Risky opt-out | Effect |
|---|---|---|---|
disks[*].thin_provisioned |
false (thick lazy-zeroed) |
true (thin) |
Thick avoids overcommit surprises on regulated storage. |
disks[*].eagerly_scrub |
false (lazy zero) |
true (eager zero) |
Eager zero is slower and only needed for some clustering. |
force_power_off |
false |
true |
false makes a stuck guest fail the run rather than be hard powered off. |
run_tools_scripts_after_power_on |
false |
true |
No surprise script execution after power-on. |
shutdown_wait_timeout |
3 minutes |
higher value | Bounded graceful-shutdown wait; raise deliberately for slow guests. |
ovf_deploy.allow_unverified_ssl_cert |
false |
true |
OVF/OVA source certificate is validated by default. |
clone.customize.windows_options.*_password |
sensitive-wrapped |
β | Passwords are redacted in plan and never output. |
tags / custom_attributes |
[] / {} |
β | Caller applies governance explicitly. |
Plan-only. No
terraform applyis ever run by this module system β a human reviews the plan and applies after approval. Pin the source by tag (?ref=v1.0.0), never a branch.
# 1. Initialize without a backend (static analysis only).
terraform init -backend=false
# 2. Validate types, schemas, and cross-field validations.
terraform validate
# 3. Check formatting (or run `terraform fmt` and review the diff).
terraform fmt -check
# 4. Produce a plan for human review (requires caller-configured provider auth).
# export VSPHERE_SERVER / VSPHERE_USER / VSPHERE_PASSWORD first.
terraform plan
# 5. A human reviews the plan and performs the apply out of band β NEVER from this module system.| Symptom | Likely cause | Resolution |
|---|---|---|
| VM is destroyed/recreated on a trivial change | A force-new field changed: guest_id, firmware, efi_secure_boot_enabled, a disk label/unit_number, or replace_trigger. |
Treat these as immutable. Set them correctly at create; review the plan's # forces replacement lines before approving. |
| Plan errors: "Set at most one of datastore_id or datastore_cluster_id" | Both placement inputs set. | Choose one: a single datastore (datastore_id) or SDRS placement (datastore_cluster_id). |
| Plan errors: "Each disk needs a unique (controller_type, unit_number) pair" | Two disks collide on the same controller slot. | Give each disk a distinct unit_number (boot disk = scsi:0); set controller_type where mixing controllers. |
| Apply hangs, then fails near power-off | Guest didn't shut down within shutdown_wait_timeout and force_power_off = false (secure default). |
Fix guest shutdown, raise shutdown_wait_timeout, or set force_power_off = true deliberately. |
default_ip_address / guest_ip_addresses are empty |
VMware Tools not running, VM powered off, or the IP waiter timed out. | Ensure Tools is installed/running; tune wait_for_guest_net_timeout / wait_for_guest_net_routable; exclude noise via ignored_guest_ips. |
| Customization is skipped or fails | clone.customize set without Tools/Perl/sysprep support, or both customization_spec and customize set. |
Use a template with customization support; set at most one of customization_spec / customize; align customize.network_interface positionally to the NIC sort order. |
| vTPM / Secure Boot rejected | Missing firmware = "efi" or no key provider configured in vCenter. |
Set firmware = "efi" (force-new); configure a vCenter key provider before adding vtpm. |
| Wrong NIC came up on the wrong network | NIC device order follows sorted map-key order, not declaration order. | Prefix NIC keys to pin order ("00-mgmt", "01-app"); align customize.network_interface list to that order. |
SCOPE.mdβ the design contract for this module (in scope, consumed-by-ID boundary, gotchas, decisions).examples/main.tfβ a working, plan-only clone + Linux customization example.- Provider docs β
vmware/vsphereΒ·vsphere_virtual_machine. - Sibling modules β
tf-mod-vsphere-resource-pool,tf-mod-vsphere-compute-cluster,tf-mod-vsphere-distributed-port-group,tf-mod-vsphere-content-library,tf-mod-vsphere-guest-os-customization,tf-mod-vsphere-vm-storage-policy,tf-mod-vsphere-tag,tf-mod-vsphere-virtual-machine-snapshot,tf-mod-vsphere-vapp.
A virtual machine is just a contract between a pool, a datastore, and a network β write it once, review the plan, and let a human turn the key.