A VM can have the correct FQDN intent and still receive the wrong resolver search suffix.

The trap is treating these as the same setting:

vm_domain  -> identity/FQDN domain
dns_search -> resolver search suffix list

They are related, but they are not the same control.

Symptom

An environment sets DNS search suffixes to empty:

dns_search = "[]"

But new vSphere VMs still boot with a resolver search domain such as:

search corp.example.com

The node audit shows drift even though the Terraform input looked correct:

resolv_conf_search = corp.example.com
netplan_search     = [corp.example.com]

Root Cause Pattern

The environment may define dns_search, but the module might not consume it.

The broken pattern looks like this:

module "vm_group" {
  source = "../../../modules/vsphere-vm-group"

  vm_domain       = var.vm_domain
  dns_server_list = var.dns_server_list
  # dns_search exists in variables.tf, but is not passed here
}

Then the module derives vSphere guest customization search suffixes from vm_domain:

customize {
  linux_options {
    host_name = each.value.name
    domain    = var.vm_domain
  }

  network_interface {
    ipv4_address = each.value.ipv4_address
    ipv4_netmask = tonumber(each.value.ipv4_netmask)
  }

  ipv4_gateway    = var.ipv4_gateway
  dns_server_list = var.dns_server_list
  dns_suffix_list = [var.vm_domain]
}

In that shape, dns_search = "[]" is a red herring. It exists, but it does not control anything.

Prove The Active Path

Search the Terraform code first:

rg 'dns_search|dns_suffix_list|vm_domain|network-config|guestinfo' .

Look for two possible paths:

vSphere guest customization: customize.dns_suffix_list
cloud-init guestinfo:        network-config.yaml nameservers.search

If guestinfo network config is disabled, the cloud-init template is not the active source even if it contains a search entry.

Inspect Plan JSON Without Reading Secrets

If a plan JSON already exists, inspect only the resolved fields needed for evidence:

jq -r '
  .configuration.root_module.module_calls.vm_group.expressions.vm_domain.references,
  .variables.vm_domain,
  .variables.dns_search
' tfplan.json

Then inspect vSphere customization values:

jq -r '
  .planned_values.root_module.child_modules[]?
  | select(.address == "module.vm_group")
  | .resources[]?
  | select(.type == "vsphere_virtual_machine")
  | .values.clone[0].customize[0]
  | {dns_suffix_list, linux_options}
' tfplan.json

Useful evidence shape:

{
  "dns_search": { "value": "[]" },
  "dns_suffix_list": ["corp.example.com"],
  "linux_options": [{ "domain": "corp.example.com" }]
}

That proves the search suffix came from module logic, not from the intended empty dns_search input.

Minimal Module Fix

Pass dns_search into the module:

module "vm_group" {
  source = "../../../modules/vsphere-vm-group"

  vm_domain       = var.vm_domain
  dns_server_list = var.dns_server_list
  dns_search      = var.dns_search
}

Add a module variable that preserves the existing bracketed string interface:

variable "dns_search" {
  type        = string
  description = "DNS search suffixes in bracketed form, for example [] or [corp.example.com]"
  default     = "[]"
}

Parse it once:

locals {
  dns_search_suffixes = compact([
    for suffix in split(",", trim(var.dns_search, "[] ")) : trimspace(replace(suffix, "\"", ""))
  ])
}

Use it for vSphere customization:

dns_suffix_list = local.dns_search_suffixes

If cloud-init guestinfo network config is enabled, use the same parsed value there too. Do not maintain separate search suffix logic for vSphere customization and cloud-init.

Validate The Behavior

Run formatting and validation:

terraform fmt main.tf ../../../modules/vsphere-vm-group/main.tf ../../../modules/vsphere-vm-group/variables.tf
terraform validate

Then inspect a new plan if provider credentials are available. If not, use existing plan/state evidence and document that a fresh plan was blocked by missing provider credentials.

Expected behavior:

dns_search = "[]"                  -> dns_suffix_list = []
dns_search = "[corp.example.com]"  -> dns_suffix_list = ["corp.example.com"]
vm_domain = "corp.example.com"     -> still controls VM FQDN/domain intent

Operating Rule

Do not let a variable name create false confidence.

For infrastructure modules, every important input needs a traceable path:

root variable -> module argument -> module variable -> provider argument -> plan/state -> guest audit

If the chain breaks anywhere, the variable is documentation, not behavior.