VPC is the foundation everything else in AWS sits on. EC2, RDS, ECS, Lambda in a VPC, EKS, all of it lives inside a VPC. Most engineers use VPCs without understanding them. They click through the wizard, accept the defaults, and move on. Then a security audit happens, or a cross-account connection fails, or a NAT Gateway bill arrives, and the gaps show up.
This guide builds VPC understanding from the ground up. Every concept is introduced with the problem it solves. By the end you will be able to design a production-grade multi-account, multi-region network architecture from scratch, and explain every decision.
Table of Contents
- What a VPC Is - The Core Abstraction
- CIDR Notation - The Language of IP Addressing
- Subnets - Dividing the Network
- Route Tables - Where Traffic Goes
- Internet Gateway - Public Internet Access
- NAT Gateway - Outbound-Only Internet for Private Resources
- Security Groups - Stateful Instance-Level Firewall
- Network ACLs - Stateless Subnet-Level Firewall
- VPC Endpoints - Private Access to AWS Services
- VPC Peering - Connecting Two VPCs
- Transit Gateway - Hub-and-Spoke Networking at Scale
- AWS Direct Connect - Dedicated Private Connectivity
- Site-to-Site VPN - Encrypted Tunnel to On-Premises
- Client VPN - Remote Access for Individuals
- DHCP Option Sets - Customising Network Configuration
- Elastic IPs and ENIs - IP Address Management
- Flow Logs - Network Traffic Visibility
- DNS in VPC - Route 53 Resolver
- PrivateLink - Exposing Services Privately Across Accounts
- RAM - Resource Access Manager for Cross-Account Sharing
- Production VPC Design - The Reference Architecture
What a VPC Is - The Core Abstraction
The Problem
When you launch a resource in AWS, an EC2 instance, an RDS database, a Lambda function, it needs a network to live in. Without network isolation, every resource in every AWS account in the world would be on the same flat network. Any resource could reach any other resource. Security would be impossible. Routing would be chaos.
What a VPC Is
A Virtual Private Cloud (VPC) is a logically isolated network that you define within AWS. It behaves like a traditional data centre network, with IP address ranges, subnets, routing tables, and firewalls, but it is software-defined. There is no physical hardware to rack. No cables to run. The entire network is configuration.
Within a VPC, you control:
- The IP address range
- How the network is divided into subnets
- Which subnets can reach the internet
- Which resources can communicate with each other
- What traffic is allowed in and out
A VPC is regional. It exists within one AWS region and spans all Availability Zones in that region. Your subnets live in specific AZs within the VPC.
Default VPC vs Custom VPC
Every AWS account comes with a default VPC in every region. The default VPC:
- Has a
172.31.0.0/16CIDR block - Has a subnet in every AZ with public IP assignment enabled
- Has an Internet Gateway attached
- Has a default route to the Internet Gateway
The default VPC is designed for convenience, not production. It exists so you can launch an EC2 instance immediately without configuring networking. For anything beyond experimentation, you build a custom VPC.
Production VPCs are never the default. They have carefully planned CIDR ranges, private subnets for sensitive resources, controlled internet access, and explicit security boundaries.
CIDR Notation - The Language of IP Addressing
What CIDR Is
Before designing a VPC, you need to understand CIDR (Classless Inter-Domain Routing) notation. It is the language used to define IP address ranges.
A CIDR block looks like this: 10.0.0.0/16
The number after the slash is the prefix length, the number of bits that are fixed. The remaining bits define the host range.
An IPv4 address is 32 bits. 10.0.0.0/16 means the first 16 bits are fixed (10.0), and the remaining 16 bits are available for hosts and subnets.
Calculating Host Count
/16 prefix = 32 - 16 = 16 variable bits = 2^16 = 65,536 addresses
/24 prefix = 32 - 24 = 8 variable bits = 2^8 = 256 addresses
/28 prefix = 32 - 28 = 4 variable bits = 2^4 = 16 addresses
AWS reserves 5 addresses in every subnet:
-
.0- Network address -
.1- VPC router -
.2- AWS DNS -
.3- Reserved for future use -
.255- Broadcast address (not used, but reserved)
So a /28 subnet (16 addresses) gives you 16 - 5 = 11 usable addresses.
Choosing a VPC CIDR
RFC 1918 defines private IP ranges. Use these for VPCs:
| Range | CIDR | Total Addresses |
|---|---|---|
| 10.0.0.0 – 10.255.255.255 | 10.0.0.0/8 | 16.7 million |
| 172.16.0.0 – 172.31.255.255 | 172.16.0.0/12 | 1 million |
| 192.168.0.0 – 192.168.255.255 | 192.168.0.0/16 | 65,536 |
Planning rules:
-
Do not overlap CIDRs between VPCs you will ever connect. If VPC A is
10.0.0.0/16and VPC B is10.0.0.0/16, you can never peer them. Their address spaces overlap. -
Plan for growth. A
/24VPC (256 addresses) feels fine until you need 300 resources. -
Use consistent schemes across accounts. A common pattern: allocate a
/8or/10to your organisation, then carve it into/16per environment or account.
Example allocation for a multi-account organisation:
10.0.0.0/8 - Organisation total space
10.0.0.0/16 - Production account, us-east-1
10.1.0.0/16 - Production account, eu-west-1
10.2.0.0/16 - Staging account, us-east-1
10.3.0.0/16 - Development account, us-east-1
10.4.0.0/16 - Shared services account, us-east-1
10.5.0.0/16 - Security account, us-east-1
Every VPC in the organisation has a unique, non-overlapping CIDR. They can be connected to each other without conflicts.
Subnets - Dividing the Network
What Subnets Are
A subnet is a range of IP addresses within your VPC. Every resource in your VPC, EC2 instances, RDS databases, Lambda functions, load balancers, is placed in a subnet.
Subnets serve two purposes:
- AZ placement - A subnet exists in exactly one Availability Zone. Placing resources in subnets across multiple AZs gives you resilience.
- Traffic control - Subnets are associated with route tables. The route table determines where traffic from that subnet can go.
Public vs Private Subnets
A subnet is public or private based on its route table, specifically, whether it has a route to an Internet Gateway.
Public subnet - Has a route to an Internet Gateway. Resources in a public subnet can communicate with the internet (if they have a public IP). The internet can initiate connections to resources with public IPs.
Private subnet - No route to an Internet Gateway. Resources in a private subnet cannot be reached from the internet and cannot initiate direct connections to the internet.
Private does not mean isolated. Resources in private subnets can still communicate with each other, with resources in other subnets via VPC routing, and with the internet via a NAT Gateway. They just cannot be directly reached from the internet.
The Standard Subnet Design
For a production VPC in 10.0.0.0/16 across three AZs:
VPC: 10.0.0.0/16
Public Subnets (for load balancers, NAT Gateways, bastion hosts):
10.0.0.0/24 - AZ-a (251 usable IPs)
10.0.1.0/24 - AZ-b
10.0.2.0/24 - AZ-c
Private App Subnets (for EC2, ECS, EKS worker nodes):
10.0.10.0/23 - AZ-a (507 usable IPs)
10.0.12.0/23 - AZ-b
10.0.14.0/23 - AZ-c
Private Data Subnets (for RDS, ElastiCache, OpenSearch):
10.0.20.0/24 - AZ-a (251 usable IPs)
10.0.21.0/24 - AZ-b
10.0.22.0/24 - AZ-c
Three tiers, three AZs, nine subnets. Each tier has a distinct purpose and distinct security controls.
CloudFormation
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: production-vpc
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.0.0/24
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: false # Never auto-assign public IPs
Tags:
- Key: Name
Value: public-subnet-a
- Key: Tier
Value: public
PrivateAppSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.10.0/23
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: private-app-subnet-a
- Key: Tier
Value: private-app
PrivateDataSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.20.0/24
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: private-data-subnet-a
- Key: Tier
Value: private-data
Notice MapPublicIpOnLaunch: false even on public subnets. Public IPs should be assigned explicitly where needed, not automatically. This forces intentionality.
Route Tables - Where Traffic Goes
What Route Tables Do
A route table is a set of rules, routes, that determine where network traffic is directed. Every subnet is associated with exactly one route table. Every VPC has a main route table that subnets use by default.
A route has two components:
- Destination - The IP address range the route applies to
- Target - Where to send traffic destined for that range
How Routing Works
When a resource in a subnet sends traffic, the VPC router looks at the destination IP address and finds the most specific matching route in the subnet's route table.
Route Table for Public Subnet:
Destination Target
10.0.0.0/16 local ← All VPC traffic goes to local router
0.0.0.0/0 igw-abc123 ← All other traffic goes to Internet Gateway
The local route is created automatically for every VPC. It covers the entire VPC CIDR and routes traffic within the VPC. You cannot delete it.
The 0.0.0.0/0 route (the default route) sends all traffic not matched by a more specific route to the specified target. For public subnets, the target is the Internet Gateway.
Route Table for Private App Subnet:
Destination Target
10.0.0.0/16 local
0.0.0.0/0 nat-abc123 ← Internet-bound traffic goes to NAT Gateway
Private subnets have a default route to the NAT Gateway instead of the Internet Gateway. Traffic can get out (via NAT), but nothing from the internet can initiate a connection in.
Route Table for Private Data Subnet:
Destination Target
10.0.0.0/16 local
← No default route. No internet access at all.
The data tier has no default route. Database servers have no path to or from the internet. They can only talk to resources within the VPC.
CloudFormation
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: public-route-table
PublicDefaultRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRouteTable
PrivateAppRouteTableA:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: private-app-route-table-a
PrivateAppDefaultRouteA:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateAppRouteTableA
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGatewayA
PrivateAppSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateAppSubnetA
RouteTableId: !Ref PrivateAppRouteTableA
One private route table per AZ for the app tier. Why not one shared route table for all three private subnets? Because each private subnet routes internet traffic to the NAT Gateway in its own AZ. If AZ-a goes down, the NAT Gateway in AZ-a goes down too. Private subnets in AZ-b and AZ-c should route to their own NAT Gateways, not AZ-a's. One route table per AZ enables this.
Internet Gateway - Public Internet Access
What It Is
An Internet Gateway (IGW) is a horizontally scaled, redundant, highly available VPC component that enables communication between your VPC and the internet. It does two things:
- Provides a target in route tables for internet-routable traffic
- Performs Network Address Translation (NAT) for resources with public IP addresses, translating between the private IP inside the VPC and the public IP visible on the internet
How It Works
A resource in a public subnet with a public IP address:
- Sends a packet to
8.8.8.8(Google's DNS) - The VPC router matches
0.0.0.0/0and sends the packet to the IGW - The IGW sees the source private IP (
10.0.0.25) and the associated public IP (52.90.100.200) - The IGW translates the source to the public IP and forwards the packet
- The response comes back to the public IP
- The IGW translates back to the private IP and delivers it to the instance
This translation is called one-to-one NAT. One private IP maps to one public IP. The instance itself does not know its public IP, it only sees its private IP.
What Would Happen Without It
Without an Internet Gateway attached to the VPC, no subnet can reach the internet, regardless of route table configuration. The IGW is the gateway. The route table just tells traffic to go there.
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: production-igw
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
An IGW can only be attached to one VPC at a time. A VPC can only have one IGW.
NAT Gateway - Outbound-Only Internet for Private Resources
The Problem
Your application servers are in private subnets. They need to download OS patches, pull container images from ECR, or call external APIs. But they must not be directly reachable from the internet.
An Internet Gateway handles public subnets, resources with public IPs. Private resources have no public IPs. They need a way to initiate outbound connections to the internet without having an inbound-accessible public address.
What NAT Gateway Is
A NAT Gateway (Network Address Translation Gateway) sits in a public subnet and provides outbound internet access for private resources. Private resources send traffic to the NAT Gateway, which translates their private IP to its own Elastic IP (public IP) and forwards the traffic. Responses come back to the NAT Gateway, which translates back and delivers to the private resource.
The critical property: this translation is one-directional. The internet can respond to connections initiated from inside, but cannot initiate new connections to the private resources. There is no public IP on the private resource for the internet to target.
Architecture
Private EC2 (10.0.10.15)
↓ sends packet to 8.8.8.8
Private Route Table: 0.0.0.0/0 → nat-abc123
↓
NAT Gateway (10.0.0.5, EIP: 52.90.100.201) in Public Subnet
↓ translates source 10.0.10.15 → 52.90.100.201
Internet Gateway
↓
8.8.8.8
↓ response to 52.90.100.201
NAT Gateway
↓ translates back to 10.0.10.15
Private EC2
High Availability
A NAT Gateway is AZ-specific. If the AZ it is in fails, the NAT Gateway fails. For high availability, deploy one NAT Gateway per AZ and configure each AZ's private route table to use its own NAT Gateway.
EIPA:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NATGatewayA:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt EIPA.AllocationId
SubnetId: !Ref PublicSubnetA
Tags:
- Key: Name
Value: nat-gateway-a
EIPB:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NATGatewayB:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt EIPB.AllocationId
SubnetId: !Ref PublicSubnetB
Tags:
- Key: Name
Value: nat-gateway-b
The Cost Reality
NAT Gateway costs money in two ways: hourly ($0.045/hour per gateway) and per GB processed ($0.045/GB). In a three-AZ design, three NAT Gateways cost approximately $97/month in idle costs before any data is processed.
For development environments, one NAT Gateway across all AZs is acceptable. For production, the HA cost is justified.
NAT Gateway is also a frequent source of unexpected AWS bills. A Lambda function in a VPC that calls an external API thousands of times per second generates significant NAT Gateway data processing costs. VPC Endpoints (section 9) eliminate this cost for AWS service traffic.
NAT Gateway vs NAT Instance
AWS used to recommend NAT Instances, EC2 instances configured to perform NAT. NAT Gateway replaced them. NAT Gateway is managed, scales automatically, is highly available within an AZ, and requires no administration. NAT Instance requires patching, scaling, and management. Always use NAT Gateway.
Security Groups - Stateful Instance-Level Firewall
What Security Groups Are
A Security Group is a virtual firewall that controls inbound and outbound traffic at the resource level, EC2 instances, RDS databases, Lambda functions in a VPC, load balancers.
Security Groups are stateful. If an inbound rule allows traffic in, the response traffic is automatically allowed out, regardless of outbound rules. You do not need matching outbound rules for established connections.
How They Work
A Security Group has inbound rules and outbound rules. Each rule specifies:
- Protocol - TCP, UDP, ICMP, or All
- Port range - Single port or range
- Source/Destination - CIDR block, another Security Group, or prefix list
WebServerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for web application servers
VpcId: !Ref VPC
SecurityGroupIngress:
# HTTPS from the load balancer only
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref ALBSecurityGroup
# HTTP from the load balancer only (for redirect)
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
SecurityGroupEgress:
# HTTPS to internet (for external API calls)
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
# PostgreSQL to database security group
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
DestinationSecurityGroupId: !Ref DatabaseSecurityGroup
# Redis to cache security group
- IpProtocol: tcp
FromPort: 6379
ToPort: 6379
DestinationSecurityGroupId: !Ref CacheSecurityGroup
DatabaseSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for PostgreSQL RDS
VpcId: !Ref VPC
SecurityGroupIngress:
# PostgreSQL only from web servers
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref WebServerSecurityGroup
SecurityGroupEgress: [] # No outbound - databases don't initiate
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Application Load Balancer
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0 # HTTPS from internet
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0 # HTTP from internet (for redirect)
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
DestinationSecurityGroupId: !Ref WebServerSecurityGroup
- IpProtocol: tcp
FromPort: 80
ToPort: 80
DestinationSecurityGroupId: !Ref WebServerSecurityGroup
Security Group Chaining
The most important pattern: reference Security Groups in rules, not CIDR blocks, wherever possible.
# Instead of this (allows any IP in the 10.0.10.0/23 range):
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: 10.0.10.0/23
# Do this (allows only resources in the WebServerSecurityGroup):
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref WebServerSecurityGroup
When you reference a Security Group, the rule applies to any resource assigned that Security Group, regardless of IP address. It is identity-based, not location-based. A new web server added to the Security Group automatically gets database access. An instance not in the Security Group cannot reach the database, even if it is in the same subnet.
What Would Happen Without Correct Security Groups
The default Security Group in a VPC allows all outbound and all inbound from within the same Security Group. Applied broadly, this means all resources in the same Security Group can talk to each other on any port, a flat network with no segmentation. A compromised web server could reach the database directly. That is not a network architecture. That is one layer of failed perimeter security.
Network ACLs - Stateless Subnet-Level Firewall
What NACLs Are
A Network ACL (Access Control List) is a firewall at the subnet level. Unlike Security Groups (which are resource-level and stateful), NACLs are subnet-level and stateless.
Stateless means: each packet is evaluated independently. If an inbound rule allows a request in, you must also have an outbound rule that allows the response out, the return traffic is not automatically allowed.
NACL Rules
NACLs have numbered rules evaluated in order, lowest first. The first matching rule is applied.
PublicSubnetNACL:
Type: AWS::EC2::NetworkAcl
Properties:
VpcId: !Ref VPC
# Inbound rules
NACLInboundHTTPS:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref PublicSubnetNACL
RuleNumber: 100
Protocol: 6 # TCP
RuleAction: allow
Egress: false
CidrBlock: 0.0.0.0/0
PortRange:
From: 443
To: 443
NACLInboundHTTP:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref PublicSubnetNACL
RuleNumber: 110
Protocol: 6
RuleAction: allow
Egress: false
CidrBlock: 0.0.0.0/0
PortRange:
From: 80
To: 80
# Ephemeral ports - required for return traffic (stateless!)
NACLInboundEphemeral:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref PublicSubnetNACL
RuleNumber: 120
Protocol: 6
RuleAction: allow
Egress: false
CidrBlock: 0.0.0.0/0
PortRange:
From: 1024
To: 65535 # Ephemeral port range for response traffic
# Deny all other inbound
NACLInboundDenyAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref PublicSubnetNACL
RuleNumber: 32766
Protocol: -1 # All protocols
RuleAction: deny
Egress: false
CidrBlock: 0.0.0.0/0
# Outbound rules
NACLOutboundAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref PublicSubnetNACL
RuleNumber: 100
Protocol: -1
RuleAction: allow
Egress: true
CidrBlock: 0.0.0.0/0
The Ephemeral Port Requirement
This is the most common NACL mistake. TCP responses use ephemeral ports temporary high-numbered ports (1024-65535) chosen by the operating system. When a client connects to port 443, the response goes back to the client's ephemeral port, some random port above 1024.
Because NACLs are stateless, they evaluate the response packet independently. If you have an inbound rule allowing port 443 but no inbound rule allowing ephemeral ports, the response to your own client is blocked. You must explicitly allow inbound ephemeral ports for return traffic from your outbound connections, and outbound ephemeral ports for return traffic to your inbound connections.
Security Groups vs NACLs - When to Use Which
| Security Group | NACL | |
|---|---|---|
| Level | Resource (ENI) | Subnet |
| State | Stateful | Stateless |
| Rules | Allow only | Allow and Deny |
| Evaluation | All rules evaluated | Rules evaluated in order, first match wins |
| Default | Deny all inbound, allow all outbound | Allow all inbound and outbound |
Use Security Groups as your primary traffic control mechanism. They are more expressive, easier to reason about, and stateful. NACLs are a secondary defence, use them to implement subnet-level deny rules for broad IP ranges (blocking a known malicious IP range) where Security Groups cannot deny.
Security Groups cannot "deny", they can only "allow". If you need to explicitly block traffic from a specific IP range even if a Security Group would allow it, that is a NACL use case.
VPC Endpoints - Private Access to AWS Services
The Problem
Your EC2 instances are in private subnets. They need to access S3, DynamoDB, Secrets Manager, SSM Parameter Store. Without VPC Endpoints, this traffic goes through the NAT Gateway, out of the VPC, across the internet (via AWS's network), and back in. You pay NAT Gateway data processing costs for every byte. The traffic leaves the VPC boundary.
What VPC Endpoints Are
VPC Endpoints allow private connectivity to AWS services without traffic leaving the AWS network. No NAT Gateway. No Internet Gateway. No public IP required. The traffic stays entirely within AWS.
Two types:
Gateway Endpoints
Available only for S3 and DynamoDB. Free. Added as a route in your route table.
S3GatewayEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
VpcEndpointType: Gateway
RouteTableIds:
- !Ref PrivateAppRouteTableA
- !Ref PrivateAppRouteTableB
- !Ref PrivateAppRouteTableC
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: "*"
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource:
- !Sub "arn:aws:s3:::${AppBucket}/*"
- !Sub "arn:aws:s3:::${AppBucket}"
DynamoDBGatewayEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub "com.amazonaws.${AWS::Region}.dynamodb"
VpcEndpointType: Gateway
RouteTableIds:
- !Ref PrivateAppRouteTableA
- !Ref PrivateAppRouteTableB
- !Ref PrivateAppRouteTableC
The endpoint adds a route to the route table: traffic destined for S3 or DynamoDB IP ranges goes to the endpoint, not to the NAT Gateway. No configuration change in your application, the AWS SDK already knows the S3 endpoint address. The routing is transparent.
The endpoint policy is an IAM-like policy that restricts which S3 buckets or DynamoDB tables can be accessed through the endpoint. This is an additional security control, even if an IAM policy allows access to another bucket, the endpoint policy can restrict it to only your application's buckets.
Interface Endpoints
Available for most other AWS services, SSM, Secrets Manager, KMS, SQS, SNS, ECR, CloudWatch, and many more. Uses AWS PrivateLink. Creates an Elastic Network Interface (ENI) in your subnet with a private IP address. Not free ($0.01/hour per AZ plus $0.01/GB).
SecretsManagerEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub "com.amazonaws.${AWS::Region}.secretsmanager"
VpcEndpointType: Interface
SubnetIds:
- !Ref PrivateAppSubnetA
- !Ref PrivateAppSubnetB
- !Ref PrivateAppSubnetC
SecurityGroupIds:
- !Ref EndpointSecurityGroup
PrivateDnsEnabled: true # Override public DNS with private endpoint IP
SSMEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm"
VpcEndpointType: Interface
SubnetIds:
- !Ref PrivateAppSubnetA
- !Ref PrivateAppSubnetB
- !Ref PrivateAppSubnetC
SecurityGroupIds:
- !Ref EndpointSecurityGroup
PrivateDnsEnabled: true
SSMMessagesEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages"
VpcEndpointType: Interface
SubnetIds:
- !Ref PrivateAppSubnetA
SecurityGroupIds:
- !Ref EndpointSecurityGroup
PrivateDnsEnabled: true
EC2MessagesEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages"
VpcEndpointType: Interface
SubnetIds:
- !Ref PrivateAppSubnetA
SecurityGroupIds:
- !Ref EndpointSecurityGroup
PrivateDnsEnabled: true
PrivateDnsEnabled: true is critical. It overrides the public DNS for the service with the private endpoint IP. Your application calls secretsmanager.us-east-1.amazonaws.com, with PrivateDnsEnabled, that resolves to the private endpoint IP inside your VPC. No code change required.
The three SSM endpoints (ssm, ssmmessages, ec2messages) are required together to enable AWS Systems Manager Session Manager, the secure shell replacement for EC2 instances in private subnets with no SSH, no bastion host, and no public IP.
The Cost Comparison
Without VPC Endpoints, a Lambda function in a private subnet that reads a secret from Secrets Manager on every invocation, invoked 1 million times per day, processes approximately 5 MB of data per million requests through the NAT Gateway.
NAT Gateway data: 5 MB/million × 1M/day × 30 days = 150 GB/month
Cost: 150 GB × $0.045/GB = $6.75/month
Plus NAT Gateway hourly: $0.045 × 24 × 30 × 3 AZs = $97.20/month
Total: ~$104/month for this one traffic pattern
VPC Interface Endpoint: $0.01/hour × 24 × 30 × 3 AZs = $21.60/month
Data processing: $0.01/GB × 150 GB = $1.50/month
Total: ~$23/month
For high-throughput workloads, VPC Endpoints save significant money beyond the security benefit.
VPC Peering - Connecting Two VPCs
What It Is
VPC Peering creates a private network connection between two VPCs. Traffic routes via AWS's internal network, not through the internet. The two VPCs can be in the same account, different accounts, or different regions.
After peering, resources in VPC A can communicate with resources in VPC B as if they were on the same network, using private IP addresses.
Setup
Peering requires three steps:
- Create the peering connection (one VPC initiates, the other accepts)
- Add routes in both VPCs pointing to the peering connection
- Update Security Groups to allow traffic from the peer VPC's CIDR
# In the requester account/VPC
PeeringConnection:
Type: AWS::EC2::VPCPeeringConnection
Properties:
VpcId: !Ref VpcA
PeerVpcId: vpc-0abc1234 # The peer VPC
PeerOwnerId: "123456789012" # Peer account ID
PeerRegion: eu-west-1 # For cross-region peering
Tags:
- Key: Name
Value: prod-to-shared-services
# Route in VPC A pointing to VPC B
RouteToVpcB:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateAppRouteTableA
DestinationCidrBlock: 10.4.0.0/16 # VPC B's CIDR
VpcPeeringConnectionId: !Ref PeeringConnection
# In the accepter account/VPC - must accept the request and add return routes
# Route in VPC B pointing back to VPC A
RouteToVpcA:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref SharedServicesRouteTable
DestinationCidrBlock: 10.0.0.0/16 # VPC A's CIDR
VpcPeeringConnectionId: !Ref PeeringConnection
The Transitive Routing Problem
VPC Peering is not transitive. This is the most important limitation to understand.
VPC A ←→ VPC B ←→ VPC C
VPC A cannot reach VPC C through VPC B.
If you want full mesh connectivity between three VPCs, you need three peering connections: A-B, B-C, and A-C. For five VPCs, you need ten peering connections. For ten VPCs, you need 45.
This does not scale. For more than a handful of VPCs, use Transit Gateway.
When VPC Peering Is the Right Choice
- Two or three VPCs that need to communicate
- Simple, point-to-point connectivity
- No complex routing requirements
- Cost-sensitive (peering has no hourly cost, only data transfer cost)
Transit Gateway - Hub-and-Spoke Networking at Scale
The Problem VPC Peering Cannot Solve
You have 20 VPCs, production, staging, development, one per business unit, shared services, security tooling. Full mesh peering requires 190 peering connections. Managing 190 peering connections, each with its own route table entries and Security Group rules, is operationally unmanageable.
Transit Gateway solves this.
What Transit Gateway Is
A Transit Gateway (TGW) is a regional network hub. VPCs and on-premises networks attach to the TGW. All routing goes through the TGW. Instead of 190 peering connections, you have 20 VPC attachments to one TGW.
Transit Gateway
/ | | \
VPC A VPC B VPC C VPC D
|
On-premises (via VPN or Direct Connect)
Every VPC that attaches to the TGW can route to every other attached VPC, without individual peering connections. Adding a new VPC means one TGW attachment, not connections to every existing VPC.
TGW Route Tables
Transit Gateway has its own route tables. These are separate from VPC route tables. You create TGW route tables and associate VPC attachments with them, giving you full control over inter-VPC routing.
TransitGateway:
Type: AWS::EC2::TransitGateway
Properties:
Description: Organisation-wide network hub
AutoAcceptSharedAttachments: disabled # Explicit acceptance required
DefaultRouteTableAssociation: disable # Use custom route tables
DefaultRouteTablePropagation: disable
DnsSupport: enable
VpnEcmpSupport: enable
Tags:
- Key: Name
Value: org-transit-gateway
# Attach production VPC to TGW
ProductionVPCAttachment:
Type: AWS::EC2::TransitGatewayVpcAttachment
Properties:
TransitGatewayId: !Ref TransitGateway
VpcId: !Ref ProductionVPC
SubnetIds:
- !Ref ProductionPrivateSubnetA
- !Ref ProductionPrivateSubnetB
- !Ref ProductionPrivateSubnetC
Tags:
- Key: Name
Value: production-vpc-attachment
# TGW route table for production - can reach shared services, not dev
ProductionRouteTable:
Type: AWS::EC2::TransitGatewayRouteTable
Properties:
TransitGatewayId: !Ref TransitGateway
Tags:
- Key: Name
Value: production-route-table
# Associate production attachment with production route table
ProductionRouteTableAssociation:
Type: AWS::EC2::TransitGatewayRouteTableAssociation
Properties:
TransitGatewayAttachmentId: !Ref ProductionVPCAttachment
TransitGatewayRouteTableId: !Ref ProductionRouteTable
# Route: production can reach shared services
RouteToSharedServices:
Type: AWS::EC2::TransitGatewayRoute
Properties:
TransitGatewayRouteTableId: !Ref ProductionRouteTable
DestinationCidrBlock: 10.4.0.0/16 # Shared services CIDR
TransitGatewayAttachmentId: !Ref SharedServicesVPCAttachment
# Route in production VPC pointing to TGW
ProductionRouteToTGW:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref ProductionPrivateRouteTableA
DestinationCidrBlock: 10.4.0.0/16 # Shared services
TransitGatewayId: !Ref TransitGateway
Network Segmentation with TGW Route Tables
Different TGW route tables enable network segmentation at scale. Production VPCs can reach shared services but not development VPCs. Development VPCs can reach shared services but not production. Inspection VPCs can see all traffic.
Production Route Table:
→ Shared Services VPC (10.4.0.0/16)
→ On-premises (10.100.0.0/16)
NOT → Development VPC
Development Route Table:
→ Shared Services VPC (10.4.0.0/16)
NOT → Production VPC
NOT → On-premises
Shared Services Route Table:
→ Production VPC (10.0.0.0/16)
→ Development VPC (10.3.0.0/16)
→ On-premises (10.100.0.0/16)
This is network segmentation that would require hundreds of peering connections and route table entries to replicate. With TGW, it is route table configuration.
Cross-Account and Cross-Region
TGW supports cross-account attachment via AWS RAM (Resource Access Manager). Share the TGW with other accounts; they attach their VPCs without needing a separate TGW.
TGW Peering connects Transit Gateways in different regions, enabling global network architectures without the internet as the backbone.
AWS Direct Connect - Dedicated Private Connectivity
The Problem
Your on-premises data centre needs to connect to AWS. A Site-to-Site VPN over the internet works, but it has variable latency, limited bandwidth, and the traffic traverses the public internet, which is a compliance concern for some regulated industries.
What Direct Connect Is
AWS Direct Connect is a dedicated, private network connection from your data centre to AWS. It does not use the internet. It is a physical fibre connection to an AWS Direct Connect location (a colocation facility where AWS has equipment).
Traffic on a Direct Connect connection:
- Does not traverse the internet
- Has consistent, predictable latency
- Has higher bandwidth (up to 100 Gbps)
- Is not subject to internet bandwidth fluctuations
Connection Types
Dedicated connection - A physical 1 Gbps, 10 Gbps, or 100 Gbps port at a Direct Connect location. AWS provisions the port. Your network connects to it.
Hosted connection - A sub-Gbps or 1-10 Gbps connection provided by an AWS Direct Connect Partner. The partner provides the physical infrastructure. Useful if you cannot co-locate equipment at a Direct Connect location.
Virtual Interfaces
Over a Direct Connect connection, you create Virtual Interfaces (VIFs):
Private VIF - Connects to a VPC via a Virtual Private Gateway or Transit Gateway. Traffic goes to your VPC's private IP addresses. Used for accessing private resources.
Public VIF - Connects to AWS public services (S3, DynamoDB, public endpoints) without traversing the internet. Traffic goes to AWS public IP ranges over the private connection.
Transit VIF - Connects to a Transit Gateway. Enables a single Direct Connect connection to reach multiple VPCs via TGW.
Resilience
A single Direct Connect connection is a single point of failure. For production, always configure two connections from different Direct Connect locations (for location redundancy) and use Site-to-Site VPN as a fallback.
On-Premises Router
├── Direct Connect Connection 1 → DX Location A → AWS Region
├── Direct Connect Connection 2 → DX Location B → AWS Region
└── Site-to-Site VPN → Internet → AWS Region (backup)
Traffic prefers Direct Connect when available. If both Direct Connect connections fail, traffic fails over to the VPN automatically via BGP routing.
Site-to-Site VPN - Encrypted Tunnel to On-Premises
What It Is
AWS Site-to-Site VPN creates an encrypted IPsec tunnel between your on-premises network and your AWS VPC over the public internet. It is faster to set up than Direct Connect (minutes vs weeks/months), costs less, but has variable performance and does traverse the internet.
Components
Virtual Private Gateway (VGW) - The AWS-side endpoint of the VPN. Attached to a VPC.
Customer Gateway (CGW) - The on-premises-side endpoint. Represents your physical router or firewall.
VPN Connection - The tunnel between the VGW and CGW. Each connection has two tunnels for redundancy.
VirtualPrivateGateway:
Type: AWS::EC2::VPNGateway
Properties:
Type: ipsec.1
Tags:
- Key: Name
Value: production-vgw
VPCGatewayAttachmentVPN:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
VpnGatewayId: !Ref VirtualPrivateGateway
CustomerGateway:
Type: AWS::EC2::CustomerGateway
Properties:
Type: ipsec.1
BgpAsn: 65000 # Your on-premises BGP AS number
IpAddress: 203.0.113.1 # Your on-premises public IP
Tags:
- Key: Name
Value: office-router
VPNConnection:
Type: AWS::EC2::VPNConnection
Properties:
Type: ipsec.1
CustomerGatewayId: !Ref CustomerGateway
VpnGatewayId: !Ref VirtualPrivateGateway
StaticRoutesOnly: false # Use BGP for dynamic routing
Tags:
- Key: Name
Value: office-to-production
# Enable route propagation - VGW automatically adds on-premises routes
RoutePropagation:
Type: AWS::EC2::VPNGatewayRoutePropagation
Properties:
RouteTableIds:
- !Ref PrivateAppRouteTableA
- !Ref PrivateAppRouteTableB
VpnGatewayId: !Ref VirtualPrivateGateway
Route propagation automatically adds routes for your on-premises network to your VPC route tables. Instead of manually adding a route for each on-premises subnet, the VGW learns routes via BGP and propagates them.
VPN vs Direct Connect
| Site-to-Site VPN | Direct Connect | |
|---|---|---|
| Setup time | Minutes | Weeks to months |
| Cost | ~$36/month + data | From ~$216/month + data |
| Bandwidth | Up to 1.25 Gbps | Up to 100 Gbps |
| Latency | Variable (internet) | Consistent (dedicated) |
| Reliability | Internet-dependent | SLA-backed |
| Traverses internet | Yes (encrypted) | No |
| Use case | Low cost, quick setup, lower traffic | High bandwidth, consistent latency, compliance |
Client VPN - Remote Access for Individuals
The Problem
Site-to-Site VPN connects networks. But a developer working from home, a support engineer travelling, or an operations team member needs individual access to private resources in the VPC. Site-to-Site VPN is not for individual users — it is for connecting office networks.
What Client VPN Is
AWS Client VPN is a managed OpenVPN service that allows individual users to connect to your VPC from anywhere. Each user authenticates, establishes an encrypted tunnel, and gains access to specified resources in the VPC.
ClientVPNEndpoint:
Type: AWS::EC2::ClientVpnEndpoint
Properties:
Description: Developer access to production VPC
ServerCertificateArn: arn:aws:acm:us-east-1:123456789012:certificate/abc123
AuthenticationOptions:
- Type: federated-authentication
FederatedAuthentication:
SAMLProviderArn: !Ref SAMLProvider # SSO via Okta, Azure AD, etc.
ConnectionLogOptions:
Enabled: true
CloudwatchLogGroup: /aws/clientvpn
CloudwatchLogStream: connections
ClientCidrBlock: 172.16.0.0/22 # IP range assigned to VPN clients
SplitTunnel: true # Only VPC traffic goes through VPN
VpcId: !Ref VPC
SecurityGroupIds:
- !Ref ClientVPNSecurityGroup
DnsServers:
- 10.0.0.2 # VPC DNS resolver
ClientVPNNetworkAssociation:
Type: AWS::EC2::ClientVpnTargetNetworkAssociation
Properties:
ClientVpnEndpointId: !Ref ClientVPNEndpoint
SubnetId: !Ref PrivateAppSubnetA
# Authorisation rule — which users can access which networks
ClientVPNAuthRule:
Type: AWS::EC2::ClientVpnAuthorizationRule
Properties:
ClientVpnEndpointId: !Ref ClientVPNEndpoint
TargetNetworkCidr: 10.0.0.0/16 # Access to entire VPC
AuthorizeAllGroups: false
AccessGroupId: "engineers-group-id" # Only engineers group
Description: Engineers access to VPC
SplitTunnel: true is important for productivity. With split tunnelling, only traffic destined for the VPC goes through the VPN. Internet traffic (GitHub, Slack, YouTube) uses the user's normal internet connection. Without split tunnelling, all traffic goes through the VPN, adding latency and NAT Gateway costs for traffic that has nothing to do with the VPC.
DHCP Option Sets - Customising Network Configuration
What They Are
When a resource launches in your VPC, it needs network configuration: an IP address, a default gateway, a DNS server, a domain name. DHCP (Dynamic Host Configuration Protocol) provides this automatically. VPCs have DHCP Option Sets that define what the DHCP server tells resources.
The default DHCP Option Set uses AmazonProvidedDNS for DNS and the VPC's domain (region.compute.internal or ec2.internal for us-east-1).
When to Customise
Custom DNS servers - If you run your own DNS servers on-premises and need resources in the VPC to resolve internal hostnames (like db.internal.company.com), point the DHCP Option Set at your DNS servers.
Custom domain name - Change the domain name used for internal hostname resolution. Instead of ip-10-0-10-15.ec2.internal, instances can be named ip-10-0-10-15.production.company.internal.
CustomDHCPOptions:
Type: AWS::EC2::DHCPOptions
Properties:
DomainName: production.company.internal
DomainNameServers:
- 10.0.0.2 # VPC resolver (must include this for Route 53 Private Hosted Zones)
- 10.100.1.10 # On-premises DNS server
NtpServers:
- 169.254.169.123 # AWS NTP service
Tags:
- Key: Name
Value: production-dhcp-options
DHCPOptionsAssociation:
Type: AWS::EC2::VPCDHCPOptionsAssociation
Properties:
VpcId: !Ref VPC
DhcpOptionsId: !Ref CustomDHCPOptions
One DHCP Option Set per VPC. Changes apply to new DHCP leases, existing instances do not update immediately unless they release and renew their lease.
Elastic IPs and ENIs - IP Address Management
Elastic IPs
An Elastic IP (EIP) is a static public IPv4 address that you allocate to your account and associate with resources. Unlike auto-assigned public IPs (which change when an instance stops and restarts), an EIP stays the same.
ElasticIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: nat-gateway-eip
# Associate with an EC2 instance
EIPAssociation:
Type: AWS::EC2::EIPAssociation
Properties:
InstanceId: !Ref MyInstance
AllocationId: !GetAtt ElasticIP.AllocationId
Use EIPs for:
- NAT Gateways (required)
- Resources that external partners whitelist by IP
- Resources where the IP must be known in advance (firewall rules, DNS)
Unattached EIPs cost money ($0.005/hour). Release them when not in use.
Elastic Network Interfaces
An Elastic Network Interface (ENI) is a virtual network card. Every EC2 instance has at least one ENI, its primary network interface. ENIs have:
- Private IP address (primary and secondary)
- Optional public IP or Elastic IP
- MAC address
- Security Groups
- Source/destination check flag
Secondary ENIs can be attached to instances and moved between instances. This enables:
- Dual-homed instances (connected to two subnets/VPCs)
- Failover by moving an ENI (with its IP and MAC) to a standby instance
- Licensing tied to MAC address that must stay consistent across failover
import boto3
ec2 = boto3.client('ec2', region_name='us-east-1')
# Create a secondary ENI
eni = ec2.create_network_interface(
SubnetId='subnet-0abc1234',
Description='Secondary interface for failover',
Groups=['sg-0abc1234'],
PrivateIpAddresses=[
{'PrivateIpAddress': '10.0.10.50', 'Primary': True}
]
)
ENI_ID = eni['NetworkInterface']['NetworkInterfaceId']
# Attach to an instance
ec2.attach_network_interface(
NetworkInterfaceId=ENI_ID,
InstanceId='i-0abc1234',
DeviceIndex=1 # 0 is primary, 1+ is secondary
)
# Move to another instance during failover
ec2.detach_network_interface(AttachmentId='eni-attach-abc123')
ec2.attach_network_interface(
NetworkInterfaceId=ENI_ID,
InstanceId='i-0def5678', # Standby instance
DeviceIndex=1
)
Flow Logs - Network Traffic Visibility
What They Are
VPC Flow Logs capture metadata about IP traffic in your VPC. Not the packet contents, the metadata: source IP, destination IP, source port, destination port, protocol, bytes transferred, action (ACCEPT or REJECT), and whether the traffic was accepted or rejected by Security Group or NACL rules.
Flow Logs are the primary visibility tool for network troubleshooting and security investigation in a VPC.
What They Tell You
- Which resources are communicating
- Which connections are being rejected (Security Group or NACL blocking)
- Unusual traffic patterns (potential data exfiltration, port scanning, lateral movement)
- NAT Gateway and VPC Endpoint utilisation
- Traffic volume between subnets
Configuration
Flow Logs can be captured at three levels:
- VPC — All traffic in the VPC
- Subnet — All traffic in a specific subnet
- ENI — Traffic on a specific network interface
FlowLogsRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: vpc-flow-logs.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: FlowLogsPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
- logs:DescribeLogGroups
- logs:DescribeLogStreams
Resource: "*"
FlowLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /vpc/flow-logs/production
RetentionInDays: 90
VPCFlowLog:
Type: AWS::EC2::FlowLog
Properties:
ResourceType: VPC
ResourceId: !Ref VPC
TrafficType: ALL # ACCEPT, REJECT, or ALL
LogDestinationType: cloud-watch-logs
LogGroupName: !Ref FlowLogGroup
DeliverLogsPermissionArn: !GetAtt FlowLogsRole.Arn
LogFormat: >-
${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr}
${srcport} ${dstport} ${protocol} ${packets} ${bytes}
${start} ${end} ${action} ${log-status}
${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags}
${type} ${pkt-srcaddr} ${pkt-dstaddr}
Querying Flow Logs with CloudWatch Insights
# Find all rejected connections (Security Group or NACL blocks)
fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, protocol, action
| filter action = "REJECT"
| sort @timestamp desc
| limit 100
# Find the top talkers in the VPC
fields @timestamp, srcAddr, dstAddr, bytes
| stats sum(bytes) as totalBytes by srcAddr
| sort totalBytes desc
| limit 20
# Find connections to a specific port (potential port scanning)
fields @timestamp, srcAddr, dstAddr, dstPort
| filter dstPort = 22 and action = "REJECT"
| stats count() as attempts by srcAddr
| sort attempts desc
# Find traffic leaving to the internet on unusual ports
fields @timestamp, srcAddr, dstAddr, dstPort, bytes
| filter dstAddr not like /^10\./ and dstAddr not like /^172\./ and dstAddr not like /^192\.168\./
| filter dstPort != 443 and dstPort != 80
| sort bytes desc
Flow Logs sent to S3 can be queried with Athena — more powerful for complex queries across long retention periods.
DNS in VPC - Route 53 Resolver
How VPC DNS Works
Every VPC has a DNS resolver at VPC_CIDR_BASE + 2. For 10.0.0.0/16, the resolver is at 10.0.0.2. This resolver handles:
- Private Hosted Zone queries (your internal domains)
- AWS service name resolution (EC2 hostnames, service endpoints)
- Public DNS queries (forwarded to Route 53 public resolvers)
Private Hosted Zones
A Private Hosted Zone is a Route 53 DNS zone visible only within specified VPCs. It provides internal DNS names for your private resources.
PrivateHostedZone:
Type: AWS::Route53::HostedZone
Properties:
Name: internal.company.com
VPCs:
- VPCId: !Ref VPC
VPCRegion: !Ref AWS::Region
HostedZoneConfig:
Comment: Private zone for internal services
DatabaseRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref PrivateHostedZone
Name: postgres.internal.company.com
Type: CNAME
TTL: 60
ResourceRecords:
- !GetAtt Database.Endpoint.Address
CacheRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref PrivateHostedZone
Name: redis.internal.company.com
Type: CNAME
TTL: 60
ResourceRecords:
- !GetAtt RedisCluster.ConfigurationEndPoint.Address
Instead of hardcoding RDS endpoint URLs in your application config, which change when you restore from a snapshot or replace the database, use a CNAME like postgres.internal.company.com. When the RDS endpoint changes, update the DNS record. The application does not change.
Route 53 Resolver - Hybrid DNS
For hybrid environments, DNS resolution crosses the on-premises/cloud boundary. On-premises systems need to resolve AWS private hostnames. AWS resources need to resolve on-premises hostnames.
Inbound Resolver Endpoint - On-premises DNS servers forward queries for your AWS domain to this endpoint. Queries enter the VPC and are resolved by the VPC resolver.
Outbound Resolver Endpoint - AWS queries for on-premises domains are forwarded to your on-premises DNS servers through this endpoint.
InboundResolverEndpoint:
Type: AWS::Route53Resolver::ResolverEndpoint
Properties:
Direction: INBOUND
IpAddresses:
- SubnetId: !Ref PrivateAppSubnetA
Ip: 10.0.10.10 # Fixed IP for on-premises to target
- SubnetId: !Ref PrivateAppSubnetB
Ip: 10.0.12.10
SecurityGroupIds:
- !Ref ResolverSecurityGroup
Name: inbound-resolver
OutboundResolverEndpoint:
Type: AWS::Route53Resolver::ResolverEndpoint
Properties:
Direction: OUTBOUND
IpAddresses:
- SubnetId: !Ref PrivateAppSubnetA
- SubnetId: !Ref PrivateAppSubnetB
SecurityGroupIds:
- !Ref ResolverSecurityGroup
Name: outbound-resolver
ForwardRule:
Type: AWS::Route53Resolver::ResolverRule
Properties:
DomainName: internal.company.com # Forward this domain to on-premises
RuleType: FORWARD
ResolverEndpointId: !Ref OutboundResolverEndpoint
TargetIps:
- Ip: 10.100.1.10 # On-premises DNS server
Port: 53
- Ip: 10.100.1.11
Port: 53
Name: forward-to-onprem
PrivateLink - Exposing Services Privately Across Accounts
The Problem
You have a service in VPC A that another team's VPC B needs to consume. VPC Peering would expose all of VPC A to VPC B. You only want to expose one specific service endpoint, not the entire network.
What PrivateLink Is
AWS PrivateLink allows you to expose a service in your VPC to other VPCs (in the same or different accounts) without peering, without public internet, and without exposing your VPC. Consumers connect to an Interface VPC Endpoint in their own VPC. The traffic goes through AWS's network to your service.
Consumer VPC Provider VPC
│ │
Interface Endpoint ←PrivateLink→ NLB → Your Service
(10.1.0.15) (10.0.0.0/16)
The consumer never sees your VPC's address space. They only see the private IP of the Interface Endpoint in their own VPC.
# Provider side - expose your service via an Endpoint Service
NetworkLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Type: network
Subnets:
- !Ref PrivateAppSubnetA
- !Ref PrivateAppSubnetB
Scheme: internal
EndpointService:
Type: AWS::EC2::VPCEndpointService
Properties:
NetworkLoadBalancerArns:
- !Ref NetworkLoadBalancer
AcceptanceRequired: true # You must approve each connection request
Tags:
- Key: Name
Value: my-service-endpoint
# Consumer side - in the other account
ServiceEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref ConsumerVPC
ServiceName: com.amazonaws.vpce.us-east-1.vpce-svc-abc123 # Provider's endpoint service name
VpcEndpointType: Interface
SubnetIds:
- !Ref ConsumerSubnetA
SecurityGroupIds:
- !Ref EndpointSecurityGroup
PrivateDnsEnabled: true
PrivateLink is also how AWS's own managed services (S3 via Interface Endpoint, Secrets Manager, etc.) work under the hood. Every AWS Interface VPC Endpoint uses PrivateLink.
RAM - Resource Access Manager for Cross-Account Sharing
What It Is
AWS Resource Access Manager (RAM) allows you to share AWS resources across accounts within your AWS Organisation without duplicating them. Instead of each account creating its own Transit Gateway, subnets, or Route 53 Resolver rules, one account creates them and shares via RAM.
What Can Be Shared
- Transit Gateways (most common)
- Subnets (in a shared VPC model)
- Route 53 Resolver rules
- Prefix Lists
- License Manager configurations
- Many other resource types
The Shared VPC Model
Instead of each account having its own VPC, a central networking account creates the VPC with all subnets, route tables, NAT Gateways, and VPC Endpoints. Subnets are shared to workload accounts via RAM. Resources in workload accounts launch into the shared subnets.
# In the networking account - share subnets to workload accounts
SubnetShare:
Type: AWS::RAM::ResourceShare
Properties:
Name: production-subnet-share
ResourceArns:
- !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${PrivateAppSubnetA}"
- !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${PrivateAppSubnetB}"
- !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${PrivateAppSubnetC}"
Principals:
- "123456789012" # Workload account A
- "234567890123" # Workload account B
AllowExternalPrincipals: false
Tags:
- Key: Name
Value: shared-subnets
Workload accounts can launch EC2 instances, ECS tasks, and RDS databases into the shared subnets. They do not manage the network, the networking account does. This centralises network governance while enabling workload account autonomy.
Production VPC Design - The Reference Architecture
The Complete Architecture
Putting everything together, here is the reference architecture for a production-grade, multi-account, multi-AZ AWS network:
┌─────────────────────────────────────────────────────────────────────┐
│ AWS Organisation │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Networking Account │ │
│ │ │ │
│ │ Transit Gateway (shared via RAM to all accounts) │ │
│ │ │ │ │ │ │ │
│ │ Direct Connect VPN Inspection Central │ │
│ │ Gateway GW VPC (FW) Egress VPC │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ┌─────────────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Production │ │ Staging │ │ Dev │ │
│ │ Account │ │ Account │ │ Account │ │
│ │ │ │ │ │ │ │
│ │ VPC 10.0.0.0/16│ │ 10.2.0.0 │ │ 10.3.0.0 │ │
│ │ ┌───────────┐ │ │ /16 │ │ /16 │ │
│ │ │Public /24 │ │ └──────────┘ └──────────┘ │
│ │ │ALB,NAT GW │ │ │
│ │ ├───────────┤ │ ┌──────────────────────────────────────────┐ │
│ │ │Private /23│ │ │ Shared Services Account │ │
│ │ │App Servers│ │ │ │ │
│ │ ├───────────┤ │ │ VPC 10.4.0.0/16 │ │
│ │ │Private /24│ │ │ Route 53 (Private Hosted Zones) │ │
│ │ │Databases │ │ │ VPC Endpoints (SSM, Secrets, ECR) │ │
│ │ └───────────┘ │ │ Centralised Logging │ │
│ └─────────────────┘ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Design Decisions Explained
Centralised Transit Gateway in the Networking Account - One TGW, shared to all accounts. All inter-VPC routing is controlled in one place. The networking team owns the network. Application teams own their workloads.
Three-tier subnet design in every VPC - Public (load balancers, NAT Gateways), Private App (compute), Private Data (databases). Each tier has distinct route tables and Security Groups. Databases are never in subnets with internet access.
One NAT Gateway per AZ - AZ resilience. If AZ-a fails, AZ-b and AZ-c have their own NAT Gateways.
VPC Endpoints for AWS services - S3 and DynamoDB via Gateway Endpoints (free). SSM, Secrets Manager, ECR, CloudWatch, STS via Interface Endpoints. Private subnets never need internet access to reach AWS services.
Inspection VPC for egress control - All internet-bound traffic from private subnets routes through a centralised inspection VPC with AWS Network Firewall or a third-party appliance. Centralised egress filtering, logging, and threat detection.
Shared Services VPC for common infrastructure - Route 53 Resolver endpoints, centralised logging, shared tooling. Every other VPC connects to this one through TGW.
Private Hosted Zones for all internal DNS - No hardcoded endpoints. Database connection strings use DNS names. When the underlying resource changes, update DNS. Applications do not change.
Flow Logs everywhere - VPC-level flow logs to CloudWatch Logs. 90-day retention. Athena queries for security investigations.
No SSH, no bastion hosts - SSM Session Manager provides secure shell access to private EC2 instances with full audit logging, no SSH keys, no open port 22. The SSM endpoint (Interface VPC Endpoint) provides the connectivity.
What Would Happen Without This Design
A flat VPC with one subnet where all resources share the same route table and security group. Every EC2 instance can reach every other EC2 instance. A compromised web server can connect to the database, the cache, and every other internal service. There is no network segmentation. A single breach has unrestricted lateral movement across the entire environment. Compliance audits fail. Security certifications are impossible.
Network architecture is not overhead. It is the foundation of every security control above it.
Written by Onyedikachi Obidiegwu | Cloud Security Engineer
Top comments (0)