Links#
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/subnets1. 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 / enumerableRecommended minimum:
Project:
order
Environment:
dev / uat / prod
ManagedBy:
terraform
NetworkName:
order-prod
NetworkTier:
public / private
SubnetRole:
public-ingress / private-app / private-data / private-endpoint2. 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 route3. 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 tableCheck 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 tableCheck 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 tableExpected:
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 groups6. 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 behaviorDo not replace NetworkTier / SubnetRole with Kubernetes tags:
Kubernetes tags:
for controller discovery
NetworkTier / SubnetRole:
for Terraform, platform modules, and human network intent7. 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 filters8. 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 |