Skip to main content
Back to Blog
13 September 202515 min read

AWS VPC: Networking and Security Architecture

AWSVPCNetworkingSecurityCloud

Designing secure network architectures with AWS VPC. Subnets, route tables, security groups, NACLs, VPC endpoints, and multi-account networking strategies.


AWS VPC: Networking and Security Architecture

Amazon Virtual Private Cloud (VPC) provides isolated network environments in AWS. Designing secure, scalable VPC architectures is fundamental to cloud infrastructure. Understanding subnets, routing, security groups, and connectivity options enables building robust multi-tier applications.

VPC Architecture

Basic Components

VPC Architecture Overview:

┌─────────────────────────────────────────────────────────────────┐
│                         VPC (10.0.0.0/16)                       │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                    Availability Zone A                       ││
│  │  ┌─────────────────┐     ┌─────────────────┐                ││
│  │  │  Public Subnet  │     │ Private Subnet  │                ││
│  │  │  10.0.0.0/24    │     │  10.0.10.0/24   │                ││
│  │  │                 │     │                 │                ││
│  │  │  [NAT Gateway]  │────▶│  [EC2/Lambda]   │                ││
│  │  │  [ALB]          │     │  [RDS]          │                ││
│  │  └────────┬────────┘     └─────────────────┘                ││
│  └───────────┼──────────────────────────────────────────────────┘│
│              │                                                    │
│  ┌───────────┼──────────────────────────────────────────────────┐│
│  │           │        Availability Zone B                        ││
│  │  ┌────────▼────────┐     ┌─────────────────┐                ││
│  │  │  Public Subnet  │     │ Private Subnet  │                ││
│  │  │  10.0.1.0/24    │     │  10.0.11.0/24   │                ││
│  │  │                 │     │                 │                ││
│  │  │  [NAT Gateway]  │────▶│  [EC2/Lambda]   │                ││
│  │  │  [ALB]          │     │  [RDS]          │                ││
│  │  └────────┬────────┘     └─────────────────┘                ││
│  └───────────┼──────────────────────────────────────────────────┘│
│              │                                                    │
│              ▼                                                    │
│        [Internet Gateway]                                         │
└─────────────────────────────────────────────────────────────────┘

Subnet Strategy

Subnet TypeCIDR ExampleUse Case
Public10.0.0.0/24Load balancers, NAT gateways, bastion hosts
Private10.0.10.0/24Application servers, containers
Data10.0.20.0/24Databases, caches
Management10.0.30.0/24CI/CD, monitoring, admin tools

VPC Configuration

Terraform Module

# vpc/main.tf resource "aws_vpc" "main" { cidr_block = var.vpc_cidr enable_dns_hostnames = true enable_dns_support = true tags = merge(var.tags, { Name = "${var.project}-vpc" }) } # Internet Gateway resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id tags = merge(var.tags, { Name = "${var.project}-igw" }) } # Public Subnets resource "aws_subnet" "public" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index) availability_zone = var.availability_zones[count.index] map_public_ip_on_launch = true tags = merge(var.tags, { Name = "${var.project}-public-${var.availability_zones[count.index]}" Type = "public" }) } # Private Subnets resource "aws_subnet" "private" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10) availability_zone = var.availability_zones[count.index] tags = merge(var.tags, { Name = "${var.project}-private-${var.availability_zones[count.index]}" Type = "private" }) } # Data Subnets (for databases) resource "aws_subnet" "data" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 20) availability_zone = var.availability_zones[count.index] tags = merge(var.tags, { Name = "${var.project}-data-${var.availability_zones[count.index]}" Type = "data" }) } # Elastic IP for NAT Gateway resource "aws_eip" "nat" { count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 0 domain = "vpc" tags = merge(var.tags, { Name = "${var.project}-nat-eip-${count.index}" }) depends_on = [aws_internet_gateway.main] } # NAT Gateway resource "aws_nat_gateway" "main" { count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 0 allocation_id = aws_eip.nat[count.index].id subnet_id = aws_subnet.public[count.index].id tags = merge(var.tags, { Name = "${var.project}-nat-${count.index}" }) depends_on = [aws_internet_gateway.main] } # Public Route Table resource "aws_route_table" "public" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.main.id } tags = merge(var.tags, { Name = "${var.project}-public-rt" }) } resource "aws_route_table_association" "public" { count = length(var.availability_zones) subnet_id = aws_subnet.public[count.index].id route_table_id = aws_route_table.public.id } # Private Route Tables resource "aws_route_table" "private" { count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 0 vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.main[var.single_nat_gateway ? 0 : count.index].id } tags = merge(var.tags, { Name = "${var.project}-private-rt-${count.index}" }) } resource "aws_route_table_association" "private" { count = length(var.availability_zones) subnet_id = aws_subnet.private[count.index].id route_table_id = aws_route_table.private[var.single_nat_gateway ? 0 : count.index].id }

