A standalone module that creates one standard-vSwitch port group on one ESXi host
(vsphere_host_port_group), with locked-down Layer-2 security defaults and optional NIC-teaming
and traffic-shaping overrides.
Why it matters: A standard-vSwitch port group is the per-host attachment point that gives virtual machines their VLAN and Layer-2 security posture. Left to defaults, vSphere lets a port group inherit its security policy from the parent vSwitch — which can silently leave promiscuous mode, MAC changes, or forged transmits enabled. This module pins all three security options to the locked-down value (
false) on every call, so the empty call produces the safe port group and the caller must type extra characters to relax it. Teaming and shaping are left to inherit unless you explicitly override them, so the module never quietly changes host-wide network behaviour you did not ask it to.
This is a standalone module: exactly one keystone resource, vsphere_host_port_group.this.
It does not create the host or the vSwitch — both pre-exist and are referenced as inputs.
| Keystone resource | vsphere_host_port_group.this |
| Module type | Standalone (single resource) |
| Parent host | Consumed by MOID (host_system_id) |
| Parent vSwitch | Consumed by name string (virtual_switch_name) — not a MOID |
| Taggable | No — vsphere_host_port_group does not accept tags / custom_attributes |
terraform apply |
Never performed by this module — plan-only; a human reviews and applies |
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
Render the source below via the Mermaid Chart MCP (validated
flowchart). This module is highlighted in#607078. It attaches to ahost_system_id(MOID) fromterraform-vsphere-hostand binds to a pre-existing standard vSwitch by name (virtual_switch_name, a string, not a MOID). Itsidis a compositehost:nameidentifier used at the host networking layer, not wired into a downstream sibling module.
graph LR
host["terraform-vsphere-host<br/>host_system_id"]
vsw["pre-existing standard vSwitch<br/>virtual_switch_name (string on host)"]
hpg["terraform-vsphere-host-port-group"]
wl(["consumed at the host networking layer;<br/>id is a composite host:name identifier,<br/>not wired into a sibling module"])
host -->|host_system_id| hpg
vsw -->|virtual_switch_name| hpg
hpg --> wl
style hpg fill:#607078,color:#fff,stroke:#333,stroke-width:2px
Keystone node in
#00A1E0. This module wraps exactly one resource —vsphere_host_port_group.this— bound to a host byhost_system_id(MOID) and to its vSwitch byvirtual_switch_name(string). The L2 security policy is locked down by default. This resource is not taggable, so it carries no universal tail.
graph TD
host["host_system_id (MOID) +<br/>virtual_switch_name (string)<br/>consumed by ID / name"]
this["vsphere_host_port_group.this<br/>(keystone — one standard port group)<br/>security policy locked down by default"]
host --> this
style this fill:#00A1E0,color:#fff,stroke:#333,stroke-width:2px
Grant the Terraform integration account the minimum privilege set below on the target host (or a parent inventory object that propagates to it). Do not use the built-in vCenter Administrator account.
| Privilege | Why it is needed |
|---|---|
Host.Config.Network |
Create, modify, and delete standard-vSwitch port groups and their security / teaming / shaping policy on the ESXi host. |
Notes
- vCenter implicitly grants
System.Anonymous,System.View, andSystem.Readto every role — the read access needed to resolve the host and vSwitch is therefore already present and does not need to be added explicitly.- Validate the final set against your role with vCenter's Check Privileges feature before the first run.
| Requirement | Detail |
|---|---|
| vCenter version | ≥ 7.0 (any release supported by vmware/vsphere ~> 2.0). |
| License | None special. Standard vSwitches are available in every vSphere edition (Essentials through Enterprise Plus). Unlike a vSphere Distributed Switch, a standard-vSwitch port group does not require Enterprise Plus. |
| Host present | The target ESXi host must already exist and be reachable so the caller can resolve host_system_id (e.g. via data "vsphere_host"). |
| vSwitch present | The standard vSwitch named in virtual_switch_name must already exist on that specific host (e.g. vSwitch0, or one created by vsphere_host_virtual_switch). |
| Uplinks present | Any adapter named in active_nics / standby_nics must already be attached to that vSwitch's uplink set. |
| Tooling | Terraform >= 1.12.0; provider vmware/vsphere ~> 2.0, configured by the caller (never inside this module). |
# --- Caller's root module ---------------------------------------------------------
# Auth is configured here, on the provider block — never inside the module.
# Prefer environment variables: VSPHERE_SERVER, VSPHERE_USER, VSPHERE_PASSWORD.
provider "vsphere" {
allow_unverified_ssl = false # production: validate the vCenter certificate
}
# Parent objects pre-exist — the caller resolves them by ID.
data "vsphere_datacenter" "dc" {
name = "dc-primary"
}
data "vsphere_host" "esxi" {
name = "esxi-01.example.com"
datacenter_id = data.vsphere_datacenter.dc.id
}
# --- The module -------------------------------------------------------------------
module "app_network" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "app-tier-120"
host_system_id = data.vsphere_host.esxi.id
virtual_switch_name = "vSwitch0" # vSwitch name on the host (string, not a MOID); must pre-exist
vlan_id = 120
# allow_promiscuous / allow_mac_changes / allow_forged_transmits all default to false.
# teaming and shaping are omitted, so they inherit the parent vSwitch policy.
}
output "app_network_id" {
value = module.app_network.id
}Source is pinned to the immutable tag
?ref=v1.0.0— never a branch. No credentials appear in module code or examples; they flow from the caller's provider block / environment.
See examples/main.tf for a second example (an IDS / monitoring port group
that deliberately enables promiscuous mode and sets explicit failover teaming).
1 · Minimal — management port group on a vSwitch
# virtual_switch_name is the switch NAME (string), NOT a MOID.
module "pg_mgmt" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-mgmt"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch0"
}2 · VM network on a specific VLAN
module "pg_vm_network" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-vm-prod-100"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch1"
vlan_id = 100 # VLAN 100 for production VM network
}3 · vMotion network (dedicated NIC teaming, no VLAN tagging)
module "pg_vmotion" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-vmotion"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch2"
vlan_id = 0 # no VLAN tagging
active_nics = ["vmnic2"]
standby_nics = ["vmnic3"]
failback = true
}4 · iSCSI storage port group (dedicated NIC, no failover)
# iSCSI requires a single dedicated path per VMkernel — no NIC teaming/failover.
module "pg_iscsi_a" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-iscsi-a"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitchStorage"
vlan_id = 200
active_nics = ["vmnic4"] # only one NIC active — explicit path A
standby_nics = [] # no standby for iSCSI path A
check_beacon = false # beacon probing not supported without multiple NICs
notify_switches = false # not required for storage-only traffic
}5 · Trunk port group (VLAN 4095 — pass all VLANs to VMs)
# VLAN 4095 = "All" in vSphere: frames from all VLANs pass untagged to the VM.
# Used for virtual routers/firewalls doing their own VLAN tagging.
module "pg_trunk_vr" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-trunk-virtual-router"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch1"
vlan_id = 4095 # pass all VLANs
}6 · Teaming policy — IP hash (requires EtherChannel/LACP on physical switch)
module "pg_iphash" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-vm-lacp-300"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch1"
vlan_id = 300
teaming_policy = "loadbalance_ip" # IP hash — requires EtherChannel on uplink switch
active_nics = ["vmnic0", "vmnic1"]
notify_switches = false # not needed with IP hash
}7 · Traffic shaping (burst control for tenant isolation)
module "pg_shaped" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-tenant-a-400"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch1"
vlan_id = 400
shaping_enabled = true
shaping_average_bandwidth = 500000000 # 500 Mbps average (bits/sec)
shaping_peak_bandwidth = 1000000000 # 1 Gbps burst peak (bits/sec)
shaping_burst_size = 65536000 # 65 MB burst bucket (bytes)
}8 · Secure port group — all MAC security options explicitly denied
# secure default: all three MAC security options off (false = deny/reject).
# Override only with documented security exception.
module "pg_secure" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-secure-pci-500"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch1"
vlan_id = 500
allow_promiscuous = false # no sniffer / IDS-promiscuous mode
allow_mac_changes = false # VMs cannot change their effective MAC
allow_forged_transmits = false # VMs cannot spoof source MAC
}9 · Beacon probing (failover detection with 3+ NICs)
# Beacon probing requires at least 3 NICs to be reliable.
# Do NOT enable check_beacon with fewer than 3 uplinks.
module "pg_beacon" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-vm-beacon-600"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch1"
vlan_id = 600
active_nics = ["vmnic0", "vmnic1", "vmnic2"]
check_beacon = true # requires 3+ NICs for meaningful link health detection
failback = true # prefer original active NIC when it recovers
notify_switches = true # send gratuitous ARP on failover
}10 · Route based on originating virtual port (default, most common)
module "pg_route_portid" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-vm-portid-700"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch1"
vlan_id = 700
teaming_policy = "loadbalance_srcid" # default — hash on virtual port ID
active_nics = ["vmnic0", "vmnic1"]
standby_nics = []
failback = true
notify_switches = true
}11 · NFS storage port group (multiple active NICs, round-robin at switch)
module "pg_nfs" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-nfs-storage"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitchStorage"
vlan_id = 201
active_nics = ["vmnic4", "vmnic5"] # both active for NFS (unlike iSCSI)
standby_nics = []
notify_switches = true
}12 · Multiple port groups on the same host with for_each
locals {
port_groups = {
mgmt = { vlan = 10, sw = "vSwitch0" }
vmotion = { vlan = 20, sw = "vSwitch1" }
vm_prod = { vlan = 100, sw = "vSwitch1" }
vm_dev = { vlan = 110, sw = "vSwitch1" }
storage = { vlan = 200, sw = "vSwitchStorage" }
}
}
module "port_groups" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
for_each = local.port_groups
name = "pg-${each.key}"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = each.value.sw
vlan_id = each.value.vlan
}13 · Import an existing host port group
import {
to = module.pg_existing.vsphere_host_port_group.this
id = "tf-HostPortGroup:host-123:pg-mgmt" # format: tf-HostPortGroup:<host-id>:<pg-name>
}
module "pg_existing" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-mgmt"
host_system_id = data.vsphere_host.esxi01.id
virtual_switch_name = "vSwitch0"
vlan_id = 0
}14 · Same port group provisioned across all cluster hosts
locals {
cluster_hosts = {
esxi01 = data.vsphere_host.esxi01.id
esxi02 = data.vsphere_host.esxi02.id
esxi03 = data.vsphere_host.esxi03.id
esxi04 = data.vsphere_host.esxi04.id
}
}
module "pg_vm_prod_all_hosts" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
for_each = local.cluster_hosts
name = "pg-vm-prod-100"
host_system_id = each.value
virtual_switch_name = "vSwitch1"
vlan_id = 100
}15 · End-to-end: host → standard vSwitch → port group → VM
# Note: terraform-vsphere-host-port-group manages port groups only.
# The standard vSwitch (vsphere_host_virtual_switch) is managed by terraform-vsphere-host-virtual-switch.
# Wire the switch name string from that module's output.
module "vswitch1" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-virtual-switch?ref=v1.0.0"
name = "vSwitch1"
host_system_id = module.esxi01.host_id
network_adapters = ["vmnic2", "vmnic3"]
}
module "pg_vm_prod" {
source = "git::https://github.com/microsoftexpert/terraform-vsphere-host-port-group?ref=v1.0.0"
name = "pg-vm-prod-100"
host_system_id = module.esxi01.host_id
virtual_switch_name = module.vswitch1.name # ← string name from vSwitch module
vlan_id = 100
active_nics = ["vmnic2"]
standby_nics = ["vmnic3"]
}
output "port_group_key_for_vm" {
value = module.pg_vm_prod.key # wire to VM network interface port_group
}| Name | Type | Description |
|---|---|---|
name |
string |
Port group name (e.g. "Management", "vMotion"). Immutable — changing it forces destroy/recreate. |
host_system_id |
string (MOID) |
MOID of the ESXi host on which to create the port group. Consumed by ID (caller resolves via data "vsphere_host" or terraform-vsphere-host.host_id). Immutable — force-new. |
virtual_switch_name |
string (name) |
Name of the standard vSwitch to bind to — the vSwitch's string name on the host (e.g. "vSwitch0"), not a MOID. Must already exist on the host. Immutable — force-new. |
| Name | Type | Default | Description |
|---|---|---|---|
vlan_id |
number |
0 |
0 = no tagging (native/untagged); 1–4094 = tag with that 802.1Q VLAN; 4095 = trunk (guest manages its own tagging). Validated to 0–4095. In segmented networks, set an explicit VLAN rather than relying on 0. |
These three are rendered explicitly on every call (default false), so the port group is locked
down regardless of the parent vSwitch's posture. Set one to true only to deliberately relax it.
| Name | Type | Default | Risky opt-out (true) means… |
|---|---|---|---|
allow_promiscuous |
bool |
false |
Every VM on the port group sees all traffic on the vSwitch. Network-monitoring / IDS use only. |
allow_mac_changes |
bool |
false |
A guest may change its adapter's effective MAC address (enables MAC spoofing). |
allow_forged_transmits |
bool |
false |
A guest may send frames with a source MAC different from its own (enables MAC/IP spoofing). |
| Name | Type | Default | Description |
|---|---|---|---|
teaming_policy |
string |
null |
Load-balancing policy. One of loadbalance_ip, loadbalance_srcmac, loadbalance_srcid, failover_explicit. Validated. loadbalance_ip requires static EtherChannel on the physical switch. |
active_nics |
list(string) |
null |
Ordered list of active uplink names (e.g. ["vmnic0","vmnic1"]). Must already be uplinks on the target vSwitch. |
standby_nics |
list(string) |
null |
Ordered list of standby uplink names used for failover. |
check_beacon |
bool |
null |
Enable beacon probing for link-failure detection (requires vSwitch beacon-probing config; otherwise only link status is used). |
failback |
bool |
null |
If true, a recovered higher-precedence uplink is reactivated. |
notify_switches |
bool |
null |
If true, notify physical switches on a NIC failover (triggers their MAC-cache updates). |
The bandwidth / burst values apply only when shaping_enabled = true.
| Name | Type | Default | Description |
|---|---|---|---|
shaping_enabled |
bool |
null |
Enable egress traffic shaping on this port group. |
shaping_average_bandwidth |
number |
null |
Average bandwidth in bits per second. |
shaping_burst_size |
number |
null |
Maximum burst size in bytes. |
shaping_peak_bandwidth |
number |
null |
Peak bandwidth in bits per second during bursts. |
This resource exposes every policy option as a flat attribute — there are no nested blocks and no
objectinputs.vsphere_host_port_groupdoes not supporttagsorcustom_attributes, so this module has no universal tail.
id is listed first per the library standard. Note that for a standard-vSwitch port group it is a
composite per-host identifier, not a pure vCenter MOID.
| Output | Type | Description |
|---|---|---|
id |
string |
Terraform-unique ID, formatted tf-HostPortGroup:<host_system_id>:<name> (e.g. tf-HostPortGroup:host-10:Management). Standard-vSwitch port groups are tracked per host, not as a vCenter managed object, so this is a composite identifier rather than a pure MOID. |
name |
string |
Name of the port group. |
key |
string |
The port group key as returned by the vSphere API. |
computed_policy |
map |
Effective policy options (defaults + overrides) resolved by vSphere after vSwitch inheritance — security, teaming, and shaping. Use it to confirm what was actually applied. |
ports |
list |
Ports currently in use on this port group (each with key, type, and mac_addresses), as reported by vSphere. |
| Setting | Secure default | Risky opt-out | Rationale |
|---|---|---|---|
allow_promiscuous |
false (explicit) |
true |
Promiscuous mode exposes all vSwitch traffic to every attached VM — only valid for an IDS/span port. |
allow_mac_changes |
false (explicit) |
true |
Blocks a guest from changing its effective MAC (anti-spoofing). |
allow_forged_transmits |
false (explicit) |
true |
Blocks a guest from sourcing frames with a foreign MAC (anti-spoofing). |
vlan_id |
0 (untagged) |
explicit VLAN / 4095 trunk |
Defaults to untagged for the simplest call; documented to set an explicit VLAN in segmented networks. |
Beyond the secure defaults:
- Security is explicit, not inherited. The three security flags carry a real
falsedefault (notnull), so the module always writes the locked-down value to vCenter. The port group stays hardened even if the parent vSwitch is permissive. To relax a flag you must opt in per port group. - Teaming & shaping inherit by default. These carry
nulldefaults, so an unset value passes through and the port group inherits the parent vSwitch policy. The module never silently overrides host-wide teaming/shaping behaviour — you override only what you name. - Parents are consumed, never created. The host (
host_system_id, a MOID) and the vSwitch (virtual_switch_name, a per-host name string) must pre-exist. The module owns only the port group. - One keystone resource named
this. Standalone single-resource module — nocount, no child collections, nofor_each. - Auth lives in the caller. No
vsphere_server/user/password/allow_unverified_sslvariables exist in this module; the caller configures the provider.
Plan-only / static-analysis gate. This module never runs terraform apply — a human reviews the
plan and applies it from the caller's root module after review.
# From the module directory (static validation — no backend, no cloud calls):
terraform init -backend=false
terraform validate
terraform fmt -check# From the examples directory, validate the example wiring the same way:
cd examples
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module source to the immutable tag
?ref=v1.0.0— never a branch. - A
terraform planagainst live vCenter happens in the caller's root module (where the provider is configured). Review the plan; a human runsterraform apply— this module system never does.
| Symptom | Cause & resolution |
|---|---|
The vSwitch <name> was not found / port group fails to create |
virtual_switch_name is a per-host string name, not a MOID, and the vSwitch must already exist on that exact host. Confirm the name on the host (vCenter UI → Host → Configure → Virtual switches, or esxcli network vswitch standard list). A vSwitch on a different host does not count. |
Plan shows the port group being destroyed and recreated (-/+) |
name, host_system_id, and virtual_switch_name are all immutable / force-new. Changing any one replaces the port group, which briefly detaches VMs attached to it. To rename with less disruption, add a new module instance with the new name, migrate VM NICs, then remove the old one. |
active_nics / standby_nics rejected or has no effect |
The named uplinks (e.g. vmnic4) must already be attached to the target vSwitch's uplink set. Teaming overrides cannot reference adapters the vSwitch does not have. Attach the uplink to the vSwitch first (e.g. via vsphere_host_virtual_switch). |
| Security looser than expected, or a flag "won't turn off" | The module always writes the three security flags explicitly (false by default). If a VM still sees foreign traffic, check the vSwitch-level policy and any other port group on the same vSwitch, then inspect the computed_policy output to see the resolved effective values. |
"Where is the MOID?" — id looks unusual |
Expected. Standard-vSwitch port groups are not global vCenter managed objects, so id is the composite tf-HostPortGroup:<host>:<name> string. Use key for the API-side port group key, or computed_policy to confirm applied settings. |
| Same port group needed on several hosts | A standard-vSwitch port group is per host. There is no single multi-host resource — instantiate this module once per host (one module block per host_system_id), or use a distributed port group (terraform-vsphere-distributed-port-group) if you want one object spanning hosts. |
SCOPE.md— design intent, scope boundary, privileges, prerequisites, gotchas.examples/main.tf— working plan-only examples.- Provider resource:
vsphere_host_port_group - Sibling modules:
terraform-vsphere-distributed-port-group(vDS port group, spans hosts) ·terraform-vsphere-distributed-virtual-switch·terraform-vsphere-host
A standard vSwitch lives on one host at a time — so does its port group. Name the switch, pin the VLAN, lock the policy, and let the rest inherit.