Australian cloud for business software — built and run in Sydney since 2003

NetLeverage
NetLeverage

How to automate VMware provisioning with Terraform and PowerCLI

Guide
CloudVMware

Use Terraform to declare the VM's intended configuration and review its proposed changes. Use PowerCLI and an application check to verify the result. A successful apply confirms that the provider completed its operations; it does not prove that a useful service is running.

This guide provisions one disposable Linux VM from an existing template. It contains a complete small configuration, not a production landing zone. No Terraform initialisation, plan, apply or infrastructure connection was executed for this article. The reader must validate it against the installed CLI and approved lab before reuse.

1. Define a narrow template contract

Prepare a supported Linux template with working VMware Tools/open-vm-tools and guest-customisation support. This example deliberately requires BIOS firmware, one SCSI disk at unit 0, one supported network adapter, no vTPM, and no shared or independent disks. EFI, Secure Boot, multi-disk and encrypted templates require an expanded configuration and separate testing.

Use a non-production folder, dedicated resource pool, approved datastore and uniquely named lab port group with DHCP. Confirm that the pool's hosts can access the chosen datastore and network. Provision the folder and template separately. All names below are fictional and must be replaced with reviewed inventory paths.

Cloning requires vCenter rather than an ESXi-only connection. Template disk size and device order matter; an arbitrary smaller destination disk is not a valid clone. Provider 2.12.0 VM resource reference: https://raw.githubusercontent.com/vmware/terraform-provider-vsphere/v2.12.0/docs/resources/virtual_machine.md

2. Protect authentication and state

Use an approved secret-injection mechanism to populate VSPHERE_SERVER, VSPHERE_USER and VSPHERE_PASSWORD in the Terraform process environment. Do not write real values in HCL, a shell history, a shared terminal transcript or the article. Keep TLS verification enabled and fix trust problems in the runner's trust store. Pinned provider configuration: https://raw.githubusercontent.com/vmware/terraform-provider-vsphere/v2.12.0/docs/index.md

The example uses local state because no backend is declared. Use it only in a protected, single-operator disposable lab directory. Before team use, select and configure a reviewed remote backend with access controls, encryption, state locking and recoverable versions. That backend is deliberately environment-specific.

Treat saved plans and state as confidential. Marking a variable sensitive redacts normal display; it does not encrypt the stored value. Avoid passing Windows administrator or domain-join passwords as resource arguments without an explicit secret/state design. Terraform sensitive-data guidance: https://developer.hashicorp.com/terraform/language/manage-sensitive-data

3. Create the lab configuration

Save the following as main.tf in an otherwise empty lab project. The template data source supplies firmware, controller, disk and adapter metadata; its documented attributes: https://raw.githubusercontent.com/vmware/terraform-provider-vsphere/v2.12.0/docs/data-sources/virtual_machine.md determine the clone configuration.

terraform {
  required_version = ">= 1.6.0, < 2.0.0"
  required_providers {
    vsphere = {
      source  = "vmware/vsphere"
      version = "2.12.0"
    }
  }
}

provider "vsphere" {
  allow_unverified_ssl = false
}

data "vsphere_datacenter" "lab" {
  name = "DC-LAB"
}

data "vsphere_resource_pool" "lab" {
  name          = "CL-LAB/Resources/LAB-AUTOMATION"
  datacenter_id = data.vsphere_datacenter.lab.id
}

data "vsphere_datastore" "lab" {
  name          = "DS-LAB"
  datacenter_id = data.vsphere_datacenter.lab.id
}

data "vsphere_network" "lab" {
  name          = "PG-LAB-DHCP"
  datacenter_id = data.vsphere_datacenter.lab.id
}

data "vsphere_virtual_machine" "template" {
  name          = "Templates/linux-single-disk"
  datacenter_id = data.vsphere_datacenter.lab.id
}

resource "vsphere_virtual_machine" "lab" {
  name             = "iac-lab-01"
  folder           = "Lab/Automation"
  resource_pool_id = data.vsphere_resource_pool.lab.id
  datastore_id     = data.vsphere_datastore.lab.id
  num_cpus         = 2
  memory           = 4096
  guest_id         = data.vsphere_virtual_machine.template.guest_id
  firmware         = data.vsphere_virtual_machine.template.firmware
  scsi_type        = data.vsphere_virtual_machine.template.scsi_type

  network_interface {
    network_id   = data.vsphere_network.lab.id
    adapter_type = data.vsphere_virtual_machine.template.network_interface_types[0]
  }

  disk {
    label            = "disk0"
    unit_number      = 0
    size             = data.vsphere_virtual_machine.template.disks[0].size
    thin_provisioned = data.vsphere_virtual_machine.template.disks[0].thin_provisioned
    eagerly_scrub    = data.vsphere_virtual_machine.template.disks[0].eagerly_scrub
  }

  clone {
    template_uuid = data.vsphere_virtual_machine.template.id
    customize {
      linux_options {
        host_name = "iac-lab-01"
        domain    = "lab.example.invalid"
      }
      network_interface {}
    }
  }

  lifecycle {
    prevent_destroy = true
    precondition {
      condition = (
        length(data.vsphere_virtual_machine.template.disks) == 1 &&
        length(data.vsphere_virtual_machine.template.network_interface_types) == 1 &&
        data.vsphere_virtual_machine.template.firmware == "bios" &&
        !data.vsphere_virtual_machine.template.vtpm &&
        data.vsphere_virtual_machine.template.disks[0].unit_number == 0 &&
        contains(["pvscsi", "lsilogic-sas"], data.vsphere_virtual_machine.template.scsi_type)
      )
      error_message = "Use the reviewed single-SCSI-disk, single-NIC BIOS template without vTPM."
    }
  }
}

