TL;DR
VCF 9.1 expands the VMware.VimAutomation.Vpc module so PowerCLI can build much more of the VPC networking foundation, including IP blocks, external connections, transit gateways, connectivity profiles, VPCs, subnets, and external-IP assignments.
The practical automation pattern is to build provider-owned connectivity first, create application-owned VPC objects second, and publish selected private workloads by assigning an external IP from the connectivity profile.
The documented high-level cmdlet set does not expose a New-VpcNatRule command. Default outbound SNAT is enabled on the connectivity profile, while external-IP assignment on a VM network adapter drives the supported workload-publishing workflow. NAT state can then be inspected with Get-VpcNatRule.
This tutorial provides an end-to-end script, validation commands, sample output, troubleshooting guidance, and a rollback script that protects shared provider networking by default.
Introduction
PowerCLI has traditionally been strongest when the infrastructure object already existed. The network team created port groups and routed VLANs, then the virtualization team automated virtual machines against those prepared networks.
VCF 9.1 changes that operating boundary. The VPC module now exposes high-level cmdlets for objects that sit below the application subnet, including IP blocks, external connections, transit gateways, and connectivity profiles. That makes it possible to automate a complete VPC path instead of stopping at New-Vpc and New-VpcSubnet.
The opportunity is significant, but so is the blast radius. An application VPC can often be treated as disposable. An external connection, transit gateway, connectivity profile, or enterprise IP block may be shared by many VPCs.
A production script therefore needs more than a sequence of New-* commands. It needs ownership boundaries, validation, deterministic naming, dependency-aware rollback, and accurate handling of NAT and external IP assignments.
This walkthrough builds those controls into the automation rather than adding them after deployment.
What You Will Build
By the end of the walkthrough, you will have a PowerCLI workflow that:
connects to the VCF 9.1 workload-domain vCenter
explicitly loads the VPC module
validates the required span and edge cluster
creates an external connection backed by a routed VLAN
creates external and private transit-gateway IP blocks
creates a transit gateway and connectivity profile
enables north-south services and default outbound SNAT
creates an application VPC with three private subnets
connects a VM network adapter to a private VPC subnet
assigns either a requested or automatically allocated external IP
validates subnet status and NAT state
rolls back application objects without deleting shared provider constructs
The script is intentionally opinionated. It separates provider-owned networking from application-owned networking because those layers usually have different owners, approval paths, privileges, and lifecycles.
How the VPC Networking Objects Fit Together
The most important detail is dependency order. A VPC cannot consume a connectivity profile that does not exist, and a connectivity profile cannot provide external IPs or default SNAT without the required transit, IP, and service objects beneath it.
The divider is an operational boundary, not only a technical one.
Provider objects should normally be created by a network-platform pipeline and reused by application pipelines. This article creates both layers in one script so the full dependency chain is visible, but the rollback script does not delete provider objects unless an explicit switch is supplied.
The NAT Automation Boundary
VCF VPC networking supports default outbound SNAT and external-IP-based workload publishing, but the high-level PowerCLI surface does not treat every NAT rule as a directly authored object.
The supported pattern used here is:
Enable default outbound SNAT on the connectivity profile.
Associate an external IP block with that profile.
Connect a VM adapter to a VPC subnet.
Assign a specific external IP or request automatic allocation.
Inspect the resulting NAT state with Get-VpcNatRule.
This distinction matters because a script that invents New-VpcNatRule would look plausible but would not match the documented command set.
For custom NAT policies beyond the high-level workflow, use the appropriate supported API or SDK surface after validating the exact VCF 9.1 endpoint and object model.
Public subnets and external-IP assignments also solve different problems.
A public subnet is directly routed through the VPC architecture. An external IP assigned to a workload on a private subnet provides translated reachability without changing the private subnet’s access mode.
The script uses private subnets and publishes the sample VM from the private web tier so the external-IP and NAT behavior is explicit.
Prerequisites and Assumptions
Platform Prerequisites
You need:
a VCF 9.1 environment with VPC networking enabled
a workload-domain vCenter reachable from the PowerShell host
the current VCF.PowerCLI package installed
the VMware.VimAutomation.Vpc module available
permission to create VPC provider and application objects
an existing VPC span
an existing VPC edge cluster for centralized north-south services
an upstream VLAN, gateway, and routing configuration for the external network
a test VM with a network adapter that can be moved to the VPC subnet
a rollback port group if you plan to test the teardown script
Explicitly import VMware.VimAutomation.Vpc. Broadcom has documented environments where the module does not autoload even though VCF PowerCLI is installed.
Addressing Assumptions
Purpose
Example value
Ownership
External VLAN
3100
Physical or network platform team
External gateway
198.51.100.1/24
Upstream routing team
External allocation range
198.51.100.100-198.51.100.150
Network platform team
Requested VM external IP
198.51.100.120
Application allocation from approved pool
Transit subnet
100.64.16.0/21
Network platform team
Private TGW block
172.20.0.0/24
Network platform or project team
VPC private CIDR
10.42.0.0/16
Application or VPC owner
The 198.51.100.0/24 range is reserved for documentation and will not provide real enterprise connectivity.
Replace every sample value with addresses approved for your environment. Confirm upstream routing, VLAN reachability, overlap checks, address reservations, and ownership before execution.
Preflight Commands
Run these commands before the deployment script:
Get-Module VCF.PowerCLI -ListAvailable |
Sort-Object Version -Descending |
Select-Object -First 1 Name, Version
Import-Module VMware.VimAutomation.Vpc -ErrorAction Stop
Get-Command New-VpcIpBlock,
New-VpcExternalConnection,
New-VpcTransitGateway,
New-VpcConnectivityProfile,
New-Vpc,
New-VpcSubnet,
Get-VpcNatRule,
Get-VpcSubnetStatus
Successful output confirms that the advanced VPC cmdlets are available in the current session. It does not confirm that the connected account has permission to use them.
Confirm the Required Existing Objects
The deployment assumes that the span and edge cluster already exist.
Connect-VIServer -Server ‘vcsa01.corp.example’
Get-VpcSpan |
Select-Object Name, Id |
Format-Table -AutoSize
Get-VpcEdgeCluster |
Select-Object Name, Id |
Format-Table -AutoSize
Do not let the deployment script select an arbitrary span or edge cluster. Use exact names, validate that only one object matches, and confirm that the selected objects belong to the intended VCF networking design.
End-to-End PowerCLI Deployment Script
What You Must Change
Review every value in the configuration block.
At minimum, change:
the vCenter name
span name
edge cluster name
external VLAN
external gateway
external IP ranges
transit subnet
VPC private CIDR
VM name
fallback assumptions
requested external IP
The object-creation functions are rerunnable by name. If exactly one matching object exists, the script reuses it. If more than one object matches, the script stops rather than guessing.
Reuse is not full reconciliation. The helper does not automatically modify an existing object when its settings differ from the configuration block. The validation stage must detect that drift.
The final VM publishing action is deliberately not treated as blindly idempotent because changing an external IP or subnet can interrupt active workload connectivity.
Set-StrictMode -Version Latest
$ErrorActionPreference = ‘Stop’
$Config = [ordered]@{
VCenter = ‘vcsa01.corp.example’
SpanName = ‘default-span’
EdgeClusterName = ‘edge-cluster-01’
ExternalConnectionName = ‘dtd-ext-3100’
ExternalVlan = 3100
ExternalGatewayCidr = ‘198.51.100.1/24’
TransitGatewayName = ‘dtd-tgw’
TransitSubnet = ‘100.64.16.0/21’
ExternalIpBlockName = ‘dtd-external-ip’
ExternalIpCidr = ‘198.51.100.0/24’
ExternalIpRange = ‘198.51.100.100-198.51.100.150’
ExternalReservedIp = ‘198.51.100.100-198.51.100.109’
PrivateTgwIpBlockName = ‘dtd-private-tgw’
PrivateTgwCidr = ‘172.20.0.0/24’
PrivateTgwIpRange = ‘172.20.0.10-172.20.0.250’
ConnectivityProfileName = ‘dtd-vpc-profile’
VpcName = ‘dtd-app-vpc’
VpcPrivateCidr = ‘10.42.0.0/16’
Subnets = @(
[pscustomobject]@{
Name = ‘web-private’
AccessMode = ‘Private’
Cidr = ‘10.42.10.0/24’
Ipv4Size = $null
}
[pscustomobject]@{
Name = ‘app-private’
AccessMode = ‘Private’
Cidr = ‘10.42.20.0/24’
Ipv4Size = $null
}
[pscustomobject]@{
Name = ‘db-private’
AccessMode = ‘Private’
Cidr = ‘10.42.30.0/24’
Ipv4Size = $null
}
)
PublishedVmName = ‘api01’
PublishedAdapterName = ‘Network adapter 1’
PublishedSubnetName = ‘web-private’
# Use a valid address from ExternalIpRange.
# Set this to $null to request automatic allocation.
ExternalIp = ‘198.51.100.120’
}
function Get-ExactlyOneObject {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Label,
[Parameter(Mandatory)]
[scriptblock]$Lookup
)
$matches = @(& $Lookup)
if ($matches.Count -eq 0) {
throw “Required object not found: $Label”
}
if ($matches.Count -gt 1) {
throw “More than one object matched ‘$Label’. Use a unique name or scope.”
}
return $matches[0]
}
function Get-OrCreateObject {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Label,
[Parameter(Mandatory)]
[scriptblock]$Lookup,
[Parameter(Mandatory)]
[scriptblock]$Create
)
$matches = @(& $Lookup)
if ($matches.Count -gt 1) {
throw “More than one object matched ‘$Label’. Refusing ambiguous reuse.”
}
if ($matches.Count -eq 1) {
Write-Host “[EXISTS] $Label”
return $matches[0]
}
Write-Host “[CREATE] $Label”
return & $Create
}
if (-not (Get-Module -ListAvailable -Name VCF.PowerCLI)) {
throw ‘VCF.PowerCLI is not installed.’
}
Import-Module VCF.PowerCLI -ErrorAction Stop
Import-Module VMware.VimAutomation.Vpc -ErrorAction Stop
$credential = Get-Credential -Message “Credentials for $($Config.VCenter)”
$viServer = $null
try {
$viServer = Connect-VIServer `
-Server $Config.VCenter `
-Credential $credential
$span = Get-ExactlyOneObject `
-Label “Span ‘$($Config.SpanName)'” `
-Lookup {
Get-VpcSpan `
-Name $Config.SpanName `
-ErrorAction SilentlyContinue
}
$edgeCluster = Get-ExactlyOneObject `
-Label “Edge cluster ‘$($Config.EdgeClusterName)'” `
-Lookup {
Get-VpcEdgeCluster `
-Name $Config.EdgeClusterName `
-ErrorAction SilentlyContinue
}
$externalConnection = Get-OrCreateObject `
-Label “External connection ‘$($Config.ExternalConnectionName)'” `
-Lookup {
Get-VpcExternalConnection `
-Name $Config.ExternalConnectionName `
-ErrorAction SilentlyContinue
} `
-Create {
New-VpcExternalConnection `
-Name $Config.ExternalConnectionName `
-Vlan $Config.ExternalVlan `
-GatewayCidr $Config.ExternalGatewayCidr `
-Description ‘Managed by the DTD VPC PowerCLI workflow’
}
$externalIpBlock = Get-OrCreateObject `
-Label “External IP block ‘$($Config.ExternalIpBlockName)'” `
-Lookup {
Get-VpcIpBlock `
-Name $Config.ExternalIpBlockName `
-ErrorAction SilentlyContinue
} `
-Create {
New-VpcIpBlock `
-Name $Config.ExternalIpBlockName `
-Cidr $Config.ExternalIpCidr `
-IpRange $Config.ExternalIpRange `
-ReservedIp $Config.ExternalReservedIp `
-Visibility External `
-Description ‘Externally routed addresses for VPC services’
}
$privateTgwIpBlock = Get-OrCreateObject `
-Label “Private TGW IP block ‘$($Config.PrivateTgwIpBlockName)'” `
-Lookup {
Get-VpcIpBlock `
-Name $Config.PrivateTgwIpBlockName `
-ErrorAction SilentlyContinue
} `
-Create {
New-VpcIpBlock `
-Name $Config.PrivateTgwIpBlockName `
-Cidr $Config.PrivateTgwCidr `
-IpRange $Config.PrivateTgwIpRange `
-Visibility Private `
-Description ‘Private transit-gateway addressing for VPCs’
}
$transitGateway = Get-OrCreateObject `
-Label “Transit gateway ‘$($Config.TransitGatewayName)'” `
-Lookup {
Get-VpcTransitGateway `
-Name $Config.TransitGatewayName `
-ErrorAction SilentlyContinue
} `
-Create {
New-VpcTransitGateway `
-Name $Config.TransitGatewayName `
-TransitSubnet $Config.TransitSubnet `
-ExternalConnection $externalConnection `
-Span $span `
-Description ‘Transit gateway for the DTD application VPC pattern’
}
$connectivityProfile = Get-OrCreateObject `
-Label “Connectivity profile ‘$($Config.ConnectivityProfileName)'” `
-Lookup {
Get-VpcConnectivityProfile `
-Name $Config.ConnectivityProfileName `
-ErrorAction SilentlyContinue
} `
-Create {
New-VpcConnectivityProfile `
-Name $Config.ConnectivityProfileName `
-TransitGateway $transitGateway `
-ExternalIpBlock $externalIpBlock `
-PrivateTgwIpBlock $privateTgwIpBlock `
-EnableServiceGateway $true `
-EdgeCluster $edgeCluster `
-EnableDefaultSnat $true `
-Description ‘N-S services, external IP allocation, and default SNAT’
}
$vpc = Get-OrCreateObject `
-Label “VPC ‘$($Config.VpcName)'” `
-Lookup {
Get-Vpc `
-Name $Config.VpcName `
-ErrorAction SilentlyContinue
} `
-Create {
New-Vpc `
-Name $Config.VpcName `
-PrivateIp $Config.VpcPrivateCidr `
-ConnectivityProfile $connectivityProfile `
-Description ‘Application VPC created by PowerCLI’
}
$subnetObjects = @{}
foreach ($subnetSpec in $Config.Subnets) {
$subnet = Get-OrCreateObject `
-Label “Subnet ‘$($subnetSpec.Name)'” `
-Lookup {
Get-VpcSubnet `
-Vpc $vpc `
-Name $subnetSpec.Name `
-ErrorAction SilentlyContinue
} `
-Create {
$subnetParameters = @{
Vpc = $vpc
Name = $subnetSpec.Name
AccessMode = $subnetSpec.AccessMode
DhcpMode = ‘Server’
Description = “Application subnet $($subnetSpec.Name)”
}
if ($subnetSpec.Cidr) {
$subnetParameters.IpAddress = $subnetSpec.Cidr
}
else {
$subnetParameters.Ipv4Size = $subnetSpec.Ipv4Size
}
New-VpcSubnet @subnetParameters
}
$subnetObjects[$subnetSpec.Name] = $subnet
}
$vm = Get-ExactlyOneObject `
-Label “VM ‘$($Config.PublishedVmName)'” `
-Lookup {
Get-VM `
-Name $Config.PublishedVmName `
-ErrorAction SilentlyContinue
}
$networkAdapter = Get-ExactlyOneObject `
-Label “Adapter ‘$($Config.PublishedAdapterName)’ on ‘$($Config.PublishedVmName)'” `
-Lookup {
Get-NetworkAdapter `
-VM $vm `
-Name $Config.PublishedAdapterName `
-ErrorAction SilentlyContinue
}
$publishedSubnet = $subnetObjects[$Config.PublishedSubnetName]
if (-not $publishedSubnet) {
throw “Published subnet ‘$($Config.PublishedSubnetName)’ was not created.”
}
if ([string]::IsNullOrWhiteSpace([string]$Config.ExternalIp)) {
Write-Host “[PUBLISH] Requesting an automatic external IP for $($vm.Name)”
Set-NetworkAdapter `
-NetworkAdapter $networkAdapter `
-Subnet $publishedSubnet `
-AutoAssignExternalIp `
-Confirm:$false | Out-Null
$externalIpRequest = ‘Automatic allocation’
}
else {
$requestedExternalIp = [ipaddress]$Config.ExternalIp
Write-Host “[PUBLISH] Assigning external IP $requestedExternalIp to $($vm.Name)”
Set-NetworkAdapter `
-NetworkAdapter $networkAdapter `
-Subnet $publishedSubnet `
-ExternalIp $requestedExternalIp `
-Confirm:$false | Out-Null
$externalIpRequest = $requestedExternalIp.IPAddressToString
}
Start-Sleep -Seconds 5
$subnetStatus = @(Get-VpcSubnetStatus -Vpc $vpc)
$natRules = @(Get-VpcNatRule -Vpc $vpc)
[pscustomobject]@{
VCenter = $viServer.Name
Vpc = $vpc.Name
ConnectivityProfile = $connectivityProfile.Name
TransitGateway = $transitGateway.Name
SubnetCount = @($subnetObjects.Values).Count
SubnetStatusCount = $subnetStatus.Count
PublishedVm = $vm.Name
PublishedSubnet = $publishedSubnet.Name
ExternalIpRequest = $externalIpRequest
NatRuleCount = $natRules.Count
} | Format-List
}
finally {
if ($viServer) {
Disconnect-VIServer `
-Server $viServer `
-Confirm:$false | Out-Null
}
}
What the Script Does
The workflow follows the platform dependency chain rather than creating objects in an arbitrary order.
The external connection defines the VLAN and gateway-facing CIDR. The transit gateway binds that connection to an existing span and receives its own transit subnet.
The two IP blocks then describe:
external address allocation
private transit-gateway addressing
The connectivity profile combines those resources with an edge cluster, enables north-south services, and enables default outbound SNAT.
The application layer starts only after that provider foundation exists.
New-Vpc creates the private VPC address space. New-VpcSubnet creates three explicit Private access-mode subnets from the VPC-owned CIDR.
Those subnets consume space from 10.42.0.0/16. They do not consume the private TGW IP block. The private TGW block is attached to the provider profile so the same profile can support Private-TGW subnet allocation when that access mode is needed.
The VM adapter is then attached to web-private and receives either the requested external IP or an automatically allocated address.
The helper functions are intentionally strict. Reusing one exact object is reasonable. Silently selecting one of several identically named objects is not.
Validate the Deployment
A successful cmdlet return does not prove end-to-end connectivity.
Validation should cover:
object existence
object configuration
realized subnet status
VM attachment
NAT state
address ownership
upstream reachability
security policy
return routing
Validate the Provider Foundation
Import-Module VMware.VimAutomation.Vpc -ErrorAction Stop
Connect-VIServer -Server ‘vcsa01.corp.example’
Get-VpcIpBlock -Name ‘dtd-external-ip’ |
Format-List *
Get-VpcIpBlock -Name ‘dtd-private-tgw’ |
Format-List *
Get-VpcExternalConnection -Name ‘dtd-ext-3100’ |
Format-List *
Get-VpcTransitGateway -Name ‘dtd-tgw’ |
Format-List *
Get-VpcConnectivityProfile -Name ‘dtd-vpc-profile’ |
Format-List *
Check that:
the external block uses External visibility
the private TGW block uses Private visibility
the external range and reserved addresses are correct
the transit gateway references the intended span
the transit gateway references the expected external connection
the profile references the intended edge cluster
the profile references both IP blocks
north-south services are enabled
default outbound SNAT is enabled
Validate the VPC and Subnets
$vpc = Get-Vpc -Name ‘dtd-app-vpc’
$vpc |
Format-List *
Get-VpcSubnet -Vpc $vpc |
Select-Object Name, Id |
Format-Table -AutoSize
Get-VpcSubnetStatus -Vpc $vpc |
Format-List *
The status objects are more useful than inventory alone.
Inventory confirms that the desired objects exist. Status helps determine whether those objects were realized successfully by the underlying networking platform.
Validate the VM Attachment and NAT State
Get-VM -Name ‘api01’ |
Get-NetworkAdapter -Name ‘Network adapter 1’ |
Format-List Name, NetworkName, ConnectionState
Get-VpcNatRule -Vpc $vpc |
Format-List *
The NAT query is deliberately read-only. It confirms the platform-managed rules associated with default outbound SNAT and the workload external-IP assignment.
Rule names, identifiers, and the total rule count can vary by deployment and implementation state.
Add NAT Statistics to Validation
Where NAT statistics are available, add them to the evidence collection:
Get-VpcNatRule -Vpc $vpc |
Get-VpcNatRuleStatistics |
Format-List *
Statistics are useful after test traffic has been generated. A rule that exists but has no matching traffic may indicate that the test path, security policy, routing, or workload addressing is incorrect.
Expected Script Output
A fresh deployment should produce progress similar to this:
[CREATE] External connection ‘dtd-ext-3100’
[CREATE] External IP block ‘dtd-external-ip’
[CREATE] Private TGW IP block ‘dtd-private-tgw’
[CREATE] Transit gateway ‘dtd-tgw’
[CREATE] Connectivity profile ‘dtd-vpc-profile’
[CREATE] VPC ‘dtd-app-vpc’
[CREATE] Subnet ‘web-private’
[CREATE] Subnet ‘app-private’
[CREATE] Subnet ‘db-private’
[PUBLISH] Assigning external IP 198.51.100.120 to api01
VCenter : vcsa01.corp.example
Vpc : dtd-app-vpc
ConnectivityProfile : dtd-vpc-profile
TransitGateway : dtd-tgw
SubnetCount : 3
SubnetStatusCount : 3
PublishedVm : api01
PublishedSubnet : web-private
ExternalIpRequest : 198.51.100.120
NatRuleCount : 2
The NAT rule count above is illustrative, not a pass criterion.
Validate the actual rule objects and their translated addresses instead of expecting a fixed count.
Validate Data-Plane Connectivity
Complete the workflow with tests outside PowerCLI:
confirm the guest receives an address from web-private
confirm the expected default gateway and DNS settings
test communication between the application and database subnets
test outbound connectivity through default SNAT
test reachability to the assigned external IP from an approved upstream test point
confirm upstream routing or adjacency for the external block
confirm gateway firewall policy permits the required traffic
confirm distributed firewall policy permits the required traffic
verify return-path routing
verify that the external IP is recorded in the enterprise IPAM system
Object realization and packet forwarding are separate validation stages. Do not close the change based only on successful object creation.
Capture Deployment Evidence
A useful production script should preserve evidence rather than displaying everything only in the terminal.
$evidencePath = Join-Path `
-Path $PWD `
-ChildPath “vpc-evidence-$($Config.VpcName)-$(Get-Date -Format ‘yyyyMMdd-HHmmss’)”
New-Item `
-Path $evidencePath `
-ItemType Directory `
-Force | Out-Null
Get-Vpc -Name $Config.VpcName |
ConvertTo-Json -Depth 10 |
Set-Content -Path (Join-Path $evidencePath ‘vpc.json’)
Get-VpcSubnet -Vpc $vpc |
ConvertTo-Json -Depth 10 |
Set-Content -Path (Join-Path $evidencePath ‘subnets.json’)
Get-VpcSubnetStatus -Vpc $vpc |
ConvertTo-Json -Depth 10 |
Set-Content -Path (Join-Path $evidencePath ‘subnet-status.json’)
Get-VpcNatRule -Vpc $vpc |
ConvertTo-Json -Depth 10 |
Set-Content -Path (Join-Path $evidencePath ‘nat-rules.json’)
Get-VM -Name $Config.PublishedVmName |
Get-NetworkAdapter -Name $Config.PublishedAdapterName |
ConvertTo-Json -Depth 10 |
Set-Content -Path (Join-Path $evidencePath ‘network-adapter.json’)
Store the resulting evidence with the change ticket, pipeline run, or Git-controlled deployment record.
That evidence becomes valuable when troubleshooting drift, reviewing an incident, or proving which objects were created by a specific automation run.
Roll Back Safely
Rollback should reverse dependencies from the workload upward.
It should also preserve shared provider infrastructure unless the operator explicitly chooses a full teardown.
Default Rollback Behavior
The rollback script below:
removes the VM’s external IP assignment
moves the VM adapter to a fallback distributed port group
removes the application subnets
removes the application VPC
leaves the connectivity profile intact
leaves the transit gateway intact
leaves the external connection intact
leaves the provider IP blocks intact
never removes the existing span or edge cluster
Supplying -RemoveProviderObjects extends the rollback to provider objects created by the deployment script.
Before running rollback, confirm that no other VM uses the VPC subnets and no other VPC depends on the provider objects.
[CmdletBinding()]
param(
[switch]$RemoveProviderObjects
)
Set-StrictMode -Version Latest
$ErrorActionPreference = ‘Stop’
$Config = [ordered]@{
VCenter = ‘vcsa01.corp.example’
VpcName = ‘dtd-app-vpc’
PublishedVmName = ‘api01’
PublishedAdapterName = ‘Network adapter 1’
PublishedSubnetName = ‘web-private’
FallbackPortGroupName = ‘VM-Network-Fallback’
ConnectivityProfileName = ‘dtd-vpc-profile’
TransitGatewayName = ‘dtd-tgw’
ExternalConnectionName = ‘dtd-ext-3100’
ExternalIpBlockName = ‘dtd-external-ip’
PrivateTgwIpBlockName = ‘dtd-private-tgw’
}
Import-Module VCF.PowerCLI -ErrorAction Stop
Import-Module VMware.VimAutomation.Vpc -ErrorAction Stop
$credential = Get-Credential -Message “Credentials for $($Config.VCenter)”
$viServer = $null
try {
$viServer = Connect-VIServer `
-Server $Config.VCenter `
-Credential $credential
$vpc = Get-Vpc `
-Name $Config.VpcName `
-ErrorAction SilentlyContinue
if ($vpc) {
$publishedSubnet = Get-VpcSubnet `
-Vpc $vpc `
-Name $Config.PublishedSubnetName `
-ErrorAction SilentlyContinue
$vm = Get-VM `
-Name $Config.PublishedVmName `
-ErrorAction SilentlyContinue
if ($vm -and $publishedSubnet) {
$networkAdapter = Get-NetworkAdapter `
-VM $vm `
-Name $Config.PublishedAdapterName `
-ErrorAction SilentlyContinue
if ($networkAdapter) {
Write-Host ‘[ROLLBACK] Removing external IP assignment’
Set-NetworkAdapter `
-NetworkAdapter $networkAdapter `
-Subnet $publishedSubnet `
-UnassignExternalIp `
-Confirm:$false | Out-Null
$fallbackPortGroup = Get-VDPortgroup `
-Name $Config.FallbackPortGroupName `
-ErrorAction Stop
Write-Host ‘[ROLLBACK] Moving VM adapter to fallback port group’
Set-NetworkAdapter `
-NetworkAdapter $networkAdapter `
-Portgroup $fallbackPortGroup `
-Confirm:$false | Out-Null
}
}
Write-Host ‘[ROLLBACK] Removing application subnets’
@(Get-VpcSubnet `
-Vpc $vpc `
-ErrorAction SilentlyContinue) |
ForEach-Object {
Remove-VpcSubnet -Subnet $_
}
Write-Host ‘[ROLLBACK] Removing application VPC’
Remove-Vpc -Vpc $vpc
}
if ($RemoveProviderObjects) {
$confirmation = Read-Host `
‘Type REMOVE-PROVIDER to delete shared provider objects’
if ($confirmation -ne ‘REMOVE-PROVIDER’) {
throw ‘Provider-object removal was not confirmed.’
}
Write-Host ‘[ROLLBACK] Removing connectivity profile’
@(Get-VpcConnectivityProfile `
-Name $Config.ConnectivityProfileName `
-ErrorAction SilentlyContinue) |
ForEach-Object {
Remove-VpcConnectivityProfile `
-ConnectivityProfile $_
}
Write-Host ‘[ROLLBACK] Removing transit gateway’
@(Get-VpcTransitGateway `
-Name $Config.TransitGatewayName `
-ErrorAction SilentlyContinue) |
ForEach-Object {
Remove-VpcTransitGateway `
-TransitGateway $_
}
Write-Host ‘[ROLLBACK] Removing external connection’
@(Get-VpcExternalConnection `
-Name $Config.ExternalConnectionName `
-ErrorAction SilentlyContinue) |
ForEach-Object {
Remove-VpcExternalConnection `
-ExternalConnection $_
}
Write-Host ‘[ROLLBACK] Removing IP blocks’
@(
Get-VpcIpBlock `
-Name $Config.ExternalIpBlockName `
-ErrorAction SilentlyContinue
Get-VpcIpBlock `
-Name $Config.PrivateTgwIpBlockName `
-ErrorAction SilentlyContinue
) | ForEach-Object {
Remove-VpcIpBlock -IpBlock $_
}
}
}
finally {
if ($viServer) {
Disconnect-VIServer `
-Server $viServer `
-Confirm:$false | Out-Null
}
}
Run the default application-only rollback as follows:
./Remove-DtdVpc.ps1
Run a full teardown only after dependency review:
./Remove-DtdVpc.ps1 -RemoveProviderObjects
A rollback failure is often a useful safety signal.
It may indicate that:
a subnet still has attached workloads
a VPC still has dependent objects
a connectivity profile is still referenced
a transit gateway supports other VPCs
an external IP is still allocated
a supposedly dedicated provider object is actually shared
Do not respond to those failures by adding wildcard deletion or forced cleanup without understanding the remaining dependencies.
Troubleshooting Common Failures
VPC Cmdlets Are Not Recognized
Explicitly import the VPC module and verify the installed VCF.PowerCLI version.
Import-Module VMware.VimAutomation.Vpc -Force
Get-Command New-VpcIpBlock,
New-VpcTransitGateway,
New-VpcConnectivityProfile
Do not assume that importing only the umbrella module made every VPC cmdlet available in the current session.
The Script Finds More Than One Named Object
Do not remove the ambiguity check.
Duplicate names can exist across scopes, projects, or connected servers. Use unique naming, connect to one vCenter at a time, or extend the lookup with an appropriate server, project, or object-ID boundary.
An Existing Object Has the Correct Name but the Wrong Configuration
The helper functions reuse a single matching object. They do not reconcile every property.
Compare the existing object’s settings against the configuration block before proceeding:
Get-VpcExternalConnection -Name ‘dtd-ext-3100’ |
Format-List *
Get-VpcTransitGateway -Name ‘dtd-tgw’ |
Format-List *
Get-VpcConnectivityProfile -Name ‘dtd-vpc-profile’ |
Format-List *
In a production pipeline, detect drift and stop unless the operator has explicitly approved a change.
IP Block Creation Fails
Validate:
CIDR syntax
start and end addresses
reserved-address ranges
address overlap
visibility
upstream ownership
whether the requested block already exists under a different name
The platform can reject an invalid object, but it cannot determine whether the enterprise network team has approved the address space.
Transit Gateway Creation Fails
Validate that:
the span exists
the external connection exists
the transit subnet is valid
the transit subnet does not overlap another reserved range
the external connection belongs to the intended design
the current account has provider-level permissions
Connectivity Profile Creation Fails
Validate that:
the transit gateway is realized
the external IP block exists
the private TGW block exists
the edge cluster exists
the edge cluster supports the requested service path
the environment supports the selected centralized north-south design
EdgeCluster and ServiceCluster are not supplied together
Enabling north-south services with an edge cluster is a centralized-services pattern. Confirm that it matches the deployed VCF architecture before running the script.
Subnet Creation Fails
Check that each private subnet:
falls within the VPC private CIDR
does not overlap another subnet
uses a supported access mode
uses a valid DHCP mode
has enough available address space
can be realized by the selected connectivity profile
If adapting the example to create a public or private-TGW subnet, confirm that the correct provider IP block is associated with the connectivity profile and has sufficient free capacity.
External IP Assignment Fails
Check all of the following:
the requested IP is inside the associated external IP block
the IP falls within the configured allocation range
the IP is not reserved
the IP is not already assigned
the VM adapter is attached to the intended VPC subnet
the connectivity profile has north-south services enabled
only one of ExternalIp or AutoAssignExternalIp is used
the external address block is routed and operational upstream
To remove an existing assignment during controlled reconciliation, use UnassignExternalIp before requesting another address.
Get-VpcNatRule Returns Rules but Traffic Still Fails
NAT object presence does not prove packet delivery.
Validate:
guest IP configuration
guest default gateway
guest firewall
upstream routing
external VLAN reachability
gateway firewall policy
distributed firewall policy
physical firewall policy
return-path routing
source and destination port expectations
whether the application is actually listening
Generate test traffic, then inspect NAT statistics where supported.
Get-VpcNatRule Returns No Rules
Check that:
the VPC uses the expected connectivity profile
default outbound SNAT is enabled
the VM adapter is connected to the VPC subnet
an external IP was actually assigned
the requested address allocation completed successfully
the networking objects have finished realizing
Do not assume that an empty result means a missing PowerCLI module. It can also mean that no NAT state has been created for the VPC.
You Need a Custom NAT Rule
The documented high-level VPC cmdlet index exposes Get-VpcNatRule and Get-VpcNatRuleStatistics, but not a high-level New-VpcNatRule cmdlet.
Use connectivity-profile default SNAT and network-adapter external-IP assignment for the supported workflows shown here.
For a custom NAT rule, validate and use the relevant VCF 9.1 API or generated PowerCLI SDK binding rather than fabricating a high-level cmdlet.
Rollback Cannot Remove a Subnet or Provider Object
Treat the dependency error as a reason to stop.
Inventory:
attached VMs
external-IP assignments
connected VPCs
connectivity profiles
connectivity policies
shared IP pools
transit-gateway consumers
external-connection consumers
Never add wildcard removal merely to make the rollback script appear successful.
Production Improvements Beyond the Example
The single script is useful for learning and lab validation, but production automation should separate responsibilities.
Split Provider and Application Pipelines
The network-platform pipeline should own:
external connections
transit gateways
spans
edge-backed connectivity profiles
enterprise IP blocks
upstream routing coordination
Application pipelines should normally consume approved profiles and create:
VPCs
application subnets
workload attachments
approved external-IP assignments
This split reduces privilege, clarifies rollback, and allows provider objects to be lifecycle-managed independently of application releases.
Persist Desired State and Object IDs
Names are readable, but IDs are safer for long-term reconciliation.
Export a deployment manifest containing:
object names
object IDs
input CIDRs
allocated subnet ranges
external-IP assignments
connected VM adapters
module versions
execution timestamps
pipeline run identifiers
change-ticket identifiers
Store the manifest with the change record or pipeline artifact.
Add Preflight Address Validation
Before creating anything, validate:
CIDR overlap against existing VPCs
CIDR overlap against provider pools
requested external-IP membership
requested external-IP availability
reserved-address exclusions
transit-subnet uniqueness
upstream route ownership
IPAM approval
DNS requirements
The cmdlets can reject invalid platform state, but they cannot enforce every enterprise address-management rule.
Use Non-Interactive Authentication
Get-Credential is appropriate for an operator-run tutorial.
A production pipeline should use:
an approved secret store
a supported OAuth workflow
workload identity where available
short-lived tokens
scoped service identities
auditable credential rotation
Do not embed usernames, passwords, refresh tokens, or API tokens directly in the script.
Add WhatIf and Approval Boundaries
Where the cmdlet supports WhatIf, use it during planning and change review.
For cmdlets without a useful WhatIf path, add your own preview stage that prints:
objects that will be created
objects that will be reused
objects that differ from desired state
workloads that will be moved
external IPs that will be allocated
objects that rollback would remove
The preview should be generated before any state-changing command runs.
Create Evidence for Every Change
Capture before-and-after output from:
Get-VpcIpBlock
Get-VpcExternalConnection
Get-VpcTransitGateway
Get-VpcConnectivityProfile
Get-Vpc
Get-VpcSubnet
Get-VpcSubnetStatus
Get-VpcNatRule
Get-VpcNatRuleStatistics
Get-NetworkAdapter
Export the evidence as JSON so operators can compare desired state with realized state and preserve an audit trail.
Separate Control-Plane and Data-Plane Validation
A complete deployment has two validation gates.
Do not treat a successful control-plane deployment as proof that application traffic works.
The final acceptance gate should require both platform evidence and packet-flow evidence.
Conclusion
VCF 9.1 makes VPC networking a serious PowerCLI automation target rather than a small extension to VM provisioning.
The expanded VPC module can build the provider connectivity chain, allocate application networking, attach workloads, enable default outbound SNAT, and assign external IPs without leaving the PowerShell workflow.
The key is to automate the platform model accurately.
Default SNAT belongs on the connectivity profile. Workload publishing is requested through the VM network adapter. NAT state is validated with Get-VpcNatRule. Provider objects and application objects should have separate owners and separate rollback policies.
The end-to-end script in this article is a strong lab and implementation starting point, but the production pattern is modular:
establish a governed provider baseline once
let application pipelines consume it repeatedly
validate both control-plane state and packet flow
preserve evidence
remove only the objects the application pipeline truly owns
That approach turns PowerCLI from a collection of provisioning commands into a repeatable VCF networking operating model.
External References
VMware Cloud Foundation Blog: Programmable Infrastructure with VCF 9.1Canonical URL: https://blogs.vmware.com/cloud-foundation/2026/05/25/unlocking-the-full-potential-of-programmable-infrastructure-with-vmware-cloud-foundation-9-1-new-features-and-capabilities/
Broadcom Knowledge Base: VMware.VimAutomation.Vpc module does not autoload in VCF PowerCLICanonical URL: https://knowledge.broadcom.com/external/article/412787/vmwarevimautomationvpc-module-does-not-a.html
Broadcom Developer: New-VpcIpBlock Command | Vmware PowerCLI ReferenceCanonical URL: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.vpc/commands/new-vpcipblock
Broadcom Developer: New-VpcExternalConnection Command | Vmware PowerCLI ReferenceCanonical URL: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.vpc/commands/new-vpcexternalconnection
Broadcom Developer: New-VpcTransitGateway Command | Vmware PowerCLI ReferenceCanonical URL: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.vpc/commands/new-vpctransitgateway
Broadcom Developer: New-VpcConnectivityProfile Command | Vmware PowerCLI ReferenceCanonical URL: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.vpc/commands/new-vpcconnectivityprofile
Broadcom Developer: New-Vpc Command | Vmware PowerCLI ReferenceCanonical URL: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.vpc/commands/new-vpc
Broadcom Developer: New-VpcSubnet Command | Vmware PowerCLI ReferenceCanonical URL: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.vpc/commands/new-vpcsubnet
Broadcom Developer: Set-NetworkAdapter Command | Vmware PowerCLI ReferenceCanonical URL: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.core/commands/set-networkadapter
Broadcom Developer: Get-VpcNatRule Command | Vmware PowerCLI ReferenceCanonical URL: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.vpc/commands/get-vpcnatrule
Broadcom Developer: Get-VpcSubnetStatus Command | Vmware PowerCLI ReferenceCanonical URL: https://developer.broadcom.com/powercli/latest/vmware.vimautomation.vpc/commands/get-vpcsubnetstatus
VMware Cloud Foundation Blog: Virtual Private Clouds (VPCs) in vCenterCanonical URL: https://blogs.vmware.com/cloud-foundation/2025/06/25/virtual-private-clouds-vpcs-in-vcenter/
VCF 9.1 VPC Networking: Distributed vs. Centralized Transit Gateway Designs
TL;DR VCF 9.1 supports two distinct approaches for connecting Virtual Private Clouds to the physical data center network. A Distributed Transit Gateway…
Next PostVCF 5.2.x to 9.1 Upgrade Runbook: Exact Sequence, Dependencies, Downtime, and ValidationTL;DR A VCF 5.2.x to 9.1 upgrade is not a single SDDC Manager update followed by routine infrastructure patching. It is a dependency-controlled platform transition that begins with the Operations…
The post Automating VCF 9.1 VPC Networking with PowerCLI: IP Blocks, Subnets, NAT, and External IPs appeared first on Digital Thought Disruption.

