Ongoing service disruptions
For the most recent update on ongoing service disruptions affecting the AWS Middle East (UAE) Region (ME-CENTRAL-1), refer to the AWS Health Dashboard. For information on AWS Service migration, see How do I migrate my services to another region?
Applying AWS Cloud WAN routing policy labels to cross-account attachments at creation time
In this article, we review an automation to apply routing policy labels in Cloud WAN attachments between the attachment creation and the association of those attachments to their segment.
With the announcement of routing policies in AWS Cloud WAN, you can now apply advanced routing controls (such as filtering, summarization, path preference, or BGP communities) within your core network. These policies can be applied at the attachment, sharing, or CNE-to-CNE level. While the last two are configured in the core network policy, routing policies at the attachment level are configured using a routing policy label. For more details on this feature, see the AWS Cloud WAN Routing Policy: Fine-grained controls for your global network blog post.
The routing policy label can only be configured by the AWS Cloud WAN core network account owner. From a security standpoint, only the team managing the network will be able to configure which advanced routing controls an attachment is going to have. However, in multi-account environments, this means that we cannot apply routing controls to attachments at creation time:
- Attachments such as VPC, TGW route table, and Direct Connect gateway can be created in the spoke accounts (from the shared core network).
- These attachments are associated to their corresponding segments following the
attachment-policiesconfiguration in the core network policy document. - Once the attachment has been created and associated to the segment, that is when the routing policy label can be applied.
If we are applying advanced routing controls, the network will not behave as expected for the period of time between the attachment creation and the configuration of the routing policy label. How can we make sure that these routing policies are applied between the attachment creation and the association of those attachments to their segment? In this article, we review an automation to achieve this. In addition, we discuss how Terraform users can use multiple providers to avoid the creation of such automation.
Automating routing policy label assignment before segment association
In this automation, the idea is that although the attachment created from other accounts comes with the metadata needed to connect it to the corresponding segment, in our policy document we are going to add an extra condition (tag) to only create the association once our automation validates the attachment. We take advantage of Network Manager events to catch new attachments created and apply our automation logic.
- A spoke account creates an attachment (VPC, TGW route table, or DX gateway) to the shared core network.
- Network Manager generates a Topology Change event in us-west-2 (Cloud WAN's home region). An AWS EventBridge rule catches the attachment creation event and invokes an AWS Step Functions state machine for processing. We recommend you filter by the attachment types that can only be created by another AWS account: VPC, Transit Gateway route table, and Direct Connect gateway.
- The state machine applies the routing policy label to the attachment and adds the validation tag, which triggers the attachment-policy evaluation and associates the attachment to its corresponding segment.
Important: For this automation to work, you need to ensure that the tag key-value used in the core network owner account to validate the attachment cannot be configured in the spoke accounts. You can use AWS Organizations Service Control Policies (SCPs) to prevent the use of those tags in those accounts.
Core network policy
To show how the automation works, we use the following policy document as example. The segment, routing action, and routing policy definitions are not relevant to the automation logic; focus on the attachment-policies section.
{ "core-network-configuration": { "dns-support": true, "security-group-referencing-support": false, "vpn-ecmp-support": false, "asn-ranges": [ "65520-65525" ], "edge-locations": [ { "location": "eu-west-1" }, { "location": "us-east-1" }, { "location": "us-west-2" } ] }, "attachment-routing-policy-rules": [ { "rule-number": 100, "action": { "associate-routing-policies": [ "filterSecondaryCidr" ] }, "conditions": [ { "type": "routing-policy-label", "value": "exampleAttachment" } ] } ], "routing-policies": [ { "routing-policy-rules": [ { "rule-number": 100, "rule-definition": { "condition-logic": "or", "action": { "type": "drop" }, "match-conditions": [ { "type": "prefix-equals", "value": "172.168.0.0/16" } ] } } ], "routing-policy-name": "filterSecondaryCidr", "routing-policy-direction": "inbound", "routing-policy-number": 100 } ], "version": "2025.11", "attachment-policies": [ { "rule-number": 100, "condition-logic": "and", "action": { "association-method": "tag", "tag-value-of-key": "domain" }, "conditions": [ { "type": "tag-value", "value": "validated", "key": "attachment", "operator": "equals" }, { "type": "attachment-type", "value": "vpc", "operator": "equals" } ] }, { "rule-number": 200, "condition-logic": "and", "action": { "association-method": "tag", "tag-value-of-key": "domain" }, "conditions": [ { "type": "tag-value", "value": "validated", "key": "attachment", "operator": "equals" }, { "type": "attachment-type", "value": "transit-gateway-route-table", "operator": "equals" } ] }, { "rule-number": 300, "condition-logic": "and", "action": { "association-method": "constant", "segment": "hybrid" }, "conditions": [ { "type": "tag-value", "value": "validated", "key": "attachment", "operator": "equals" }, { "type": "attachment-type", "value": "direct-connect-gateway", "operator": "equals" } ] } ], "segments": [ { "isolate-attachments": true, "name": "production", "require-attachment-acceptance": false }, { "isolate-attachments": false, "name": "development", "require-attachment-acceptance": false }, { "isolate-attachments": false, "name": "hybrid", "require-attachment-acceptance": false } ] }
Looking at the attachment-policies section:
- Rules 100 and 200 associate VPC or Transit Gateway route table attachments to the segment matching the value of the attachment's
domaintag (tag-value-of-keyaction). With this rule, we can cover any segment association with only one rule. However, we include an extra condition: the association will happen only if the tagattachment = validatedis present (that's our validation tag). - Rule 300 works in a similar way, but the action is
constant, so any Direct Connect gateway attachment is associated to thehybridsegment. The extra condition is also included.
The following SCP example denies spoke accounts from creating or updating the attachment = validated tag on Cloud WAN attachments:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "DenyAttachmentValidationTag", "Effect": "Deny", "Action": [ "networkmanager:TagResource", "networkmanager:UntagResource" ], "Resource": "arn:aws:networkmanager::*:attachment/*", "Condition": { "StringEquals": { "aws:RequestTag/attachment": "validated" } } } ] }
EventBridge rule
With the policy document in place, let's look at the automation components. First, the EventBridge rule that catches new attachments created and invokes the Step Functions state machine:
{ "detail": { "changeType": ["VPC_ATTACHMENT_CREATED", "TGW_RTB_ATTACHMENT_CREATED", "DIRECT_CONNECT_GATEWAY_ATTACHMENT_CREATED"] }, "detail-type": ["Network Manager Topology Change"], "source": ["aws.networkmanager"] }
Note: The EventBridge rule must have permissions to invoke the state machine.
Step Functions state machine
Next, the Step Functions state machine. The state machine has the following steps:
- The first step extracts the attachment type, attachment ID, attachment ARN, and core network ID from the EventBridge event as global variables for use in subsequent states.
- The second step applies the routing policy label (in our example
exampleAttachment) to the attachment via a Lambda function. - After applying the label, the attachment enters a transitional state. The state machine waits and polls the attachment status (using the corresponding Get API based on the attachment type) until it reaches
AVAILABLE. - Once the attachment is ready, the state machine adds the
attachment = validatedtag to trigger the attachment-policy evaluation and associate the attachment to its corresponding segment. - After tagging, the attachment enters another transitional state. The state machine waits and polls again until the attachment reaches
AVAILABLE.
At the time of the creation of this article, the put-attachment-routing-policy-label is not supported as a native Step Functions SDK integration, so we use a Lambda function for this action. We will update this article once the SDK integration becomes available.
Note: The Step Functions state machine and Lambda function must have permissions to perform the required actions (put routing policy label, get attachment status, and create tags).
The full state machine definition (using JSONata query language):
{ "Comment": "Apply routing policy label and validate new attachments", "QueryLanguage": "JSONata", "StartAt": "SetVariables", "States": { "SetVariables": { "Type": "Pass", "Assign": { "changeType": "{% $states.input.detail.changeType %}", "attachmentId": "{% $split($states.input.detail.attachmentArn, '/')[1] %}", "attachmentArn": "{% $states.input.detail.attachmentArn %}", "coreNetworkId": "{% $split($states.input.detail.coreNetworkArn, '/')[1] %}" }, "Next": "PutRoutingPolicyLabel" }, "PutRoutingPolicyLabel": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Arguments": { "FunctionName": "arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:cloudwan-put-routing-policy-label", "Payload": { "CoreNetworkId": "{% $coreNetworkId %}", "AttachmentId": "{% $attachmentId %}", "RoutingPolicyLabel": "exampleAttachment" } }, "Next": "WaitAfterLabel" }, "WaitAfterLabel": { "Type": "Wait", "Seconds": 10, "Next": "RouteGetAttachmentAfterLabel" }, "RouteGetAttachmentAfterLabel": { "Type": "Choice", "Choices": [ { "Condition": "{% $changeType = 'VPC_ATTACHMENT_CREATED' %}", "Next": "GetVpcAttachmentAfterLabel" }, { "Condition": "{% $changeType = 'TGW_RTB_ATTACHMENT_CREATED' %}", "Next": "GetTgwAttachmentAfterLabel" }, { "Condition": "{% $changeType = 'DIRECT_CONNECT_GATEWAY_ATTACHMENT_CREATED' %}", "Next": "GetDxgwAttachmentAfterLabel" } ] }, "GetVpcAttachmentAfterLabel": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:networkmanager:getVpcAttachment", "Arguments": { "AttachmentId": "{% $attachmentId %}" }, "Assign": { "attachmentState": "{% $states.result.VpcAttachment.Attachment.State %}" }, "Next": "CheckStateAfterLabel" }, "GetTgwAttachmentAfterLabel": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:networkmanager:getTransitGatewayRouteTableAttachment", "Arguments": { "AttachmentId": "{% $attachmentId %}" }, "Assign": { "attachmentState": "{% $states.result.TransitGatewayRouteTableAttachment.Attachment.State %}" }, "Next": "CheckStateAfterLabel" }, "GetDxgwAttachmentAfterLabel": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:networkmanager:getDirectConnectGatewayAttachment", "Arguments": { "AttachmentId": "{% $attachmentId %}" }, "Assign": { "attachmentState": "{% $states.result.DirectConnectGatewayAttachment.Attachment.State %}" }, "Next": "CheckStateAfterLabel" }, "CheckStateAfterLabel": { "Type": "Choice", "Choices": [ { "Condition": "{% $attachmentState = 'AVAILABLE' %}", "Next": "TagAttachment" } ], "Default": "WaitAfterLabel" }, "TagAttachment": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:networkmanager:tagResource", "Arguments": { "ResourceArn": "{% $attachmentArn %}", "Tags": [ { "Key": "attachment", "Value": "validated" } ] }, "Next": "WaitAfterTag" }, "WaitAfterTag": { "Type": "Wait", "Seconds": 10, "Next": "RouteGetAttachmentAfterTag" }, "RouteGetAttachmentAfterTag": { "Type": "Choice", "Choices": [ { "Condition": "{% $changeType = 'VPC_ATTACHMENT_CREATED' %}", "Next": "GetVpcAttachmentAfterTag" }, { "Condition": "{% $changeType = 'TGW_RTB_ATTACHMENT_CREATED' %}", "Next": "GetTgwAttachmentAfterTag" }, { "Condition": "{% $changeType = 'DIRECT_CONNECT_GATEWAY_ATTACHMENT_CREATED' %}", "Next": "GetDxgwAttachmentAfterTag" } ] }, "GetVpcAttachmentAfterTag": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:networkmanager:getVpcAttachment", "Arguments": { "AttachmentId": "{% $attachmentId %}" }, "Assign": { "attachmentState": "{% $states.result.VpcAttachment.Attachment.State %}" }, "Next": "CheckStateAfterTag" }, "GetTgwAttachmentAfterTag": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:networkmanager:getTransitGatewayRouteTableAttachment", "Arguments": { "AttachmentId": "{% $attachmentId %}" }, "Assign": { "attachmentState": "{% $states.result.TransitGatewayRouteTableAttachment.Attachment.State %}" }, "Next": "CheckStateAfterTag" }, "GetDxgwAttachmentAfterTag": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:networkmanager:getDirectConnectGatewayAttachment", "Arguments": { "AttachmentId": "{% $attachmentId %}" }, "Assign": { "attachmentState": "{% $states.result.DirectConnectGatewayAttachment.Attachment.State %}" }, "Next": "CheckStateAfterTag" }, "CheckStateAfterTag": { "Type": "Choice", "Choices": [ { "Condition": "{% $attachmentState = 'AVAILABLE' %}", "Next": "Done" } ], "Default": "WaitAfterTag" }, "Done": { "Type": "Succeed" } } }
The Lambda function used by the PutRoutingPolicyLabel state signs the API request using SigV4:
import json import os from urllib import request import botocore.auth import botocore.credentials import botocore.session REGION = os.environ.get("AWS_REGION", "us-west-2") ENDPOINT = f"https://networkmanager.{REGION}.amazonaws.com/routing-policy-label" def get_credentials(): session = botocore.session.get_session() return session.get_credentials().get_frozen_credentials() def sign_request(method, url, body, credentials): req = botocore.awsrequest.AWSRequest(method=method, url=url, data=body, headers={ "Content-Type": "application/json", }) signer = botocore.auth.SigV4Auth(credentials, "networkmanager", REGION) signer.add_auth(req) return dict(req.headers) def handler(event, context): body = json.dumps({ "AttachmentId": event["AttachmentId"], "CoreNetworkId": event["CoreNetworkId"], "RoutingPolicyLabel": event["RoutingPolicyLabel"], }) credentials = get_credentials() headers = sign_request("POST", ENDPOINT, body, credentials) req = request.Request(ENDPOINT, data=body.encode(), headers=headers, method="POST") with request.urlopen(req) as resp: result = json.loads(resp.read()) return { "AttachmentId": event["AttachmentId"], "RoutingPolicyLabel": event["RoutingPolicyLabel"], "State": result.get("Attachment", {}).get("State", "UNKNOWN"), }
Considerations and limitations
- The automation resources (EventBridge rule, Step Functions state machine, and Lambda function) need to be created in us-west-2 (Cloud WAN's home region).
- We selected Step Functions to apply our logic, as the SDK integration simplifies the management of the solution.
- This automation assumes that there is no
accept-attachmentconfigured in any attachment or segment. Take into account that no EventBridge events are generated if an attachment is created with aPENDING_ATTACHMENT_ACCEPTANCEstate. - When using Infrastructure-as-Code (IaC), you may see some drifts when adding the validation tag: the attachment's IaC state configuration in the spoke account will not have the new tag created (although it exists in the resource). Make sure that updates in those attachments do not try to update the attachment itself (as it will throw errors or break that attachment's routing).
Terraform alternative: cross-account providers for routing policy labels
If you use Terraform, there is a way to avoid the creation of the automation (and the state drifts mentioned above). We take advantage of two capabilities:
- AWS Cloud WAN allows you to apply the routing policy label to an attachment in
AVAILABLE,PENDING_ATTACHMENT_ACCEPTANCE, andPENDING_TAG_ACCEPTANCEstates. This means we can apply the routing policy label before an attachment is associated to a segment without the need to add extra tags. In this case, we need to add an attachment acceptance flag in the policy document. - We can accept any attachment and put a routing policy label to an attachment by using dedicated Terraform resources: aws_networkmanager_attachment_accepter and aws_networkmanager_attachment_routing_policy_label. This way, we can make use of two different AWS provider definitions to manage resources both in the spoke and core network owner accounts.
Following the core network policy example used before, in this case we can simplify the attachment-policies section:
{ "core-network-configuration": { "dns-support": true, "security-group-referencing-support": false, "vpn-ecmp-support": false, "asn-ranges": [ "65520-65525" ], "edge-locations": [ { "location": "eu-west-1" }, { "location": "us-east-1" }, { "location": "us-west-2" } ] }, "attachment-routing-policy-rules": [ { "rule-number": 100, "action": { "associate-routing-policies": [ "filterSecondaryCidr" ] }, "conditions": [ { "type": "routing-policy-label", "value": "exampleAttachment" } ] } ], "routing-policies": [ { "routing-policy-rules": [ { "rule-number": 100, "rule-definition": { "condition-logic": "or", "action": { "type": "drop" }, "match-conditions": [ { "type": "prefix-equals", "value": "172.168.0.0/16" } ] } } ], "routing-policy-name": "filterSecondaryCidr", "routing-policy-direction": "inbound", "routing-policy-number": 100 } ], "version": "2025.11", "attachment-policies": [ { "rule-number": 100, "condition-logic": "or", "action": { "association-method": "tag", "tag-value-of-key": "domain", "require-acceptance": true }, "conditions": [ { "type": "attachment-type", "value": "transit-gateway-route-table", "operator": "equals" }, { "type": "attachment-type", "value": "vpc", "operator": "equals" } ] }, { "rule-number": 200, "condition-logic": "or", "action": { "association-method": "constant", "segment": "hybrid", "require-acceptance": true }, "conditions": [ { "type": "attachment-type", "value": "direct-connect-gateway", "operator": "equals" } ] } ], "segments": [ { "isolate-attachments": true, "name": "production", "require-attachment-acceptance": false }, { "isolate-attachments": false, "name": "development", "require-attachment-acceptance": false }, { "isolate-attachments": false, "name": "hybrid", "require-attachment-acceptance": false } ] }
As you can see, we do not need the extra tag condition for the attachment's association, but we include the require-acceptance = true flag. This way any new attachment is properly mapped to the corresponding segment, but it will not complete the routing configuration until we accept the attachment.
When we create our resources, we can create them in the desired order (Attachment -> Put routing policy label -> Attachment acceptance) at creation time:
terraform { required_version = ">= 1.3.0" required_providers { aws = { source = "hashicorp/aws" version = ">= 6.34.0" } } } provider "aws" { alias = "spoke_account" } provider "aws" { alias = "network_account" } resource "aws_networkmanager_vpc_attachment" "vpc_attachment" { provider = aws.spoke_account subnet_arns = [aws_subnet.subnet.arn] core_network_id = var.core_network_id vpc_arn = aws_vpc.vpc.arn } resource "aws_networkmanager_attachment_routing_policy_label" "attachment_policy_label" { provider = aws.network_account core_network_id = var.core_network_id attachment_id = aws_networkmanager_vpc_attachment.vpc_attachment.id routing_policy_label = "exampleAttachment" } resource "aws_networkmanager_attachment_accepter" "attachment_accepter" { provider = aws.network_account attachment_id = aws_networkmanager_vpc_attachment.vpc_attachment.id attachment_type = "VPC" depends_on = [ aws_networkmanager_attachment_routing_policy_label.attachment_policy_label ] }
Note: A similar pattern can be used for Transit Gateway route table and Direct Connect gateway attachments, using the proper values when configuring the attachment resource and the
attachment_typein the attachment accepter.
Best practices and security measures when using multiple AWS providers
When using multiple AWS provider definitions in the same Terraform state, you are managing resources across different AWS accounts from a single execution context. This introduces security and operational considerations:
- Use IAM roles with
assume_roleinstead of static credentials. Avoid passing access keys and secret keys directly. Instead, configure each provider with anassume_roleblock that references a cross-account IAM role. This way, credentials are short-lived and follow the principle of least privilege.
provider "aws" { alias = "network_account" region = var.aws_region assume_role { role_arn = "arn:aws:iam::{NETWORK_ACCOUNT_ID}:role/TerraformCrossAccountRole" } }
- Scope the cross-account IAM role permissions to the minimum required. The role assumed in the network account only needs
networkmanager:PutAttachmentRoutingPolicyLabelandnetworkmanager:AcceptAttachmentpermissions. Do not grant broad administrative access. - Protect the Terraform state file. Since the state contains references to resources in multiple accounts, treat it as sensitive.
- Be explicit with provider assignments. When using aliased providers, Terraform does not automatically assign a provider to resources. Always use the
providerargument in every resource to avoid accidentally creating resources in the wrong account. - Review the Terraform plan carefully. With cross-account providers, a misconfigured
providerargument can create or modify resources in the wrong account. Always review the plan output to confirm each resource targets the intended account. - Consider separate Terraform states for each account. While the dual-provider approach is convenient for tightly coupled resources (like attachment, label, and accept), for broader infrastructure management, separate states per account reduce blast radius and simplify access control.
- Tags
- AWS Cloud WAN
- Language
- English
Relevant content
- Accepted Answerasked 4 years ago
AWS OFFICIALUpdated 3 years ago