Security Groups

Layered Security

# security-groups.tf # ALB Security Group resource "aws_security_group" "alb" { name = "${var.project}-alb-sg" description = "Security group for Application Load Balancer" vpc_id = aws_vpc.main.id ingress { description = "HTTPS from anywhere" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { description = "HTTP from anywhere (redirect to HTTPS)" from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { description = "All traffic to VPC" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = [var.vpc_cidr] } tags = merge(var.tags, { Name = "${var.project}-alb-sg" }) } # Application Security Group resource "aws_security_group" "app" { name = "${var.project}-app-sg" description = "Security group for application servers" vpc_id = aws_vpc.main.id ingress { description = "HTTP from ALB" from_port = 8080 to_port = 8080 protocol = "tcp" security_groups = [aws_security_group.alb.id] } egress { description = "HTTPS for AWS services" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { description = "Database access" from_port = 5432 to_port = 5432 protocol = "tcp" security_groups = [aws_security_group.database.id] } egress { description = "Redis access" from_port = 6379 to_port = 6379 protocol = "tcp" security_groups = [aws_security_group.cache.id] } tags = merge(var.tags, { Name = "${var.project}-app-sg" }) } # Database Security Group resource "aws_security_group" "database" { name = "${var.project}-db-sg" description = "Security group for RDS database" vpc_id = aws_vpc.main.id ingress { description = "PostgreSQL from app servers" from_port = 5432 to_port = 5432 protocol = "tcp" security_groups = [aws_security_group.app.id] } tags = merge(var.tags, { Name = "${var.project}-db-sg" }) } # Cache Security Group resource "aws_security_group" "cache" { name = "${var.project}-cache-sg" description = "Security group for ElastiCache" vpc_id = aws_vpc.main.id ingress { description = "Redis from app servers" from_port = 6379 to_port = 6379 protocol = "tcp" security_groups = [aws_security_group.app.id] } tags = merge(var.tags, { Name = "${var.project}-cache-sg" }) }

Network ACLs

Subnet-Level Security

# nacls.tf # Public Subnet NACL resource "aws_network_acl" "public" { vpc_id = aws_vpc.main.id subnet_ids = aws_subnet.public[*].id # Inbound Rules ingress { protocol = "tcp" rule_no = 100 action = "allow" cidr_block = "0.0.0.0/0" from_port = 443 to_port = 443 } ingress { protocol = "tcp" rule_no = 110 action = "allow" cidr_block = "0.0.0.0/0" from_port = 80 to_port = 80 } ingress { protocol = "tcp" rule_no = 120 action = "allow" cidr_block = "0.0.0.0/0" from_port = 1024 to_port = 65535 # Ephemeral ports } # Outbound Rules egress { protocol = "tcp" rule_no = 100 action = "allow" cidr_block = "0.0.0.0/0" from_port = 443 to_port = 443 } egress { protocol = "tcp" rule_no = 110 action = "allow" cidr_block = "0.0.0.0/0" from_port = 80 to_port = 80 } egress { protocol = "tcp" rule_no = 120 action = "allow" cidr_block = var.vpc_cidr from_port = 8080 to_port = 8080 } egress { protocol = "tcp" rule_no = 130 action = "allow" cidr_block = "0.0.0.0/0" from_port = 1024 to_port = 65535 } tags = merge(var.tags, { Name = "${var.project}-public-nacl" }) } # Private Subnet NACL resource "aws_network_acl" "private" { vpc_id = aws_vpc.main.id subnet_ids = aws_subnet.private[*].id # Allow traffic from public subnets ingress { protocol = "tcp" rule_no = 100 action = "allow" cidr_block = var.vpc_cidr from_port = 8080 to_port = 8080 } # Allow return traffic ingress { protocol = "tcp" rule_no = 110 action = "allow" cidr_block = "0.0.0.0/0" from_port = 1024 to_port = 65535 } # Allow outbound to anywhere egress { protocol = "-1" rule_no = 100 action = "allow" cidr_block = "0.0.0.0/0" from_port = 0 to_port = 0 } tags = merge(var.tags, { Name = "${var.project}-private-nacl" }) }

VPC Endpoints

Private AWS Service Access

