AWS VPC


https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html
https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html
https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-subnets.html
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnets

1. Important Points#

VPC tag 设计的目标不是让 console 看起来漂亮,而是让 automation 能稳定选择正确的 VPC / subnet / route table。

core rule:
    Name is for humans
    structured tags are for Terraform filters

do not:
    only use Name=prod-public-a
    parse public/private from subnet name
    use one vague tag like Type=public everywhere
    let each module invent its own tag key

do:
    tag VPC, subnet, route table, NAT gateway consistently
    use exact-match tag values
    separate environment, system, network tier, subnet role, and AZ
    keep values lowercase / short / enumerable

Recommended minimum:

Project:
    order

Environment:
    dev / uat / prod

ManagedBy:
    terraform

NetworkName:
    order-prod

NetworkTier:
    public / private

SubnetRole:
    public-ingress / private-app / private-data / private-endpoint

2. Tag Schema#

common tags#

Apply these to VPC and all child network resources:

Tag Example Purpose
Project order product / workload boundary
Environment prod environment filter
ManagedBy terraform ownership / audit
Owner platform team ownership
NetworkName order-prod one logical VPC network name

vpc tags#

Tag Example Purpose
Name order-prod-vpc human readable name
NetworkName order-prod stable lookup key
NetworkType workload workload / shared-services / inspection
CidrFamily ipv4 ipv4 / dualstack

subnet tags#

Use NetworkTier for public/private split. Use SubnetRole for workload placement.

Tag Public Subnet Private App Subnet Private Data Subnet
Name order-prod-public-ap-east-1a order-prod-private-app-ap-east-1a order-prod-private-data-ap-east-1a
NetworkTier public private private
SubnetRole public-ingress private-app private-data
RouteScope internet nat isolated
AvailabilityZone ap-east-1a ap-east-1a ap-east-1a

Meaning:

NetworkTier:
    public:
        subnet route table has 0.0.0.0/0 or ::/0 to Internet Gateway
        used by public ALB / NAT Gateway / bastion only when needed

    private:
        no direct route to Internet Gateway
        used by ECS tasks, EC2 app instances, RDS, internal ALB, VPC endpoints

SubnetRole:
    public-ingress:
        internet-facing ALB / NAT Gateway

    private-app:
        application compute such as ECS / EKS / EC2

    private-data:
        database/cache/internal stateful resources

    private-endpoint:
        interface VPC endpoint ENIs

RouteScope:
    internet:
        default route to Internet Gateway

    nat:
        default route to NAT Gateway

    isolated:
        no default internet egress route

3. Terraform Create Tags#

Use default_tags for tags that apply to every AWS resource, then resource-specific tags for VPC/subnet semantics.

locals {
  project      = "order"
  environment  = "prod"
  region       = "ap-east-1"
  network_name = "${local.project}-${local.environment}"

  common_tags = {
    Project     = local.project
    Environment = local.environment
    ManagedBy   = "terraform"
    Owner       = "platform"
    NetworkName = local.network_name
  }

  public_subnets = {
    "ap-east-1a" = "10.30.0.0/24"
    "ap-east-1b" = "10.30.1.0/24"
  }

  private_app_subnets = {
    "ap-east-1a" = "10.30.10.0/24"
    "ap-east-1b" = "10.30.11.0/24"
  }

  private_data_subnets = {
    "ap-east-1a" = "10.30.20.0/24"
    "ap-east-1b" = "10.30.21.0/24"
  }
}

provider "aws" {
  region = local.region

  default_tags {
    tags = local.common_tags
  }
}

resource "aws_vpc" "main" {
  cidr_block           = "10.30.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "${local.network_name}-vpc"
    NetworkType = "workload"
    CidrFamily  = "ipv4"
  }
}

Public subnet:

resource "aws_subnet" "public" {
  for_each = local.public_subnets

  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value
  availability_zone       = each.key
  map_public_ip_on_launch = false

  tags = {
    Name             = "${local.network_name}-public-${each.key}"
    NetworkTier      = "public"
    SubnetRole       = "public-ingress"
    RouteScope       = "internet"
    AvailabilityZone = each.key
  }
}

Private app subnet:

resource "aws_subnet" "private_app" {
  for_each = local.private_app_subnets

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value
  availability_zone = each.key

  tags = {
    Name             = "${local.network_name}-private-app-${each.key}"
    NetworkTier      = "private"
    SubnetRole       = "private-app"
    RouteScope       = "nat"
    AvailabilityZone = each.key
  }
}

Private data subnet:

resource "aws_subnet" "private_data" {
  for_each = local.private_data_subnets

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value
  availability_zone = each.key

  tags = {
    Name             = "${local.network_name}-private-data-${each.key}"
    NetworkTier      = "private"
    SubnetRole       = "private-data"
    RouteScope       = "isolated"
    AvailabilityZone = each.key
  }
}

4. Terraform Filter#