The empty customisation network block requests DHCP. Replace the fictional DNS domain with the approved lab domain; DHCP must provide usable addressing and DNS. The illustrative two-vCPU/4-GiB allocation is not workload sizing advice.

The precondition catches several mismatches but is not a complete template audit. The data source's controller scan scope matters; inspect all template devices before using it. The resource-pool: https://raw.githubusercontent.com/vmware/terraform-provider-vsphere/v2.12.0/docs/data-sources/resource_pool.md, datastore: https://raw.githubusercontent.com/vmware/terraform-provider-vsphere/v2.12.0/docs/data-sources/datastore.md and network: https://raw.githubusercontent.com/vmware/terraform-provider-vsphere/v2.12.0/docs/data-sources/network.md lookups must resolve the intended objects. Duplicate network names require disambiguation, not selecting the first match.

4. Validate and review before applying

Check local CLI help and run each command only after the previous one succeeds:

terraform version
terraform fmt -check
terraform init
terraform validate
terraform plan -out=lab.tfplan
terraform show lab.tfplan

The version: https://developer.hashicorp.com/terraform/cli/commands/version and show: https://developer.hashicorp.com/terraform/cli/commands/show references describe CLI inventory and saved-plan inspection.

If formatting fails, review and format the local example, then rerun the check. Initialisation installs the pinned provider and creates the dependency lock file. Preserve that lock file with reviewed configuration. Validation checks configuration consistency, not guest readiness or successful permissions. init: https://developer.hashicorp.com/terraform/cli/commands/init, fmt: https://developer.hashicorp.com/terraform/cli/commands/fmt, validate: https://developer.hashicorp.com/terraform/cli/commands/validate

Review the plan's VM name, folder, network, pool, datastore and template identity. For this empty lab project, expect one managed VM creation and no replacement or deletion. Data-source reads are separate from managed resource additions. Stop on an unexpected change. Do not use -target to conceal a wider plan you do not understand. Plan reference: https://developer.hashicorp.com/terraform/cli/commands/plan

Only after approval of that exact plan, execute this mutating lab step:

terraform apply lab.tfplan

Applying a saved plan proceeds without the normal interactive approval prompt. Protect it and keep the review-to-execution interval short. Discard and regenerate it if inputs, permissions or environment conditions change. Apply reference: https://developer.hashicorp.com/terraform/cli/commands/apply

5. Verify inventory and guest readiness

In an approved PowerCLI session, perform read-only checks:

Get-Help Connect-VIServer -Full
Get-Help Get-VM -Full
$vc = Connect-VIServer -Server 'vc01.example.invalid'
$vm = @(Get-VM -Name 'iac-lab-01' -Server $vc)
if ($vm.Count -ne 1) { throw 'Expected one lab VM; inspect inventory identity' }
$vm[0] | Select-Object Name, PowerState, NumCpu, MemoryGB
$vm[0].Guest | Select-Object HostName, IPAddress

The Get-VM reference: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.core/commands/get-vm defines the scoped lookup. Guest information depends on VMware Tools and may be incomplete. Also confirm the VM's folder, backing network/datastore and customisation task in vCenter. Test the intended lab service from an authorised client. An IP address alone is not service acceptance.

Run a fresh terraform plan after successful verification. An unchanged configuration should propose no unintended resource changes. Investigate differences rather than adding broad ignore_changes rules. Subsequent plans require renewed review if another operator edits the VM.

6. Adapt Windows templates without copying secrets

Use a supported prepared Windows template and replace linux_options with windows_options containing an appropriate computer_name and, for a workgroup lab, workgroup. Windows customisation uses Sysprep and needs its own template validation. Do not include both Linux and Windows blocks.

The provider also exposes administrator and domain-join credential fields. Those are not safe merely because Terraform marks output sensitive. Design guest credential provisioning and domain enrolment separately, then lab-test login, identity and service readiness. This is adaptation guidance, not a complete Windows deployment recipe. Windows customisation schema: https://raw.githubusercontent.com/vmware/terraform-provider-vsphere/v2.12.0/docs/resources/virtual_machine.md

Failure handling and rollback

If clone succeeds but customisation times out, inspect the VM, Tools, guest logs, DHCP and provider state before retrying. A failed command can leave a real VM. Do not create duplicates by deleting state or changing names blindly.

Reverting configuration is only a proposal until its new plan has been reviewed. prevent_destroy adds protection while present in configuration, but it is not a backup or an absolute deletion barrier. Removing the resource block removes that protection, as described in the lifecycle reference: https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle. Do not use destroy as routine rollback. Retiring the disposable VM is a separately reviewed cleanup action with its state and identity reconciled.

Related topics: template lifecycle, VMware security and private-cloud capacity.

Learning notes

  • A pinned provider improves repeatability but does not validate the template.
  • State and saved plans need protection even when console output hides secrets.
  • Infrastructure creation and application readiness are separate acceptance checks.

Sources and review status

See the source ledger for dates, version scope and limits, the author self-check, and the series index. Independent review and lab validation remain pending.