Skybaer Labs logo
SKYBÆR LABS
Showcase Project

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.

Terraform VMware Workstation REST API RHEL Image Builder Go Open-Source Provider Debugging Infrastructure Lifecycle Validation

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

Problem
A VM created through the provider came up with no working network interface.
Root cause
The Go API client was explicitly sending 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.
Fix
Marked the client’s 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

Problem
Applying a config that specified a VM path produced a Terraform inconsistent-result error right after creation.
Root cause
VMware’s clone API doesn’t accept a destination filesystem path at all, just a name and a parent ID. VMware decides the resulting path itself and hands it back, but the provider’s schema treated 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.
Fix
Changed 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

Problem
Terraform refreshes were failing outright, or silently discarding configured values, over a description field.
Root cause
VMware’s VM GET response doesn’t reliably return 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.
Fix
Removed the empty-field error and overwrite logic from the provider’s 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 / plan / destroy
Scroll sideways to see the full lines →
$ 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.