Use this workflow when existing vSphere VMs need to be brought under Terraform state for audit and future lifecycle management.
The first goal is not to change the VMs. The first goal is to make Terraform aware of them and classify drift.
Safety Boundary
Set the operating rule before importing:
Import state and audit only. Do not apply full vSphere changes to existing cluster nodes.
This avoids turning a state migration into an accidental infrastructure mutation.
Refactor To The Shared Module
Move the environment from root-level VM resources to the shared module shape:
module "vm_group" {
source = "../../../modules/vsphere-vm-group"
vms = local.vms
netbox_enabled = true
netbox_cluster_name = var.netbox_cluster_name
# vSphere, template, network, DNS, and bootstrap inputs omitted
}
Keep VM definitions in a local map:
locals {
vms = {
"cp1" = {
name = "cluster-a-cp-01"
ipv4_address = "192.0.2.5"
ipv4_netmask = tostring(var.netmask)
cpu = 4
ram_gb = 16
disksize = 80
attach_data_disk = true
data_disk_gb = 200
}
}
}
Import State In Layers
Import in small, reviewable layers:
1. NetBox VM records
2. NetBox VM interfaces
3. NetBox IP addresses
4. NetBox primary IPv4 relationships
5. vSphere virtual machines
After each layer, run a targeted plan or full audit plan and inspect action types.
Plan Classification Command
Generate an audit plan:
terraform plan -out=audit.tfplan
Classify every resource action:
terraform show -json audit.tfplan \
| jq -r '.resource_changes[]? | [.address, .type, (.change.actions | join(","))] | @tsv'
Useful checkpoint output:
netbox_virtual_machine no-op
netbox_interface no-op
netbox_ip_address no-op
netbox_primary_ip no-op
vsphere_virtual_machine update
That means NetBox is reconciled, but vSphere drift remains.
Targeted Primary IP Reconciliation
If primary IP resources do not import cleanly, use a targeted plan and inspect it before apply:
terraform plan \
-target='module.vm_group.netbox_primary_ip.vm' \
-out=primary-ips.tfplan
terraform show -json primary-ips.tfplan \
| jq -r '.resource_changes[]? | [.address, .type, (.change.actions | join(","))] | @tsv'
Acceptable shape for a NetBox-only reconciliation:
netbox_interface no-op
netbox_ip_address no-op or update
netbox_virtual_machine no-op or update
netbox_primary_ip create
Do not apply if any vSphere VM appears in the targeted plan.
After apply, verify count:
terraform state list | grep 'module.vm_group.netbox_primary_ip.vm' | wc -l
Verify A Primary IP Resource
terraform state show 'module.vm_group.netbox_primary_ip.vm["cp1"]'
Then verify NetBox directly:
curl -s \
-H "Authorization: Token $NETBOX_TOKEN" \
-H "Accept: application/json" \
"$NETBOX_URL/api/virtualization/virtual-machines/$NETBOX_VM_ID/" \
| jq '{id,name,memory,disk,vcpus,primary_ip4}'
Audit-Only Finish Line
The first safe finish line is not a clean full plan. It may be:
NetBox resources: no-op
vSphere resources: update
no destroys
That is still useful. It means source-of-truth inventory is reconciled and remaining drift is isolated to legacy vSphere shape.
Operating Rule
Importing existing VMs into Terraform is an audit workflow first.
Do not apply the full plan until every vSphere update is classified as safe, intentionally accepted, or replaced by a node migration plan.