Terraform
Provisioning VMware Workstation VMs with Terraform, and the compatibility bugs in an open-source provider that had to be fixed first to get a real create-read-update-delete lifecycle working.
What This Set Out to Prove
Not just a clean terraform apply
The goal wasn’t just to get terraform apply to run without errors. It
was to confirm that the elsudano/vmworkstation Terraform provider could
actually perform a full virtual machine lifecycle, create, read, update, and delete,
against the real VMware Workstation REST API, not just a happy-path example.
Down the stack, not around it
Instead of working around errors at the Terraform configuration level, each problem was traced down through the provider, its Go API client, and finally direct REST calls to VMware itself, so the real fault could be isolated instead of guessed at.
Nothing trusted at face value
Every lifecycle step (create, refresh, update, delete) was checked twice: once through Terraform’s own output, and again with an independent VMware REST API call, so a successful terraform apply was never just taken on faith.
Three Bugs, Three Fixes
Real defects in the provider and its API client, not configuration mistakes
Three separate compatibility problems surfaced between the provider and VMware’s actual REST API behavior, each one reproduced independently before touching a line of Go.
A NAT interface VMware rejected
vmnet8 when creating a NAT interface. Reproducing the same request directly against VMware’s REST API returned Code 121, Redundant parameter: vmnet8 for this operation: VMware assigns the NAT vmnet automatically and rejects being told what it already knows.Vmnet JSON field omitempty and cleared it for NAT and bridged interfaces before the request went out (wsapinet/types.go, and the same fix in the MAC-renewal path in wsapinet/wsapinet_tools.go), then rebuilt the provider.A path Terraform was never supposed to set
path produced a Terraform inconsistent-result error right after creation.path as something the user should supply, so Terraform ended up comparing a value it was told to expect against a completely different one VMware actually returned.path from a required attribute to a computed one in the provider’s resource schema (internal/provider/resource_vm.go), so Terraform records VMware’s answer as state instead of expecting it as input.A refresh that erased good state
description or denomination fields. The provider treated their absence as an error, and in some paths overwrote Terraform’s existing state with the empty values VMware sent back, even though nothing was actually wrong.Read() and Update() functions, so Terraform’s existing state is preserved when VMware simply doesn’t return those fields.Full Lifecycle, Independently Verified
Create, refresh, update, delete, each one double-checked
With the provider patched and rebuilt, the same disposable VM was carried through a full
lifecycle using Terraform’s dev_overrides mechanism to test the patched
binary without publishing anything. Every step below was also re-checked with a direct
VMware REST API call, not shown here, before being called done.
$ terraform apply
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ terraform plan
No changes. Your infrastructure matches the configuration.
$ terraform apply # memory 2048 -> 3072
Plan: 0 to add, 1 to change, 0 to destroy.
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
$ terraform destroy
Destroy complete! Resources: 1 destroyed.
A Golden Image, Validated End to End
Re-running the lifecycle against the real source, not a stand-in
rhel10-vm-base (an Image Builder blueprint) → OVA → imported into
VMware Workstation as rhel10-vm-base-test → cloned by the patched
provider. With the three fixes above in place, a fresh clone of that same image went
through create, refresh, update, and refresh again, clean.
Same golden image, same patched provider
terraform-ova-test was cloned from rhel10-vm-base-test, the very VM every lifecycle test on this page clones from. Create succeeded, an immediate refresh showed no drift, and an in-place memory change (2048 to 3072 MB) applied cleanly and was independently confirmed against VMware’s REST API.
Destroy, deliberately held back
Create, refresh, update, and a final refresh all passed clean. Unlike the earlier lifecycle test, this VM’s destroy step was intentionally deferred to a separate test, so its evidence trail stays intact in the meantime.
Still Ahead
Provisioning is one piece of the pipeline
What’s next
Image Builder has actually been part of this from the start: rhel10-vm-base-test, the VM every lifecycle test on this page clones from, is itself a VMware import of an OVA built from Image Builder’s minimal rhel10-vm-base blueprint. What’s still ahead is the handoff to Foreman and Ansible once a VM is provisioned. See Overview for how this fits into the rest of the lab, and Foreman / Satellite for what’s coming next.