# endpoints.tf # S3 Gateway Endpoint (free) resource "aws_vpc_endpoint" "s3" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.s3" vpc_endpoint_type = "Gateway" route_table_ids = concat( [aws_route_table.public.id], aws_route_table.private[*].id ) tags = merge(var.tags, { Name = "${var.project}-s3-endpoint" }) } # DynamoDB Gateway Endpoint (free) resource "aws_vpc_endpoint" "dynamodb" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.dynamodb" vpc_endpoint_type = "Gateway" route_table_ids = aws_route_table.private[*].id tags = merge(var.tags, { Name = "${var.project}-dynamodb-endpoint" }) } # Security Group for Interface Endpoints resource "aws_security_group" "vpc_endpoints" { name = "${var.project}-vpc-endpoints-sg" description = "Security group for VPC endpoints" vpc_id = aws_vpc.main.id ingress { description = "HTTPS from VPC" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = [var.vpc_cidr] } tags = merge(var.tags, { Name = "${var.project}-vpc-endpoints-sg" }) } # ECR API Endpoint resource "aws_vpc_endpoint" "ecr_api" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.ecr.api" vpc_endpoint_type = "Interface" subnet_ids = aws_subnet.private[*].id security_group_ids = [aws_security_group.vpc_endpoints.id] private_dns_enabled = true tags = merge(var.tags, { Name = "${var.project}-ecr-api-endpoint" }) } # ECR DKR Endpoint resource "aws_vpc_endpoint" "ecr_dkr" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.ecr.dkr" vpc_endpoint_type = "Interface" subnet_ids = aws_subnet.private[*].id security_group_ids = [aws_security_group.vpc_endpoints.id] private_dns_enabled = true tags = merge(var.tags, { Name = "${var.project}-ecr-dkr-endpoint" }) } # CloudWatch Logs Endpoint resource "aws_vpc_endpoint" "logs" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.logs" vpc_endpoint_type = "Interface" subnet_ids = aws_subnet.private[*].id security_group_ids = [aws_security_group.vpc_endpoints.id] private_dns_enabled = true tags = merge(var.tags, { Name = "${var.project}-logs-endpoint" }) } # Secrets Manager Endpoint resource "aws_vpc_endpoint" "secretsmanager" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.secretsmanager" vpc_endpoint_type = "Interface" subnet_ids = aws_subnet.private[*].id security_group_ids = [aws_security_group.vpc_endpoints.id] private_dns_enabled = true tags = merge(var.tags, { Name = "${var.project}-secretsmanager-endpoint" }) }

VPC Flow Logs

Network Traffic Monitoring

# flow-logs.tf resource "aws_flow_log" "main" { iam_role_arn = aws_iam_role.flow_logs.arn log_destination = aws_cloudwatch_log_group.flow_logs.arn traffic_type = "ALL" vpc_id = aws_vpc.main.id tags = merge(var.tags, { Name = "${var.project}-flow-logs" }) } resource "aws_cloudwatch_log_group" "flow_logs" { name = "/aws/vpc-flow-logs/${var.project}" retention_in_days = 30 tags = var.tags } resource "aws_iam_role" "flow_logs" { name = "${var.project}-flow-logs-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "vpc-flow-logs.amazonaws.com" } }] }) } resource "aws_iam_role_policy" "flow_logs" { name = "${var.project}-flow-logs-policy" role = aws_iam_role.flow_logs.id policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams" ] Effect = "Allow" Resource = "*" }] }) }

Multi-Account Networking

Transit Gateway

# transit-gateway.tf resource "aws_ec2_transit_gateway" "main" { description = "${var.project} Transit Gateway" default_route_table_association = "disable" default_route_table_propagation = "disable" dns_support = "enable" vpn_ecmp_support = "enable" tags = merge(var.tags, { Name = "${var.project}-tgw" }) } resource "aws_ec2_transit_gateway_vpc_attachment" "main" { subnet_ids = aws_subnet.private[*].id transit_gateway_id = aws_ec2_transit_gateway.main.id vpc_id = aws_vpc.main.id tags = merge(var.tags, { Name = "${var.project}-tgw-attachment" }) } # Share Transit Gateway with other accounts resource "aws_ram_resource_share" "tgw" { name = "${var.project}-tgw-share" allow_external_principals = false tags = var.tags } resource "aws_ram_resource_association" "tgw" { resource_arn = aws_ec2_transit_gateway.main.arn resource_share_arn = aws_ram_resource_share.tgw.arn } resource "aws_ram_principal_association" "org" { principal = var.organization_arn resource_share_arn = aws_ram_resource_share.tgw.arn }

Key Takeaways

  1. Plan CIDR blocks: Use non-overlapping ranges for VPC peering/Transit Gateway

  2. Subnet strategy: Separate public, private, and data tiers

  3. Security groups first: Use security groups as primary defence, NACLs for subnet-level rules

  4. VPC endpoints: Reduce NAT Gateway costs and improve security

  5. Flow logs: Enable for troubleshooting and security analysis

  6. High availability: Deploy across multiple availability zones

  7. NAT Gateway costs: Consider single NAT for dev, multi-AZ for production

  8. Transit Gateway: Simplify multi-VPC and multi-account networking

Proper VPC design is foundational to secure, scalable AWS architectures. Invest time in planning your network topology before deployment.

Share this article