Terraform can validate its own input, but it cannot automatically prove that the outside world is clear.

Before applying a vSphere VM plan, run preflight checks against the systems that already know about names and addresses.

After testing a disposable VM, verify destroy cleanup as a separate lifecycle check. Preflight protects allocation. Destroy verification protects source-of-truth hygiene.

Generate Plan JSON

Use the plan as the preflight input:

terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

Then run the guardrail script:

./scripts/preflight-vm-guardrails.sh --plan-json tfplan.json

Inputs To Extract

From the plan JSON, extract each planned VM’s:

  • Terraform address.
  • VM name.
  • IPv4 address.
  • prefix length.
  • DNS name, if generated.
  • NetBox enabled/disabled state.

Do not scrape HCL directly when a plan JSON is available. The plan reflects variables, locals, defaults, and module expansion after Terraform evaluation.

NetBox Checks

Check whether NetBox already has the planned VM name:

GET /api/virtualization/virtual-machines/?name=<planned-name>

Check whether NetBox already has the planned IP:

GET /api/ipam/ip-addresses/?address=<planned-ip>

Fail on exact matches:

Failures:
  - NetBox already has VM 'cluster-a-worker-01'.
  - NetBox already has IP '192.0.2.10' (192.0.2.10/24 status=active dns=cluster-a-worker-01.example.com).

If NetBox credentials are missing, decide intentionally:

  • fail if NetBox checks are mandatory for the environment.
  • warn and continue only if the script explicitly supports degraded mode.

Example warning for degraded mode:

Warnings:
  - Skipping NetBox checks because NetBox URL/token environment variables or --netbox-url/--netbox-token were not provided.

vCenter Checks

Use govc to check whether the VM name already exists in vCenter:

govc find / -type m -name 'cluster-a-worker-01'

Failure example:

Failures:
  - vCenter already has VM 'cluster-a-worker-01': /DC-Site-A/vm/K8s-Cluster/Prod/cluster-a-worker-01.

Do not silently skip this check when govc is broken.

If vCenter checks are enabled, verify govc first:

govc about >/dev/null

Failure example:

Failures:
  - vCenter checks require a working govc configuration. Export GOVC_URL, GOVC_USERNAME, GOVC_PASSWORD, and GOVC_INSECURE as needed, or rerun with --skip-govc.

Forward DNS Checks

Check whether the planned VM name already resolves:

getent hosts 'cluster-a-worker-01'
getent hosts 'cluster-a-worker-01.example.com'

Failure example:

Failures:
  - DNS already resolves planned VM name 'cluster-a-worker-01'.

Forward DNS catches stale names that may not exist in NetBox or vCenter anymore.

Reverse DNS Checks

Check whether the planned IP has a PTR record:

dig +short -x '192.0.2.10'

Fallback if dig is unavailable:

getent hosts '192.0.2.10'

Failure example:

Failures:
  - Reverse DNS/getent already resolves planned IP '192.0.2.10'.

Reverse DNS is useful because PTR records often outlive the systems they describe.

Network Liveness Checks

Check whether the planned IP responds on the network:

nmap -sn -n '192.0.2.10'

Failure example:

Failures:
  - Network scan indicates planned IP '192.0.2.10' is already active.

This catches the important case where an address is active but missing from NetBox.

Skip Flags

Skip flags are useful for isolated tests and controlled degraded mode:

./scripts/preflight-vm-guardrails.sh \
  --plan-json tfplan.json \
  --skip-netbox \
  --skip-govc \
  --skip-dns \
  --skip-nmap

But skip flags should be visible in the summary:

Preflight VM guardrail summary
Planned VMs checked: 2
NetBox checks: skipped
vCenter checks: enabled
DNS checks: enabled
nmap checks: enabled

Safe Summary Format

Make the output explicit enough to paste into a change record:

Preflight VM guardrail summary
Planned VMs checked: 2
NetBox checks: enabled
vCenter checks: enabled
DNS checks: enabled
nmap checks: enabled
Warnings: 0
Failures: 0

On failure, include the exact proof:

Failures:
  - vCenter already has VM 'cluster-a-worker-01': /DC-Site-A/vm/K8s-Cluster/Prod/cluster-a-worker-01.
  - DNS already resolves planned VM name 'cluster-a-worker-01'.
  - Network scan indicates planned IP '192.0.2.10' is already active.

Operating Rule

Preflight does not replace teardown verification.

For disposable test VMs, finish with a destroy-plan review and post-destroy checks:

terraform plan -destroy -out=destroy.tfplan
terraform show -json destroy.tfplan \
  | jq -r '.resource_changes[]? | [.address, .type, (.change.actions | join(","))] | @tsv'
terraform apply destroy.tfplan
terraform state list

Then verify the external systems are clean:

curl -s \
  -H "Authorization: Token $NETBOX_API_TOKEN" \
  -H "Accept: application/json" \
  "$NETBOX_SERVER_URL/api/virtualization/virtual-machines/?name=cluster-a-test-01" \
  | jq '.count'

curl -s \
  -H "Authorization: Token $NETBOX_API_TOKEN" \
  -H "Accept: application/json" \
  "$NETBOX_SERVER_URL/api/ipam/ip-addresses/?q=192.0.2.10" \
  | jq '.count'

govc find / -type m -name 'cluster-a-test-01'

Expected results:

NetBox VM count: 0
NetBox IP count: 0
govc find: no output

Terraform plan answers “what will Terraform try to do?”

Preflight answers “is the outside world clear enough for Terraform to do it safely?”

Destroy verification answers “did Terraform clean up the outside-world records it created?”

Run all three before trusting vSphere VM automation end to end.