Provisions a dedicated Direct Connect connection (or LAG) together with its private/public/transit virtual interfaces, an optional Direct Connect gateway, and its VGW/Transit-Gateway associations — a complete, BGP-ready hybrid-network on-ramp from one module call. Built for the AWS provider v6.x.
- 🔌 One on-ramp, fully wired. Creates
aws_dx_connection(keystone) plus everything that is meaningless without it: an optional LAG, the private/public/transit virtual interfaces ridden on it, an optional Direct Connect gateway, and its VGW/TGW associations. - 🧵 VIFs ride the right target automatically. Each virtual interface attaches to the LAG when one is created, otherwise to the bare connection — overridable per VIF with
connection_id. - 🚪 Gateway fallback built in. Private/transit VIFs and gateway associations that omit an explicit
dx_gateway_idattach to the module-created DX gateway when present, so you describe intent, not plumbing. - 🗺️
for_each, nevercount. VIFs and associations are keyed by stable caller strings, so adding or removing one never re-indexes the rest. - 🔐 Secure-by-default leanings. MACsec
encryption_modeis surfaced (prefershould_encrypt/must_encryptfor PII), private/transit VIFs are preferred over public, and per-VIF BGP MD5 auth is supported. - 🏷️ Tags everywhere.
var.tagsflows to the connection, LAG, DX gateway, and every VIF and merges with providerdefault_tags; the merged set is surfaced astags_all. - 🌍 Provider-inherited Region. No
regionvariable — the caller's provider (or alias) sets the Region; the DX location code, not a Terraform argument, pins the physical facility.
💡 Why it matters: A consistent, secure-by-default Direct Connect on-ramp keeps hybrid connectivity to the estate predictable and auditable. The blast radius of a misrouted VIF or an unencrypted PII path is large — this module makes the secure shape the easy shape.
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!
tf-mod-aws-dx is a networking edge module — it consumes a VGW from tf-mod-aws-vpn or a Transit Gateway from tf-mod-aws-transit-gateway as association targets, and (when MACsec is used) a secret ARN from tf-mod-aws-secrets-manager. It originates the VIFs and DX gateway that carry on-prem traffic into the VPC fabric.
flowchart LR
vpc["tf-mod-aws-vpc<br/>VPC + subnets"]
vpn["tf-mod-aws-vpn<br/>VGW"]
tgw["tf-mod-aws-transit-gateway"]
sm["tf-mod-aws-secrets-manager<br/>MACsec CKN/CAK"]
dx["tf-mod-aws-dx"]
onprem["On-prem / colo<br/>router (BGP)"]
pub["Public AWS<br/>service endpoints"]
vpn -->|"vpn_gateway_id"| dx
tgw -->|"transit_gateway_id"| dx
sm -->|"secret arn (MACsec)"| dx
vpc -.->|"VGW attaches to VPC"| vpn
onprem ===|"BGP over cross-connect"| dx
dx -->|"public VIF"| pub
style dx fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
flowchart TD
subgraph mod["tf-mod-aws-dx"]
conn["aws_dx_connection.this<br/>(keystone)<br/>dedicated port @ DX location"]
lag["aws_dx_lag.this<br/>optional, guarded for_each"]
gw["aws_dx_gateway.this<br/>optional, guarded for_each"]
priv["aws_dx_private_virtual_interface.this<br/>for_each private_virtual_interfaces"]
pub["aws_dx_public_virtual_interface.this<br/>for_each public_virtual_interfaces"]
tran["aws_dx_transit_virtual_interface.this<br/>for_each transit_virtual_interfaces"]
assoc["aws_dx_gateway_association.this<br/>for_each gateway_associations"]
end
conn -->|"migrated into"| lag
lag -.->|"VIFs ride LAG when present"| priv
lag -.-> pub
lag -.-> tran
conn -->|"else VIFs ride connection"| priv
conn --> pub
conn --> tran
gw --> priv
gw --> tran
gw --> assoc
style conn fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
style lag stroke-dasharray: 5 5
style gw stroke-dasharray: 5 5
| Resource | Count | Created when |
|---|---|---|
aws_dx_connection.this |
1 | always (keystone) |
aws_dx_lag.this |
0 or 1 | lag != null |
aws_dx_gateway.this |
0 or 1 | dx_gateway != null |
aws_dx_private_virtual_interface.this |
0..N | one per private_virtual_interfaces entry |
aws_dx_public_virtual_interface.this |
0..N | one per public_virtual_interfaces entry |
aws_dx_transit_virtual_interface.this |
0..N | one per transit_virtual_interfaces entry |
aws_dx_gateway_association.this |
0..N | one per gateway_associations entry |
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/aws |
>= 6.0, < 7.0 |
The module declares only a required_providers block (providers.tf) and inherits the configured provider. There is no provider {} block and no credential variable — credentials resolve through the standard AWS chain at the root/pipeline level (env vars → SSO/shared credentials → assume_role → instance profile / IRSA → OIDC web identity).
Least-privilege actions the Terraform execution identity needs to manage this module.
| Action | Required for | Notes |
|---|---|---|
directconnect:CreateConnection, directconnect:DeleteConnection, directconnect:DescribeConnections |
Connection lifecycle | Keystone CRUD |
directconnect:UpdateConnection |
encryption_mode / connection attribute updates |
Only when MACsec attributes change |
directconnect:CreateLag, directconnect:DeleteLag, directconnect:DescribeLags, directconnect:AssociateConnectionWithLag |
LAG lifecycle | Only when lag is set; migrates the keystone into the LAG |
directconnect:CreatePrivateVirtualInterface, directconnect:CreatePublicVirtualInterface, directconnect:CreateTransitVirtualInterface |
VIF creation | One per VIF map entry |
directconnect:DeleteVirtualInterface, directconnect:DescribeVirtualInterfaces |
VIF lifecycle | — |
directconnect:CreateDirectConnectGateway, directconnect:DeleteDirectConnectGateway, directconnect:DescribeDirectConnectGateways |
DX gateway lifecycle | Only when dx_gateway is set |
directconnect:CreateDirectConnectGatewayAssociation, directconnect:DeleteDirectConnectGatewayAssociation, directconnect:DescribeDirectConnectGatewayAssociations |
DX gateway ↔ VGW/TGW association | Single-account path |
directconnect:AcceptDirectConnectGatewayAssociationProposal, directconnect:DescribeDirectConnectGatewayAssociationProposals |
Cross-account association | Only with proposal_id + associated_gateway_owner_account_id |
directconnect:TagResource, directconnect:UntagResource, directconnect:DescribeTags |
Tagging | Connection / LAG / DX gateway / VIFs |
ec2:DescribeVpnGateways, ec2:DescribeTransitGateways |
Resolving association targets | Read-only on VGW/TGW |
🔐 MACsec: when MACsec is enabled, the identity additionally needs
secretsmanager:GetSecretValueon the CKN/CAK secret ARN, plusdirectconnect:AssociateMacSecKeyif key association is performed (key association itself is out of scope for this module).
ℹ️ No service-linked role and no
iam:PassRoleare required by Direct Connect for the resources this module manages.
- Physical cross-connect (mandatory). A Letter of Authorization / Connecting Facility Assignment (LOA-CFA) and a provisioned cross-connect at a Direct Connect location are required before a dedicated connection becomes operational. This is arranged out of band with the colocation/carrier provider — Terraform creates only the logical connection, which sits in
requested/pendinguntil the cross-connect is completed. - No service-linked role is required for Direct Connect itself.
- DX location code.
locationis a DX facility code (e.g.EqDC2), not a Region. Discover valid codes with theaws_dx_locationsdata source orDescribeLocations. - Association targets. A VGW (
tf-mod-aws-vpn) or Transit Gateway (tf-mod-aws-transit-gateway) must exist before a DX gateway association is created. Transit VIFs require a DX gateway. - BGP. Each VIF needs a customer-side BGP ASN; public VIFs additionally require public IPv4/IPv6 prefixes you own (
route_filter_prefixes). AWS assigns the Amazon-side ASN unless overridden viadx_gateway.amazon_side_asn. - MACsec (optional). Only supported on specific dedicated port speeds/locations; CKN/CAK material lives in Secrets Manager and is associated out of band.
- Quotas (per Direct Connect quotas):
- 50 virtual interfaces per dedicated connection (adjustable).
- 2 LAGs per Region and up to 10 connections per LAG (port speeds must match within a LAG).
- 100 BGP advertised routes per private/transit VIF; 1,000 per public VIF.
- 3 DX gateways in a transit-gateway association by default; 10 TGWs/VGWs per DX gateway.
- Raise via Service Quotas where adjustable.
tf-mod-aws-dx/
├── providers.tf # required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # name → required conn args → optional conn → LAG → gateway → VIF maps → associations → tags
├── main.tf # aws_dx_connection.this + LAG / gateway / VIF for_each + associations
├── outputs.tf # id + arn + connection/LAG/gateway/VIF/association attributes + tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM permissions, prerequisites, gotchas
Smallest working call — a dedicated 1 Gbps connection with a single private VIF to an existing VGW:
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-primary"
bandwidth = "1Gbps"
location = "EqDC2"
private_virtual_interfaces = {
prod-vif = {
bgp_asn = 65000
vlan = 100
vpn_gateway_id = module.vpn.vpn_gateway_id # from tf-mod-aws-vpn
}
}
tags = {
Environment = "prod"
CostCenter = "1234"
}
}| Input | Type | Source module |
|---|---|---|
private_virtual_interfaces[*].vpn_gateway_id |
string (VGW id) |
tf-mod-aws-vpn |
gateway_associations[*].associated_gateway_id |
string (VGW or TGW id) |
tf-mod-aws-vpn / tf-mod-aws-transit-gateway |
*.dx_gateway_id (when using an external gateway) |
string |
this module / external |
| MACsec CKN/CAK secret (by ARN, out of band) | string (Secrets Manager ARN) |
tf-mod-aws-secrets-manager |
Networking-edge module — it needs no sibling output to create the connection itself; association targets are wired in only when you attach a DX gateway to a VGW/TGW.
| Output | Description | Consumed by |
|---|---|---|
id |
DX connection id (dxcon-…) |
VIF wiring, monitoring |
arn |
DX connection ARN — the cross-resource reference type | IAM policies, tagging audits |
connection_id / connection_arn / connection_name |
Connection aliases | LAG / VIF references |
aws_device |
DX endpoint device the port terminates on | resiliency planning |
jumbo_frame_capable / macsec_capable / has_logical_redundancy |
Connection capability flags | MTU / MACsec / redundancy decisions |
port_encryption_status |
MACsec port link encryption status | PII posture verification |
owner_account_id |
Account that owns the connection | cross-account sharing |
vif_target_id |
LAG id when present, else connection id (what VIFs ride) | wiring checks |
lag_id / lag_arn |
LAG identifiers when a LAG is created; else null |
connection association |
dx_gateway_id / dx_gateway_arn / dx_gateway_owner_account_id |
DX gateway identifiers when created; else null |
TGW/VGW associations, cross-account sharing |
private_virtual_interface_ids / public_… / transit_… |
Per-type map of VIF key → id | route propagation, monitoring |
virtual_interface_ids / virtual_interface_arns |
Combined map of all VIF key → id / ARN | route propagation, monitoring |
gateway_association_ids |
Map of association key → association id | drift / inspection |
gateway_association_states |
Map of association key → associated gateway type (transitGateway / virtualPrivateGateway) |
operational verification |
transit_gateway_attachment_ids |
Map of association key → TGW attachment id (transitGateway only) | TGW route-table association |
tags_all |
All tags incl. provider default_tags (resource tags win) |
governance/audit |
1 · Private VIF to a VGW (from tf-mod-aws-vpn)
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-vgw"
bandwidth = "1Gbps"
location = "EqDC2"
private_virtual_interfaces = {
prod = {
bgp_asn = 65000
vlan = 100
vpn_gateway_id = module.vpn.vpn_gateway_id
}
}
}2 · DX gateway + private VIF (auto-attach to the module gateway)
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-dxgw"
bandwidth = "10Gbps"
location = "EqDC2"
dx_gateway = {
name = "casey-dxgw"
amazon_side_asn = "64512"
}
# Omits both vpn_gateway_id and dx_gateway_id → attaches to the module's DX gateway.
private_virtual_interfaces = {
prod = {
bgp_asn = 65000
vlan = 101
}
}
}3 · Transit VIF to a Transit Gateway (from tf-mod-aws-transit-gateway)
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-transit"
bandwidth = "10Gbps"
location = "EqDC2"
dx_gateway = {
name = "casey-dxgw-transit"
amazon_side_asn = "64512"
}
transit_virtual_interfaces = {
prod = {
bgp_asn = 65000
vlan = 200
# dx_gateway_id omitted → uses the module's DX gateway
}
}
gateway_associations = {
tgw = {
associated_gateway_id = module.tgw.transit_gateway_id # from tf-mod-aws-transit-gateway
allowed_prefixes = ["10.0.0.0/8"]
}
}
}4 · Public VIF (advertise owned public prefixes)
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-public"
bandwidth = "1Gbps"
location = "EqDC2"
# Public VIFs reach public AWS endpoints; prefer private/transit for PII traffic.
public_virtual_interfaces = {
edge = {
bgp_asn = 65000
vlan = 300
route_filter_prefixes = ["203.0.113.0/24"] # public CIDRs you own
}
}
}5 · LAG bundling dedicated connections
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-lag-member"
bandwidth = "10Gbps"
location = "EqDC2"
lag = {
name = "casey-dx-lag"
connections_bandwidth = "10Gbps"
location = "EqDC2"
# connection_id defaults to the keystone connection (migrated into the LAG)
}
# VIFs ride the LAG automatically once it exists.
private_virtual_interfaces = {
prod = {
bgp_asn = 65000
vlan = 100
vpn_gateway_id = module.vpn.vpn_gateway_id
}
}
}6 · Tags (merge with provider default_tags)
# Caller's provider block owns default_tags; the module never sets it.
provider "aws" {
default_tags { tags = { Owner = "platform", ManagedBy = "terraform" } }
}
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-tagged"
bandwidth = "1Gbps"
location = "EqDC2"
tags = {
Environment = "prod" # resource tag — wins over default_tags on key conflict
DataClass = "internal"
}
}
# module.dx.tags_all == { Owner, ManagedBy, Environment, DataClass }7 · Per-resource tag overrides (connection vs LAG vs VIF)
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-scoped-tags"
bandwidth = "10Gbps"
location = "EqDC2"
tags = { Environment = "prod" } # module-wide
lag = {
name = "casey-dx-lag"
connections_bandwidth = "10Gbps"
location = "EqDC2"
tags = { Tier = "transport" } # merged over module tags on the LAG
}
private_virtual_interfaces = {
prod = {
bgp_asn = 65000
vlan = 100
vpn_gateway_id = module.vpn.vpn_gateway_id
tags = { Tier = "data-plane" } # merged over module tags on this VIF
}
}
}8 · MACsec-capable connection with secure encryption mode
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-macsec"
bandwidth = "10Gbps"
location = "EqDC2"
request_macsec = true # FORCE-NEW — only on supported speeds/locations
encryption_mode = "should_encrypt" # use "must_encrypt" to hard-fail unencrypted traffic
# CKN/CAK association (aws_dx_macsec_key_association + Secrets Manager) is out of
# scope for this module — wire it from tf-mod-aws-secrets-manager out of band.
}9 · Jumbo frames + per-VIF BGP MD5 auth
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-jumbo"
bandwidth = "10Gbps"
location = "EqDC2"
private_virtual_interfaces = {
prod = {
bgp_asn = 65000
vlan = 100
vpn_gateway_id = module.vpn.vpn_gateway_id
mtu = 9001 # jumbo frames (private VIF: 1500 | 9001)
bgp_auth_key = var.bgp_md5 # supply via a sensitive variable, not literal
amazon_address = "169.254.0.1/30"
customer_address = "169.254.0.2/30"
}
}
}10 · IPv6 private VIF
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-v6"
bandwidth = "1Gbps"
location = "EqDC2"
private_virtual_interfaces = {
prod-v6 = {
bgp_asn = 65000
vlan = 110
address_family = "ipv6" # AWS auto-assigns peer addresses for IPv6
vpn_gateway_id = module.vpn.vpn_gateway_id
}
}
}11 · SiteLink-enabled transit VIF
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-sitelink"
bandwidth = "10Gbps"
location = "EqDC2"
dx_gateway = {
name = "casey-dxgw-sl"
amazon_side_asn = "64512"
}
transit_virtual_interfaces = {
prod = {
bgp_asn = 65000
vlan = 250
sitelink_enabled = true # PoP-to-PoP connectivity over the DX network
mtu = 8500 # transit VIF jumbo: 1500 | 8500
}
}
}12 · Cross-account DX gateway association (accept a proposal)
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-xacct"
bandwidth = "10Gbps"
location = "EqDC2"
dx_gateway = {
name = "casey-dxgw-shared"
amazon_side_asn = "64512"
}
# The TGW/VGW owner (other account) creates a proposal; this side accepts it.
gateway_associations = {
partner-tgw = {
proposal_id = "abcd1234-..."
associated_gateway_owner_account_id = "111122223333"
allowed_prefixes = ["10.20.0.0/16"]
}
}
}13 · connection_id override (VIF on a specific port)
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-override"
bandwidth = "10Gbps"
location = "EqDC2"
private_virtual_interfaces = {
on-existing = {
bgp_asn = 65000
vlan = 120
connection_id = "dxcon-existingid" # ride a pre-existing connection, not the keystone
vpn_gateway_id = module.vpn.vpn_gateway_id
}
}
}14 · skip_destroy (preserve the physical port on teardown)
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-keep-port"
bandwidth = "10Gbps"
location = "EqDC2"
# On destroy, removed from state but NOT deleted in AWS (physical cross-connect kept).
skip_destroy = true
}15 · End-to-end composition — hybrid on-ramp (VPC + VPN/VGW + TGW + DX)
# Network foundation
module "vpc" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
name = "casey-core"
cidr_block = "10.0.0.0/16"
}
# Transit Gateway hub
module "tgw" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-transit-gateway?ref=v1.0.0"
name = "casey-hub"
}
# MACsec secret (CKN/CAK) — associated out of band
module "macsec" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-secrets-manager?ref=v1.0.0"
name = "casey/dx/macsec"
}
# This module — connection + DX gateway + transit VIF + TGW association
module "dx" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dx?ref=v1.0.0"
name = "casey-dx-prod"
bandwidth = "10Gbps"
location = "EqDC2"
request_macsec = true
encryption_mode = "should_encrypt"
dx_gateway = {
name = "casey-dxgw-prod"
amazon_side_asn = "64512"
}
transit_virtual_interfaces = {
prod = {
bgp_asn = 65000
vlan = 200
}
}
gateway_associations = {
hub = {
associated_gateway_id = module.tgw.transit_gateway_id
allowed_prefixes = [module.vpc.cidr_block]
}
}
tags = { Environment = "prod", DataClass = "internal" }
}
# Wire downstream: associate module.dx.transit_gateway_attachment_ids["hub"]
# with the appropriate TGW route table in the hub.| Name | Type | Default | Description |
|---|---|---|---|
name |
string |
— required | Connection display name (keystone). |
bandwidth |
string |
— required | Port speed (e.g. 1Gbps, 10Gbps). FORCE-NEW. |
location |
string |
— required | DX location code (e.g. EqDC2). FORCE-NEW. |
provider_name |
string |
null |
Carrier/colocation provider name. |
request_macsec |
bool |
false |
Request a MACsec-capable connection. FORCE-NEW. |
encryption_mode |
string |
null |
no_encrypt / should_encrypt / must_encrypt (MACsec-capable, when available). |
skip_destroy |
bool |
false |
Remove from state on destroy without deleting in AWS. |
lag |
object({…}) |
null |
Optional LAG; the keystone connection is migrated into it. |
dx_gateway |
object({ name, amazon_side_asn, tags }) |
null |
Optional Direct Connect gateway (private/transit fallback target). |
private_virtual_interfaces |
map(object({…})) |
{} |
Private VIFs to a VGW / DX gateway, keyed by caller string. |
public_virtual_interfaces |
map(object({…})) |
{} |
Public VIFs (require route_filter_prefixes). |
transit_virtual_interfaces |
map(object({…})) |
{} |
Transit VIFs (require a DX gateway). |
gateway_associations |
map(object({…})) |
{} |
DX gateway ↔ VGW/TGW associations (single- or cross-account). |
tags |
map(string) |
{} |
Tags for all taggable resources (merge with default_tags). |
See variables.tf for full heredoc schemas, per-VIF fields, timeouts, and validation rules.
| Name | Description |
|---|---|
id |
Connection id (dxcon-…). |
arn |
Connection ARN (cross-resource reference type). |
connection_id / connection_arn / connection_name |
Connection aliases. |
aws_device |
DX endpoint device the port terminates on. |
jumbo_frame_capable / macsec_capable / has_logical_redundancy |
Connection capability flags. |
port_encryption_status |
MACsec port link encryption status. |
owner_account_id |
Account that owns the connection. |
vif_target_id |
LAG id when present, else connection id (what VIFs ride). |
lag_id / lag_arn |
LAG identifiers (or null). |
dx_gateway_id / dx_gateway_arn / dx_gateway_owner_account_id |
DX gateway identifiers (or null). |
private_virtual_interface_ids / public_virtual_interface_ids / transit_virtual_interface_ids |
Per-type VIF id maps. |
virtual_interface_ids / virtual_interface_arns |
Combined VIF id / ARN maps. |
gateway_association_ids / gateway_association_states / transit_gateway_attachment_ids |
Association attributes. |
tags_all |
All tags incl. provider default_tags. |
- ARN / ID formats:
- Connection id:
dxcon-xxxxxxxx; ARNarn:aws:directconnect:<region>:<account>:dxcon/<id>. - LAG id:
dxlag-xxxxxxxx; DX gateway id is a UUID (account-global, no Region segment in the resource id). - VIF id:
dxvif-xxxxxxxx; ARNarn:aws:directconnect:<region>:<account>:dxvif/<id>. arnis the cross-resource reference type throughout AWS — prefer it for IAM/policy references.- Force-new / immutable fields: connection
bandwidth,location,request_macsec; LAGconnections_bandwidth,location; DX gatewayamazon_side_asn; VIFvlan,bgp_asn,address_family. Changing any destroys and recreates the resource — and for the connection that means a new physical cross-connect. Plan port speed and location up front. - VIF target resolution: VIFs ride the LAG (
local.vif_connection_id) when a LAG is created, otherwise the keystone connection; a per-VIFconnection_idoverrides. Private/transit VIFs and associations that omitdx_gateway_idfall back to the module-created DX gateway (local.dx_gateway_id) when present. tags↔tags_all↔default_tags:var.tagsis applied to each taggable resource (child collections merge per-item tags overvar.tags);tags_allis the provider-computed merge of resource tags over providerdefault_tags, with resource tags winning on key conflict.default_tagsis configured in the caller's provider block — never inside this module. Gateway associations are not taggable in AWS.- Asynchronous provisioning / eventual consistency: a new connection is created in
requested/pendingand only becomesavailableafter the physical cross-connect is completed and accepted —applydoes not wait for the cross-connect. VIFs depend on a connection that may still be pending; BGP sessions only come up once on-prem routing is configured. - Destroy ordering: VIFs and gateway associations must be deleted before the connection / DX gateway. Terraform handles this via implicit dependencies, but cross-account associations and proposals can lag — allow time and re-run if a delete reports the gateway still in use. With
skip_destroy = true, the connection is dropped from state but the physical port is preserved. - us-east-1 globals: N/A. Direct Connect is a regional service tied to a DX location; it has none of the CloudFront/WAFv2/ACM us-east-1 coupling. Region comes from the provider, not a variable.
Secure-by-default posture and every opt-out, explicitly:
| Posture | Default | Opt-out |
|---|---|---|
| MACsec encryption (where supported) | prefer encryption_mode = "should_encrypt" / "must_encrypt" for PII when MACsec is requested |
encryption_mode = "no_encrypt" (discouraged) or leave MACsec off (request_macsec = false) |
| VIF type for PII | private / transit VIFs preferred (stay on the AWS backbone) | configure a public_virtual_interfaces entry explicitly |
| BGP authentication | per-VIF bgp_auth_key (MD5) supported |
omit → AWS generates a key |
| Jumbo frames | provider default (1500) unless set |
mtu = 9001 (private) / 8500 (transit) |
| Connection deletion | normal destroy deletes the logical connection | skip_destroy = true preserves the physical port |
| One target per private VIF | validation forbids setting both vpn_gateway_id and dx_gateway_id |
choose exactly one (or rely on the DX-gateway fallback) |
⚠️ Direct Connect transit is not encrypted by default. The cross-connect is private but plaintext unless MACsec is enabled or traffic rides an encrypted overlay (VPN over DX / Transit Gateway). For PII/privacy-regulation traffic, layer encryption-in-transit at the application or VPN layer, or use MACsec where the port/location supports it. Document anyno_encryptchoice in your root module so reviewers can see what was loosened.
Other principles:
- One composite, one keystone.
aws_dx_connection.thisis the keystone; the module owns only what is meaningless without it (LAG migration, VIFs, optional DX gateway, associations). VGW/TGW targets are referenced by id from sibling modules, keeping blast radius to the DX logical objects. for_each, nevercount, for VIFs and associations — keyed by stable caller strings so reorders don't churn the plan.- Primary outputs
id+arn, plus the connection capability flags, VIF/association maps, andtags_all.
# Validate without backend or credentials
terraform init -backend=false
terraform validate
terraform fmt -check
plan/applyrequire valid AWS credentials (profile / SSO / OIDC) resolved through the standard provider chain, a configured Region, and the Direct Connect / EC2 actions listed above. A new connection becomes usable only after the physical cross-connect (LOA-CFA) is completed out of band —applywill succeed while the connection is stillpending.
terraform init -backend=false && terraform validate— schema + reference integrity.terraform fmt -check— canonical formatting.terraform planagainst a sandbox account to confirm the connection, VIF maps, optional LAG/gateway, and associations materialize as expected (note: the connection will plan/apply aspendinguntil the cross-connect exists).- Assert
module.<name>.arn,virtual_interface_ids,dx_gateway_id, andtags_allin your root-module test harness.
module.dx.aws_dx_connection.this: Creation complete after 4s [id=dxcon-fgabc123]
module.dx.aws_dx_gateway.this["this"]: Creation complete [id=abcd1234-...]
module.dx.aws_dx_transit_virtual_interface.this["prod"]: Creation complete [id=dxvif-fg0a1b2c]
module.dx.aws_dx_gateway_association.this["hub"]: Creation complete
Outputs:
arn = "arn:aws:directconnect:us-east-1:123456789012:dxcon/dxcon-fgabc123"
id = "dxcon-fgabc123"
dx_gateway_id = "abcd1234-ef56-..."
virtual_interface_ids = { "prod" = "dxvif-fg0a1b2c" }
transit_gateway_attachment_ids = { "hub" = "tgw-attach-0a1b2c3d" }
tags_all = { "DataClass" = "internal", "Environment" = "prod" }
| Symptom | Likely cause | Fix |
|---|---|---|
Connection stuck in requested / pending |
Physical cross-connect (LOA-CFA) not yet completed at the DX location | Complete the cross-connect with the colocation/carrier; apply does not wait for it |
Each private VIF may set at most one of vpn_gateway_id or dx_gateway_id |
Both targets set on one private VIF | Set exactly one, or omit both to use the module DX-gateway fallback |
| Transit VIF apply fails — no gateway | Transit VIFs require a DX gateway | Create one via dx_gateway (auto-attach) or pass dx_gateway_id |
bandwidth / location change forces replacement |
Both are FORCE-NEW on the connection | Expect a destroy/recreate and a new cross-connect; plan capacity up front |
| BGP session down after apply | On-prem router / BGP peering not configured, or VIF still pending | Configure the customer side; confirm amazon_address/customer_address and bgp_auth_key match |
| Public VIF rejected | route_filter_prefixes not public CIDRs you own / not advertisable |
Advertise only owned, public prefixes; private VIFs don't use route filters |
| Tag drift on every plan | A tag also set by provider default_tags with a different value |
Let resource tags win, or remove the overlap from default_tags |
| LAG won't destroy | Member connections still attached | Set lag.force_destroy = true (deletes members — not recoverable) |
| Cross-account association won't create | Missing/expired proposal or wrong owner account | Recreate the proposal in the target account; set proposal_id + associated_gateway_owner_account_id |
AccessDenied on directconnect:* |
Terraform identity missing the actions above | Grant the least-privilege action set; add secretsmanager:GetSecretValue for MACsec |
| Destroy reports gateway "still in use" | Association/VIF delete lagging (eventual consistency) | Re-run destroy after VIFs/associations clear |
- Direct Connect quotas
- Direct Connect virtual interfaces
- Direct Connect resiliency toolkit
- Direct Connect gateways
- MACsec on Direct Connect
- Terraform:
aws_dx_connection·aws_dx_lag·aws_dx_gateway·aws_dx_private_virtual_interface·aws_dx_transit_virtual_interface·aws_dx_gateway_association - Sibling modules:
tf-mod-aws-vpn,tf-mod-aws-transit-gateway,tf-mod-aws-vpc,tf-mod-aws-secrets-manager - Module internals:
SCOPE.md
🧡 "Infrastructure as Code should be standardized, consistent, and secure."