Skip to content

microsoftexpert/tf-mod-vsphere-virtual-machine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

tf-mod-vsphere-virtual-machine

Terraform vmware/vsphere module type keystone resources plan-only

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.


Overview

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 apply is ever performed here: everything is plan-only until a human reviews and applies.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


Module Structure

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).

Family DAG β€” where this fits

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
Loading

Resource shape β€” what this builds

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
Loading

πŸ”‘ Required vCenter Privileges

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.


vSphere Prerequisites

  • 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".

Quick Start

The provider block and all data lookups 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 }
  }
}

Typical wiring

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)

Architecture Notes

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_id xor datastore_cluster_id (set at most one).
  • clone.customization_spec xor clone.customize (at most one).
  • clone.customize.linux_options xor windows_options (at most one).
  • ovf_deploy.local_ovf_path xor remote_ovf_url (exactly one).
  • Per CD-ROM: client_device = true xor path (an ISO).
  • Per disk: not both thin_provisioned and eagerly_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.


Example library

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"
}

Inputs

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).

Core / placement

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.

Guest OS / identity

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.

Compute sizing

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.

Resource allocation

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.

Storage controllers

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.

Boot options

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.

Power & VMware Tools

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.

Advanced / virtualization toggles

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.

Nested-block collections

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)
})

Universal tail

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.

Outputs

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 sensitive in main.tf) β€” never outputs (org policy: secrets out of band).


Design Principles

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.

Runbook

Plan-only. No terraform apply is 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.

Troubleshooting

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.

Related Docs

  • 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.

Packages

 
 
 

Contributors

Languages