Use exact tags in data sources. Do not filter only by Name=*private* unless it is an emergency migration script.

find vpc#

data "aws_vpc" "selected" {
  tags = {
    Project     = "order"
    Environment = "prod"
    NetworkName = "order-prod"
  }
}

public subnets for internet-facing alb#

data "aws_subnets" "public_ingress" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.selected.id]
  }

  tags = {
    NetworkTier = "public"
    SubnetRole  = "public-ingress"
  }
}

resource "aws_lb" "public" {
  name               = "order-prod-public"
  load_balancer_type = "application"
  internal           = false
  subnets            = data.aws_subnets.public_ingress.ids
}

private app subnets for ecs / eks / ec2#

data "aws_subnets" "private_app" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.selected.id]
  }

  tags = {
    NetworkTier = "private"
    SubnetRole  = "private-app"
  }
}

resource "aws_ecs_service" "api" {
  name = "order-api"

  network_configuration {
    subnets          = data.aws_subnets.private_app.ids
    security_groups  = [aws_security_group.api.id]
    assign_public_ip = false
  }
}

private data subnets for rds subnet group#

data "aws_subnets" "private_data" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.selected.id]
  }

  tags = {
    NetworkTier = "private"
    SubnetRole  = "private-data"
  }
}

resource "aws_db_subnet_group" "main" {
  name       = "order-prod-db"
  subnet_ids = data.aws_subnets.private_data.ids
}

5. AWS CLI Verify#

Check VPC:

aws ec2 describe-vpcs \
  --filters \
    "Name=tag:Project,Values=order" \
    "Name=tag:Environment,Values=prod" \
    "Name=tag:NetworkName,Values=order-prod" \
  --query "Vpcs[].{VpcId:VpcId,Cidr:CidrBlock,Tags:Tags}" \
  --output table

Check public subnets:

aws ec2 describe-subnets \
  --filters \
    "Name=vpc-id,Values=vpc-0123456789abcdef0" \
    "Name=tag:NetworkTier,Values=public" \
    "Name=tag:SubnetRole,Values=public-ingress" \
  --query "Subnets[].{SubnetId:SubnetId,AZ:AvailabilityZone,Cidr:CidrBlock,Name:Tags[?Key=='Name']|[0].Value}" \
  --output table

Check private app subnets:

aws ec2 describe-subnets \
  --filters \
    "Name=vpc-id,Values=vpc-0123456789abcdef0" \
    "Name=tag:NetworkTier,Values=private" \
    "Name=tag:SubnetRole,Values=private-app" \
  --query "Subnets[].{SubnetId:SubnetId,AZ:AvailabilityZone,Cidr:CidrBlock,RouteScope:Tags[?Key=='RouteScope']|[0].Value}" \
  --output table

Expected:

public-ingress:
    selected only public subnets
    used by internet-facing ALB and NAT Gateway placement

private-app:
    selected only app compute subnets
    used by ECS / EKS / EC2 workloads

private-data:
    selected only data subnets
    used by RDS / ElastiCache subnet groups

6. Kubernetes / Load Balancer Tags#

If VPC is used by EKS, Kubernetes controllers may also require subnet discovery tags. Keep them separate from your own selection tags.

Common pattern:

kubernetes.io/role/elb = 1
    public subnets for internet-facing load balancers

kubernetes.io/role/internal-elb = 1
    private subnets for internal load balancers

kubernetes.io/cluster/<cluster-name> = shared
    cluster ownership / discovery boundary, depending on controller behavior

Do not replace NetworkTier / SubnetRole with Kubernetes tags:

Kubernetes tags:
    for controller discovery

NetworkTier / SubnetRole:
    for Terraform, platform modules, and human network intent

7. Tagging Checklist#

VPC:
    Name
    Project
    Environment
    ManagedBy
    Owner
    NetworkName
    NetworkType

subnet:
    Name
    Project
    Environment
    ManagedBy
    Owner
    NetworkName
    NetworkTier
    SubnetRole
    RouteScope
    AvailabilityZone

route table:
    Name
    NetworkName
    NetworkTier
    RouteScope

review:
    every subnet has exactly one NetworkTier
    every subnet has exactly one SubnetRole
    public subnet route table has Internet Gateway route
    private-app subnet route table uses NAT or VPC endpoints as designed
    private-data subnet route table is isolated unless there is an approved exception
    Terraform data filters include vpc-id plus tag filters

8. Common Mistakes#

Mistake Risk Better
Name=prod-private-a only Terraform has to parse names add NetworkTier=private, SubnetRole=private-app
one tag Type=private unclear if app/data/endpoint split NetworkTier and SubnetRole
filter subnets without vpc-id may select same tags from another VPC always include vpc-id
public subnet has map_public_ip_on_launch=true by default accidental public IP on EC2 keep false unless required
RDS uses all private subnets database may land in app subnet filter SubnetRole=private-data
EKS tags used as generic platform tags controller intent mixed with platform intent keep Kubernetes discovery tags separate