AWS Cloud WAN series · Part 13 of 17
AWS Cloud WAN: Managing the Global Policy as Terraform Code
Thirteenth in the AWS Cloud WAN series. Because the entire global network is one declarative policy document, your Terraform is the network and a plan diff is the routing change you're about to make worldwide. Here's how to express that policy in HCL, where the AWS modules save you, where attachment tags actually have to live, and how a terraform apply maps onto Cloud WAN's own version-and-change-set model — every claim sourced to AWS docs and the provider.
TL;DR
Cloud WAN is unusually well-suited to infrastructure as code: one policy document defines every segment, route, and attachment mapping, so the whole topology becomes a single reviewable artifact. Build it with the aws_networkmanager_core_network_policy_document data source rather than hand-rolled JSON; bootstrap the core network with a throwaway base policy so day-to-day policy edits don't force a resource rebuild; put your routing tags on the attachment resource, never the VPC; and remember that the real pre-deploy gate is the Cloud WAN change set, not terraform plan — a new policy version is never deployed automatically.
Do it this way
- Write the policy with the data source; scale it with dynamic blocks, not copy-paste.
- Bootstrap the core network with a minimal base policy, then apply the real one separately.
- Tag the attachment resource — that's the object the policy reads.
- Split global from regional into separate stacks/workspaces.
- Review the change set's policy diff before letting it go LIVE.
Don't do this
- Don't tag the VPC and expect segment placement to work.
- Don't manage the core network and VPC attachments in one workspace.
- Don't treat
terraform planas the safety gate — the change set is. - Don't assume a one-block HCL edit is local — the whole document regenerates.
- Don't adopt the AWS blueprints wholesale — they're explicitly not production-ready.
Why one policy document is a gift to Terraform — the network is the diff
Recall the contrast from Article 1. With Transit Gateway, your network lives as a hundred separate resources — attachments, route tables, associations, propagations, peerings — scattered across Regions; adding one VPC means touching several of them, in the right Region, in the right order. Cloud WAN collapses all of that into a single declarative core network policy. In Terraform terms, your whole global topology — every segment, every routing rule, every attachment mapping — becomes one version-controlled artifact. The policy document has eight top-level sections (only core-network-configuration and segments are required), and a 2025.11 addition, attachment-routing-policy-rules, that earlier drafts missed.
That single-document model is why a terraform plan diff is so legible here: it shows exactly the routing change you're about to make to the entire network, in one place. Combined with Cloud WAN's own policy versioning and rollback (Article 2), you get a clean story — the Terraform change history mirrors the policy version history, and rollback exists at both layers. The governance wins teams actually want follow from this: enforce controls in code review, run CI checks against the policy in pre-production, move faster with lower human risk — provided you build the controls first.
Four provider resources do the heavy lifting, and it helps to see how they stack before we write any of them (Figure 1): a global_network container, the core_network itself, the resource that applies the real policy, and the per-VPC attachments.
How you express the policy — the data source beats raw JSON
You can feed Cloud WAN raw JSON, but the AWS provider ships a purpose-built data source, aws_networkmanager_core_network_policy_document, that assembles the JSON for you from HCL blocks. Most practitioners prefer it for two concrete reasons: the block structure (segments, segment-actions, attachment-policies) reads far more clearly than a wall of JSON, and the data source validates and formats the document, so you don't ship a syntax error or a malformed CIDR. A minimal but real shape:
data "aws_networkmanager_core_network_policy_document" "main" {
core_network_configuration {
asn_ranges = ["64512-65534"]
edge_locations { location = "us-east-1" }
edge_locations { location = "eu-west-1" }
}
segments {
name = "production"
}
segments {
name = "development"
isolate_attachments = true
require_attachment_acceptance = true
}
attachment_policies {
rule_number = 100
condition_logic = "and"
conditions {
type = "tag-value"
key = "segment"
operator = "equals"
value = "production"
}
action {
association_method = "constant"
segment = "production"
}
}
segment_actions {
action = "share"
segment = "shared-services"
share_with = ["*"]
}
}
One detail worth internalizing: asn_ranges = ["64512-65534"] is left-closed, right-open — it yields usable ASNs 64512 through 65533, and only 64512-65534 or 4200000000-4294967294 are legal ranges. The whole network is roughly a hundred lines of HCL. Adding a Region or a segment is adding a block. That's the entire change — and the diff shows precisely that.
How you scale it without copy-paste — dynamic blocks
Static blocks get repetitive fast. A real network has many segments across many Regions, and east-west inspection wants an inspection segment per Region. The Terraform answer is dynamic blocks driven by a variable or local. Instead of hand-writing each segment, you define them as data and generate the blocks:
variable "segments" {
default = ["production", "development", "shared-services"]
}
data "aws_networkmanager_core_network_policy_document" "main" {
#... core_network_configuration...
dynamic "segments" {
for_each = var.segments
content {
name = segments.value
}
}
}
Now adding a segment is editing a list, and the document updates with it. The same technique scales edge locations, attachment-policy rules, and segment shares; for the common east-west pattern you iterate the Region list to generate one inspection segment per Region rather than copying blocks. The policy grows with your variables, not your line count — and the plan diff still shows exactly what changed. (Keep one eye on the hard ceiling of 40 segments per core network, which is not adjustable, and which Network Function Groups quietly draw from too.)
Why your first apply fights you — the bootstrap chicken-and-egg
There's a wrinkle the docs don't flag loudly. The aws_networkmanager_core_network resource needs a policy document to come into existence, but you usually want to manage the real policy separately so that ordinary policy edits don't force the core network to be recreated. The provider's answer is the dummy-base-policy pattern: create the core network with a minimal base policy — one ASN range, one edge location, one throwaway segment — using create_base_policy, then apply the live policy as a separate resource.
resource "aws_networkmanager_global_network" "main" {
description = "global-network"
}
resource "aws_networkmanager_core_network" "main" {
global_network_id = aws_networkmanager_global_network.main.id
base_policy_document = data.aws_networkmanager_core_network_policy_document.bootstrap.json
create_base_policy = true
}
# The REAL policy is applied as its own resource, so day-to-day
# edits never recreate the core network above.
resource "aws_networkmanager_core_network_policy_attachment" "main" {
core_network_id = aws_networkmanager_core_network.main.id
policy_document = data.aws_networkmanager_core_network_policy_document.main.json
}
This decouples the existence of the core network from the evolution of its policy, which keeps routine policy changes from triggering a disruptive resource replacement. (The old inline policy_document argument on the core-network resource was removed from the provider precisely to make this separation the supported path.) It's a small pattern with an outsized effect on operational safety. One sharp edge to know: you can't override the bootstrap ASN through base_policy_regions alone — set it via the base_policy_document instead.
Why your tags aren't taking effect — attachment vs. resource tags
This is the single most-missed rule in all of Cloud WAN IaC, and it costs people an afternoon every time. The attachment policy evaluates "the tags analyzed by the attachment policy, and not the tags associated with the VPC resource." In Terraform terms: tags on your aws_vpc are invisible to segment mapping. The tags that matter live on the aws_networkmanager_vpc_attachment resource — the attachment itself.
So the working version puts the routing tag on the attachment, where the policy's tag conditions can see it:
resource "aws_networkmanager_vpc_attachment" "app" {
core_network_id = aws_networkmanager_core_network.main.id
vpc_arn = aws_vpc.app.arn
subnet_arns = [for s in aws_subnet.app: s.arn]
# This tag is what the attachment policy matches on.
tags = {
segment = "production"
}
}
The attachment policy then evaluates rules lowest-number-first and stops at the first match, mapping that segment = production tag to the named segment. This is also exactly how the AWS module behaves: the tags you define under its core_network subnet type are applied to the attachment, not the VPC. Get this wrong and there's no error — the attachment simply matches no rule and lands in no segment, which looks like a routing bug and is really a tagging bug.
How an apply goes live — plan, change set, rollback, and drift
Here's the part that surprises people coming from ordinary Terraform: a successful terraform apply does not change your routing. Applying the policy creates a new, incrementally numbered Cloud WAN policy version from LATEST — and "a policy version is never implemented automatically." You hold many versions, but only one is LIVE. Between apply and live sits Cloud WAN's own gate: a change set that walks through Pending generation → Ready to execute → Execution succeeded (or Failed generation if it doesn't validate), where "Ready to execute" means the version was "verified with no issues and… can be deployed as the new LIVE policy." Rollback is restoring an out-of-date version — effectively one action that rolls the whole global-network config back (Figure 3).
terraform apply only stages a new version; the Cloud WAN change set is the pre-deploy gate, and rollback restores a prior version in one action.Two practical consequences. First, the change set, not terraform plan, is your authoritative diff surface for routing: it shows Type, Action, and New-versus-Previous values line by line, and Cloud WAN ships no native policy linter, so teams write their own executable assertions (cfn-guard, OPA, or a custom checker) on top — that's a documented AWS-published practice, not a guarantee the platform hands you. Second, policy changes are atomic: because the policy is one document, any edit regenerates the whole document. A small HCL tweak can have broad routing effects, so read the change set's policy diff before you execute it. That same atomicity is your friend for drift — if someone hand-edits the policy in the console, Terraform's next plan surfaces the difference as a diff against the one canonical document, and applying re-asserts the IaC version as LATEST. The state to manage is singular, which is the whole reason this maps so cleanly.
How you structure it for real — two workspaces, and AWS's modules
The most important structural decision, and the one practitioners surface repeatedly: don't deploy the core network and the VPC attachments in the same Terraform workspace. AWS's own guidance is to decouple the global resources (global network, core network, policy) from the regional Central VPCs into separate stacks for independent lifecycle management — the aws-ia module README says to "create the Global Network and Core Network in a different module definition than the Central VPCs… to decouple," and the blueprints repo recommends separating network infrastructure, workload VPCs, and inspection resources. (You'll also hear a "single-apply race condition" justification for the split; that framing is practitioner lore — the decoupling recommendation is the part that's officially documented.) The clean shape is two stages (Figure 4): Workspace 1 owns the core network in a hub account; Workspace 2..N is templated per account and attaches its VPCs into the already-existing core network, sharing it via RAM (Article 14).
You also don't have to build the HCL from zero. The AWS-maintained aws-ia/cloudwan/aws module deploys the global and core networks and abstracts central-VPC creation and attachment across Inspection, Egress, Ingress, and Shared-Services VPC types — and, as noted above, lands your defined tags on the attachment, which is exactly the mechanism from Article 4. The aws-samples/aws-cloud-wan-blueprints repository collects Cloud WAN patterns in Terraform and CloudFormation (using the AWS and AWSCC providers) as PoC starting points — but AWS is explicit that "these patterns are not ready for production environments," so customize CIDRs, variables, and configuration before deploying. Use both to learn the idioms and bootstrap a proof of concept; adapt to your standards rather than adopting wholesale.
A handful of Cloud-WAN-specific IaC gotchas round this out. Map attachment subnets by AZ ID (use1-az1), not AZ name, because AWS randomizes the name-to-physical mapping per account — bake this into your modules, especially for shared-services and inspection VPCs. Mind the two different "special Regions": Cloud WAN's home Region is always US West (Oregon) regardless of your provider config, while the RAM share of the core network must be initiated from N. Virginia (us-east-1) so other Regions can see it — don't conflate them. And when central VPCs reference an IPAM pool, structure your for_each so the keys are known at plan time, or you'll hit Terraform's "keys… cannot be determined until apply" error.
You now manage Cloud WAN as code with a clean two-workspace structure, the right resource for the live policy, tags on the right object, and the change set as your real gate. Article 14 builds directly on this — the multi-account and multi-workspace patterns in depth: why core network and attachments belong in separate workspaces, sharing across an organization with RAM, per-account templated attachment deployment, and wiring policy validation into CI/CD so a bad change is caught before it goes live.
Sources
- AWS provider (HashiCorp) —
aws_networkmanager_core_network,aws_networkmanager_core_network_policy_attachment, theaws_networkmanager_core_network_policy_documentdata source, and thecreate_base_policy/base_policy_documentbootstrap. registry.terraform.io/.../networkmanager_core_network - AWS —
aws-ia/cloudwan/awsTerraform module: central VPC types (inspection, egress, ingress, shared-services), the recommendation to decouple global resources from regional Central VPCs, and tags under thecore_networksubnet type applied to the attachment. registry.terraform.io/modules/aws-ia/cloudwan/aws/latest - AWS —
aws-samples/aws-cloud-wan-blueprints: Terraform + CloudFormation Cloud WAN patterns (AWS + AWSCC providers), explicitly "not ready for production environments"; separate network / workload / inspection stacks. github.com/aws-samples/aws-cloud-wan-blueprints - AWS — Attachment policies for the core network: the attachment's own tags are analyzed, not the tags on the underlying VPC resource; rule-number ordering, first-match-wins, association methods. docs.aws.amazon.com/.../cloudwan-policy-attachments.html
- AWS — Core network policies (JSON) reference: the eight top-level sections (incl.
attachment-routing-policy-rules), required sections, and the left-closed/right-open ASN ranges64512-65534/4200000000-4294967294. docs.aws.amazon.com/.../cloudwan-policies-json.html - AWS — Create/view a policy version & change sets: every change creates a new version, only one is LIVE, "a policy version is never implemented automatically," states Pending generation / Ready to execute / Execution succeeded / Failed generation / Out of date, and rollback by restoring an out-of-date version. docs.aws.amazon.com/.../cloudwan-create-policy-version.html
- AWS — How Silverflow combined Cloud WAN and DevOps (blog): no native Cloud WAN policy lint, so teams write their own executable assertions (custom checker / cfn-guard / OPA) gated in CI. aws.amazon.com/blogs/.../how-silverflow-modernized-network-operations...
- AWS — What is AWS Cloud WAN: the home Region is US West (Oregon)/us-west-2 regardless of provider config and can't be changed. docs.aws.amazon.com/.../what-is-cloudwan.html
- AWS — Share your Cloud WAN global resources (RAM): the share must be initiated from N. Virginia (us-east-1) so all other Regions can see the global resource. docs.aws.amazon.com/.../cloudwan-share-network.html
- AWS — Cloud WAN quotas: "Segments per core network" = 40, Adjustable = No. docs.aws.amazon.com/.../cloudwan-quotas.html
- AWS — Working with AZ IDs (RAM): AZ names are randomized per account; use the stable AZ ID (e.g.
use1-az1) when placing attachment subnets. docs.aws.amazon.com/ram/.../working-with-az-ids.html