AWS Level: 400 — Advanced | Multi-account networking, VPC Lattice Resource Configurations, IaC orchestration with Terragrunt
🎯 The Challenge
Scenario
Suppose you are a Cloud Architect or DevOps Engineer at an enterprise organization, and you receive this mission: expose an Aurora PostgreSQL database to an external AWS organization — without VPC Peering, without Transit Gateway, without public endpoints, and without deploying a single NLB.
The requirements are clear:
- ✅ Zero internet exposure — the database must remain in private subnets
- ✅ Cross-account, cross-organization — the consumer lives in a completely separate AWS org
- ✅ Infrastructure as Code — everything must be declared, versioned, and reproducible
- ✅ Network validation — you must prove connectivity works before handing it off
- ✅ Least privilege — security groups scoped to exact ports and sources
- ✅ Multi-environment — same code for dev, qa, and prod with different configurations
Welcome to NetDevOps.
Cloud Networking at Scale is Broken (And We Keep Pretending It's Not)
I've been working with multi-account AWS architectures for years, and there's one conversation that keeps coming back like a boomerang: "How do we give that other team access to our database without blowing up our network?"
Every. Single. Time.
And the answers are always the same tired playbook: VPC Peering (good luck with overlapping CIDRs), Transit Gateway (hope you enjoy $0.05/GB bills), PrivateLink (another NLB to maintain). They all work, technically. But they all introduce complexity that compounds over time.
Here's the thing — cloud networking at scale isn't hard because any single concept is difficult. It's hard because everything multiplies. One VPC? Easy. Ten with peering? Fine. Fifty accounts across business units with different compliance requirements, connectivity patterns, and team boundaries? That's where traditional approaches collapse.
I'm talking about:
- Security group sprawl where nobody can tell which rules are actually needed
- Route table entropy that breaks production with one wrong entry
- Cross-account connectivity patterns that multiply with every new team
- Connectivity validation that only happens after the incident
This is exactly why I've been investing time in NetDevOps — and today I want to show you how I solved the challenge above.
What is NetDevOps?
Before jumping into the implementation, let me share what NetDevOps means in practice (and why I think every cloud engineer should care).
NetDevOps applies DevOps principles to network infrastructure. That's it. But the implications are massive:
- Networks declared as code — not clicked together in a console
- Connectivity tested before deployment — not validated after outages
- Changes reviewed as pull requests — not approved in 3-hour CAB meetings
- Configurations reproducible across environments — not buried in wiki pages that nobody updates
In AWS terms? It means treating VPCs, security groups, VPC Lattice configs, and connectivity paths as first-class IaC artifacts. With real dependency management. With real tests. With real CI/CD.
And the tool I keep reaching for to orchestrate all of this? Terragrunt + OpenTofu. The combination gives me layered dependencies, environment promotion, and DRY configurations without losing visibility into what's actually deployed.
My Approach to the Challenge
So how do we solve this mission? I evaluated the traditional options and rejected all of them:
| Approach | Why I Said No |
|---|---|
| VPC Peering | Non-transitive, CIDR overlap risk, routing table management nightmare |
| Transit Gateway | Expensive at scale, shared blast radius, route table limits |
| PrivateLink + NLB | An NLB for every resource? Port management? No thanks. |
| Database replication | Double cost, consistency challenges, operational overhead |
| Public endpoint | 🙈 Let's not even go there |
Then I remembered that AWS had released VPC Lattice Resource Configurations with ARN-based targeting. I'd been keeping an eye on it since re:Invent, and this was the perfect use case to test it.
Spoiler: it worked beautifully. And when I combined it with VPC Reachability Analyzer for automated validation, I had a full NetDevOps pipeline for cross-account connectivity.
The Architecture
This is what we're building. Three accounts, one region, zero internet exposure:
| Account | Role | What Lives There |
|---|---|---|
| External Account (ours) | Hosts the database + VPC Lattice | VPC, Aurora PostgreSQL, Resource Gateway, Service Network, RAM Share |
| Network Account | Owns the Lattice mesh | Service Network, Resource Configuration |
| Workload Account (consumer) | External org that needs DB access | Applications connecting via VPC Lattice endpoint |
The key components:
- VPC Lattice Service Network — the connectivity mesh
- Resource Gateway — network path into our VPC (deployed in DB subnets)
- Resource Configuration (ARN-based) — points to the Aurora cluster by ARN
- RAM Resource Share — shares the Service Network cross-account
- VPC Reachability Analyzer — proves the path works before anyone complains
How I Organized the Code
I'm a firm believer in layered architecture for IaC. If you've read my previous posts on Terragrunt, you know I follow a DDD-inspired approach where each layer has clear boundaries and dependencies flow in one direction:
stacks/
├── foundation/
│ └── network/
│ ├── vpc/ # VPC + database subnets only
│ └── security-groups/ # SGs for RDS, Lattice RGW, VPC association
├── platform/
│ ├── data/
│ │ └── rds/ # Aurora PostgreSQL cluster
│ └── network/
│ └── vpc-lattice/ # Service Network + Resource Gateway + RAM
└── observability/
└── network/
└── reachability-analyzer/ # Network path validation (the NetDevOps piece!)
Foundation → Platform → Observability. Never backwards. Dependencies are explicit in every terragrunt.hcl.
Why does this matter? Because when someone changes a security group rule, I can immediately see which platform and observability stacks will be affected. No surprises.
Hands-On: Let's Build It
Alright, enough theory. Let's get our hands dirty. I'll walk you through each layer, explain my decisions, and show you the actual code running in this project.
Prerequisites:
- OpenTofu or Terraform >= 1.5
- Terragrunt >= 0.98
- AWS CLI v2 configured with appropriate profiles
- An S3 bucket + DynamoDB table for remote state
Step 1: The VPC (Minimal Attack Surface)
Here's something I feel strongly about: if a VPC only hosts a database, it should only have database subnets. No public subnets. No internet gateway. No NAT. Nothing that doesn't need to be there.
# stacks/foundation/network/vpc/main.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "6.6.0"
name = "${var.project}-${var.environment}-vpc"
cidr = var.vpc_cidr
azs = var.availability_zones
database_subnets = var.database_subnets
create_database_subnet_group = true
create_database_subnet_route_table = true
create_igw = false
enable_nat_gateway = false
manage_default_security_group = true
default_security_group_ingress = []
default_security_group_egress = []
enable_dns_hostnames = true
enable_dns_support = true
tags = var.tags
}
I love this. Zero internet exposure by design. The default security group is locked down with empty rules. DNS is enabled because VPC Lattice needs it for resolution. That's it — nothing more, nothing less.
Step 2: Security Groups (This Is Where It Gets Interesting)
Most teams I work with define security groups with CIDR blocks. That works until subnets change, IP ranges shift, or someone adds a new AZ. I prefer security group references — they create a logical coupling that survives all those changes:
# stacks/foundation/network/security-groups/main.tf
# RDS only accepts traffic from the Lattice Resource Gateway
module "rds_sg" {
source = "terraform-aws-modules/security-group/aws"
version = "6.0.0"
name = "${var.project}-${var.environment}-rds"
description = "Security group for RDS Aurora cluster"
vpc_id = var.vpc_id
ingress_rules = {
postgresql_from_lattice = {
description = "Allow PostgreSQL from VPC Lattice resource gateway"
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
referenced_security_group_id = module.lattice_resource_gateway_sg.id
}
}
tags = merge(var.tags, { Name = "${var.project}-${var.environment}-rds-sg" })
}
# Resource Gateway can only talk to RDS
module "lattice_resource_gateway_sg" {
source = "terraform-aws-modules/security-group/aws"
version = "6.0.0"
name = "${var.project}-${var.environment}-lattice-rgw"
description = "Security group for VPC Lattice Resource Gateway"
vpc_id = var.vpc_id
egress_rules = {
to_rds = {
description = "Allow PostgreSQL to RDS"
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
referenced_security_group_id = module.rds_sg.id
}
}
tags = merge(var.tags, { Name = "${var.project}-${var.environment}-lattice-rgw-sg" })
}
See what's happening here? The Resource Gateway can only reach RDS on port 5432. And RDS only accepts connections from the Resource Gateway. This is defense in depth — even if someone compromises the VPC, lateral movement is blocked by design.
Step 3: Aurora PostgreSQL
Nothing revolutionary here — I'm using the official terraform-aws-modules/rds-aurora module because it handles all the complexity of parameter groups, subnet groups, and monitoring configuration:
# stacks/platform/data/rds/main.tf
module "aurora" {
source = "terraform-aws-modules/rds-aurora/aws"
version = "10.2.0"
name = "${var.project}-${var.environment}-aurora"
engine = "aurora-postgresql"
engine_version = var.engine_version
master_username = var.master_username
vpc_id = var.vpc_id
db_subnet_group_name = var.db_subnet_group_name
create_security_group = false
vpc_security_group_ids = [var.rds_security_group_id]
storage_encrypted = true
apply_immediately = true
cluster_monitoring_interval = 60
instances = var.instances
backup_retention_period = 7
deletion_protection = var.deletion_protection
skip_final_snapshot = var.environment != "prod"
tags = var.tags
}
# -----------------------------------------------------------------------------
# Discover the RDS writer ENI for downstream reachability analysis
# This runs in the same stack where the cluster is created, so ENI always exists
# -----------------------------------------------------------------------------
data "aws_network_interfaces" "rds_writer" {
filter {
name = "group-id"
values = [var.rds_security_group_id]
}
filter {
name = "status"
values = ["in-use"]
}
depends_on = [module.aurora]
}
The important detail: create_security_group = false. I pass in the security group from the foundation layer. This keeps ownership clear — the network team controls access rules, the data team controls database configuration.
And here's how Terragrunt wires the dependencies:
# stacks/platform/data/rds/terragrunt.hcl
dependency "vpc" {
config_path = "../../../foundation/network/vpc"
mock_outputs = {
vpc_id = "vpc-mock-12345"
database_subnet_group_name = "mock-db-subnet-group"
}
mock_outputs_merge_strategy_with_state = "shallow"
}
dependency "security_groups" {
config_path = "../../../foundation/network/security-groups"
mock_outputs = {
rds_security_group_id = "sg-mock-rds"
}
mock_outputs_merge_strategy_with_state = "shallow"
}
Those mock_outputs are critical — they let you run terragrunt plan on individual stacks without deploying everything first. Essential for fast feedback loops.
Step 4: VPC Lattice (Here's Where the Magic Happens)
OK, this is the part I'm most excited about. VPC Lattice with DNS-based Resource Configurations is genuinely elegant. Let me show you:
# modules/vpc-lattice/main.tf
# 1. Create the Service Network (the mesh)
resource "aws_vpclattice_service_network" "this" {
name = var.service_network_name
auth_type = var.service_network_auth_type
tags = merge(var.tags, { Name = var.service_network_name })
}
# 2. Associate our VPC to the mesh
resource "aws_vpclattice_service_network_vpc_association" "this" {
vpc_identifier = var.vpc_id
service_network_identifier = aws_vpclattice_service_network.this.id
security_group_ids = var.vpc_association_security_group_ids
}
# 3. Deploy the Resource Gateway (network path into our VPC)
resource "aws_vpclattice_resource_gateway" "this" {
name = var.resource_gateway_name
vpc_id = var.vpc_id
subnet_ids = var.resource_gateway_subnet_ids
security_group_ids = var.resource_gateway_security_group_ids
ip_address_type = "IPV4"
}
# 4. Create the Resource Configuration — DNS + custom domain!
resource "aws_vpclattice_resource_configuration" "this" {
name = var.resource_config_name
resource_gateway_identifier = aws_vpclattice_resource_gateway.this.id
custom_domain_name = var.custom_domain_name
port_ranges = ["5432"]
protocol = "TCP"
resource_configuration_definition {
dns_resource {
domain_name = var.resource_dns_name # Aurora cluster endpoint
ip_address_type = "IPV4"
}
}
}
# 5. Associate the resource to the service network
resource "aws_vpclattice_service_network_resource_association" "this" {
resource_configuration_identifier = aws_vpclattice_resource_configuration.this.id
service_network_identifier = aws_vpclattice_service_network.this.id
}
That's it. Five resources and you've exposed an RDS cluster to a service mesh with a custom private DNS name that consumers can use. No NLBs. No PrivateLink endpoints. No route tables.
⚠️ Important lesson learned: VPC Lattice
custom_domain_nameis only supported on DNS-type and IP-type resource configurations — NOT on ARN-type. If you trytype = "ARN"withcustom_domain_name, the API returnsValidationException: Unexpected attribute customDomainName for type ARN. That's why we usedns_resourcepointing to the Aurora cluster endpoint, which gives us the custom domain capability while still routing to the database correctly.
The custom_domain_name is key here — instead of consumers connecting to an auto-generated Lattice DNS endpoint, they connect using something like db.dev.internal.netdevops.com. This makes migration transparent and application configs clean.
Cross-Account Sharing with RAM (and Resharing!)
In our architecture, we don't share directly with every consumer. Instead, we share the Service Network with a Network Account that acts as a hub — and that account reshares with downstream workload accounts:
External Account → Network Account (reshare enabled) → Workload Accounts
The magic is in allow_external_principals. By enabling it, the Network Account can accept the share and propagate access to downstream consumers. AWS automatically assigns the appropriate VPC Lattice permission to the share:
resource "aws_ram_resource_share" "this" {
count = var.ram_share_enabled ? 1 : 0
name = var.ram_share_name
allow_external_principals = true
}
resource "aws_ram_resource_association" "service_network" {
count = var.ram_share_enabled ? 1 : 0
resource_arn = aws_vpclattice_service_network.this.arn
resource_share_arn = aws_ram_resource_share.this[0].arn
}
resource "aws_ram_principal_association" "this" {
count = var.ram_share_enabled ? length(var.ram_external_principals) : 0
principal = var.ram_external_principals[count.index]
resource_share_arn = aws_ram_resource_share.this[0].arn
}
⚠️ Gotcha: You might find blog posts suggesting you pass
permission_arnswith anAllowReshareARN to enable resharing. In practice, this can fail withInvalidParameterExceptiondepending on your region/account. Let AWS assign the default permission automatically — it handles the reshare capability through the RAM console or API when the Network Account accepts and reshares.
Why this pattern? Because in enterprise organizations, the team that owns the database shouldn't need to know who is consuming it. That's the Network Account's job — they control the mesh, they approve consumers, they manage the blast radius. Separation of concerns applied to network sharing.
Step 5: Network Testing as Code (My Favorite Part)
Here's where NetDevOps really comes together. Most teams deploy network infrastructure and then... hope it works. Maybe someone runs a telnet from an EC2 instance to check connectivity. Maybe they wait for an application error.
I wanted something better. I wanted a test that runs as part of the infrastructure deployment and tells me: "yes, traffic can flow from the Resource Gateway to RDS on port 5432."
Enter VPC Reachability Analyzer:
# modules/reachability-analyzer/main.tf
# Define the path we want to test
resource "aws_ec2_network_insights_path" "lattice_to_rds" {
count = var.enabled ? 1 : 0
source = var.resource_gateway_eni_id
destination = var.rds_endpoint_network_interface_id
destination_port = var.destination_port
protocol = "tcp"
tags = merge(var.tags, { Name = "${var.name_prefix}-lattice-to-rds" })
}
# Run the analysis
resource "aws_ec2_network_insights_analysis" "lattice_to_rds" {
count = var.enabled ? 1 : 0
network_insights_path_id = aws_ec2_network_insights_path.lattice_to_rds[0].id
wait_for_completion = true
tags = merge(var.tags, { Name = "${var.name_prefix}-lattice-to-rds-analysis" })
}
Notice the var.enabled guard. This is critical — the Reachability Analyzer requires real ENI IDs (not mocks). On first deployment, the upstream stacks (RDS, VPC Lattice) may not have provisioned their ENIs yet. The enabled flag skips creation when ENIs are null, preventing the InvalidParameterValue: eni-mock-xxx is not a supported Source type error you'd otherwise get. On the second apply, real ENI IDs flow through and the analysis runs.
- The RDS stack discovers its own writer ENI and outputs
rds_writer_eni_id - The VPC Lattice stack discovers its Resource Gateway ENI and outputs
resource_gateway_eni_id - The Reachability Analyzer receives both as inputs
This enables one-shot layer-by-layer deployment. During terragrunt plan, Terragrunt provides mock ENI values so the plan succeeds without deployed resources. During apply, real ENI IDs flow through the dependency chain:
# stacks/observability/network/reachability-analyzer/terragrunt.hcl
dependency "rds" {
config_path = "../../../platform/data/rds"
mock_outputs = {
rds_writer_eni_id = "eni-mock-rds-writer"
}
mock_outputs_merge_strategy_with_state = "shallow"
}
dependency "vpc_lattice" {
config_path = "../../../platform/network/vpc-lattice"
mock_outputs = {
resource_gateway_eni_id = "eni-mock-lattice-rgw"
}
mock_outputs_merge_strategy_with_state = "shallow"
}
inputs = {
resource_gateway_eni_id = dependency.vpc_lattice.outputs.resource_gateway_eni_id
rds_writer_eni_id = dependency.rds.outputs.rds_writer_eni_id
}
This is the network equivalent of a unit test. The output tells me:
- ✅
path_found = true→ Network path is confirmed, all good - ❌
path_found = false+explanations→ Exactly which component is blocking traffic
And because it's a Terraform resource, if someone changes a security group rule or modifies a NACL, the next terragrunt apply will re-run the analysis and catch the regression. Automated network regression testing. That's NetDevOps.
🔄 Terragrunt Orchestration
The entire stack deploys with dependency resolution:
# Deploy everything in dependency order
export TF_WORKSPACE=dev
terragrunt run --all apply
Terragrunt resolves this graph automatically:
foundation/network/vpc
↓
foundation/network/security-groups
↓
platform/data/rds ←→ platform/network/vpc-lattice
↓
observability/network/reachability-analyzer
And environment-specific values live in tfvars files:
# environments/dev/foundations.tfvars
vpc_cidr = "10.100.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b"]
database_subnets = ["10.100.21.0/24", "10.100.22.0/24"]
# environments/dev/platform.tfvars
engine_version = "16.6"
instance_class = "db.t4g.medium"
instances = {
writer = { instance_class = "db.t4g.medium" }
}
service_network_name = "netdevops-external-dev"
resource_gateway_name = "netdevops-rds-rgw-dev"
resource_config_name = "netdevops-rds-config-dev"
custom_domain_name = "db.dev.internal.netdevops.com"
ram_share_name = "netdevops-rds-share-dev"
ram_external_principals = ["123456789012"] # Network Account ID
# environments/prd/platform.tfvars (for comparison)
instance_class = "db.r6g.xlarge"
instances = {
writer = { instance_class = "db.r6g.xlarge" }
reader = { instance_class = "db.r6g.xlarge" }
}
Same code, different values. Dev → QA → Prod. That's the beauty of this approach.
So finally, I have the results:
Here we can find the service network and resource configuration association, pay attention to the Shareable option.
For other hand the reach ability analyzer succeeded.
And the RAM interface with the resource shared with network external account.
The Security Model
I want to call out the defense-in-depth approach because it's not accidental:
| Layer | What It Does |
|---|---|
| VPC | No internet gateway, database-only subnets, zero public exposure |
| Security Groups | SG-to-SG references, only port 5432, minimum privilege |
| VPC Lattice | Auth type configurable (NONE for now, IAM for production) |
| RAM | Explicit principal sharing, requires acceptance from consumer |
| Encryption | Storage encrypted at rest, TLS in transit |
| Reachability Analyzer | Proves the path works — and only the intended path |
No single layer is the "security layer." They all contribute. If any one fails, the others still protect you.
How the Consumer Connects
From the consumer's perspective (the Workload Account), it's beautifully simple:
- The Network Account accepts the RAM share from the External Account
- The Network Account reshares the Service Network with the Workload Account
- The Workload Account associates their VPC to the shared Service Network
- Connect to the database using the custom private DNS name — standard PostgreSQL client
App (Private subnet) → VPC Lattice (custom DNS: db.internal.example.com) → Resource Gateway → RDS
No routes to manage. No IPs to hardcode. No peering to maintain. The application connects to db.internal.example.com and doesn't even know it's crossing account boundaries. If you later migrate the database or change the underlying infrastructure, the consumer's connection string stays the same.
What I Learned
Building this out reinforced a few things for me:
1. VPC Lattice Resource Configurations are a game-changer. ARN-based targeting means you don't need to think about IPs, ports, or DNS records. You point to what you want to share, and Lattice figures out the rest.
2. Reachability Analyzer as IaC is underrated. I haven't seen many teams doing this, but it should be standard practice. If you can test your application code, you should test your network paths.
3. Layered architecture pays off. Yes, it's more files to manage. But when something breaks at 2 AM, you know exactly which layer to look at and which dependencies might be affected.
4. RAM sharing is cleaner than I expected. No network-level coupling between VPCs. The consumer doesn't need to know our CIDR ranges, our subnet layout, or our internal architecture. They just connect.
5. Security groups > CIDRs. Always. Security group references are the "zero trust" of VPC networking — they express intent, not implementation.
Cost and the Path to a "Networkless" World
This approach is significantly cheaper than Transit Gateway at scale.
Let me break it down. With Transit Gateway, you pay:
- $0.05 per GB of data processed
- $0.05 per hour per VPC attachment
- Those costs multiply with every cross-account connection
With VPC Lattice, the model is different — you pay per request and per GB processed through the service network, but there's no per-attachment hourly cost. For database workloads with steady-state connections and moderate throughput, the savings are real.
But the bigger insight is architectural. VPC Lattice represents a shift toward what I call the networkless world — a modernization path where:
- You stop thinking about routes and start thinking about resources
- You stop managing IP connectivity and start managing access policies
- You stop building network plumbing and start declaring what talks to what
This is the same evolution we saw from physical servers → VMs → containers → serverless. Now it's happening to networking: Transit Gateway (classic hub-and-spoke) → VPC Lattice (service mesh with resource-level targeting).
For organizations still running Transit Gateway architectures, VPC Lattice Resource Configurations offer a gradual migration path. You don't need to rip-and-replace — you can introduce Lattice for new cross-account patterns while TGW continues serving existing flows. Over time, as more services move to Lattice, the TGW footprint (and cost) shrinks naturally.
🔮 What's Next: The Deep Traffic Inspection Challenge
I'm already working on Part 2, which will cover the consumer side — accepting the RAM share and connecting from ECS tasks and Lambda, plus IAM authentication for zero-trust at the application layer.
But there's a bigger question I want to explore — and it's a hard one:
How do you implement deep traffic inspection with VPC Lattice?
In many financial organizations, regulatory compliance requires that all east-west traffic passes through a Network Firewall or third-party inspection appliance. Think PCI-DSS, SOX, or local financial regulations that mandate packet-level inspection for database access.
With Transit Gateway, the pattern is well-established: route traffic through an inspection VPC with AWS Network Firewall or a Palo Alto/Fortinet appliance. But with VPC Lattice, the traffic path is abstracted — you don't control the routing in the same way.
So the challenge for Part 3 becomes:
🎯 How do you satisfy deep packet inspection requirements in a VPC Lattice architecture? Can you combine Lattice with Network Firewall? Do you need a hybrid approach where Lattice handles service-to-service but TGW remains for inspected flows? Or does VPC Lattice's auth + encryption model satisfy the compliance intent without inspection?
This is the frontier of NetDevOps in regulated industries. And honestly, I don't think there's a single right answer yet — it depends on the specific regulatory framework and the organization's risk appetite.
If you're working in financial services or healthcare and have solved this, I'd really love to hear your approach in the comments.
If you want to dig into the code, the full project is structured as a reusable scaffold that you can adapt to your own cross-account patterns.
👇👇👇👇
velez94
/
tofu-aws-netdevops-rds-external
Template project to show how to use vpc lattice and cross account access with RAM and Databases in AWS
NetDevOps External - AWS Infrastructure with VPC Lattice
Cross-account network connectivity infrastructure using AWS VPC Lattice, Aurora PostgreSQL, and Reachability Analyzer — managed with OpenTofu and Terragrunt.
Architecture
This project implements a Network Account (hub) pattern for cross-account connectivity. An external AWS account shares its service via RAM to this Network Account, which then reshares it to internal workload accounts using VPC Lattice — no VPC peering or Transit Gateway required.
┌─────────────────────────────────────────────────────────────────┐
│ External AWS Account (Provider) │
│ │
│ Aurora PostgreSQL ──► RAM Share (Service Network) ─────────┐ │
└─────────────────────────────────────────────────────────────┼───┘
│
┌─────────────────────────────────────────────────────────────▼───┐
│ This Account — Network Account (Hub) │
│ │
│ VPC Lattice Service Network (received via RAM) │
│ │ │
│ ├── Resource Gateway ──► Resource Configuration │
│ │ (connects to external Aurora on port 5432) │
│ │ │
│ └── RAM Re-share ──► Workload Accounts │
│ │
│ Reachability Analyzer validates
…
velez94
/
tofu-aws-netdevops-rds-external
Template project to show how to use vpc lattice and cross account access with RAM and Databases in AWS
NetDevOps External - AWS Infrastructure with VPC Lattice
Cross-account network connectivity infrastructure using AWS VPC Lattice, Aurora PostgreSQL, and Reachability Analyzer — managed with OpenTofu and Terragrunt.
Architecture
This project implements a Network Account (hub) pattern for cross-account connectivity. An external AWS account shares its service via RAM to this Network Account, which then reshares it to internal workload accounts using VPC Lattice — no VPC peering or Transit Gateway required.
┌─────────────────────────────────────────────────────────────────┐
│ External AWS Account (Provider) │
│ │
│ Aurora PostgreSQL ──► RAM Share (Service Network) ─────────┐ │
└─────────────────────────────────────────────────────────────┼───┘
│
┌─────────────────────────────────────────────────────────────▼───┐
│ This Account — Network Account (Hub) │
│ │
│ VPC Lattice Service Network (received via RAM) │
│ │ │
│ ├── Resource Gateway ──► Resource Configuration │
│ │ (connects to external Aurora on port 5432) │
│ │ │
│ └── RAM Re-share ──► Workload Accounts │
│ │
│ Reachability Analyzer validates…📚 Resources
- AWS VPC Lattice Resource Configurations
- terraform-aws-modules/vpc
- terraform-aws-modules/rds-aurora
- VPC Reachability Analyzer
- AWS RAM Resource Sharing
If you made it this far — thanks for reading! 🙌 I'd love to hear if you're using VPC Lattice in production, or if you have other patterns for cross-account connectivity. Drop a comment below!
Happy building! 🚀
The opinions expressed in this post are my own and do not necessarily reflect those of my employer or AWS.
✨ Alejandro Velez, Platform Engineering Latam Lead @ GFT | AWS Ambassador




Top comments (0)