4. 使用Terraform定义VPC

在完成AWS凭证配置与Terraform后端准备之后,本节真正开始用代码描述网络。目标不是把控制台点击动作翻译成HCL,而是建立一套可重复、可审查、可演进的VPC定义方式:用资源块描述网络对象,用变量控制环境差异,用输出暴露关键标识,用状态文件记录资源与代码的映射关系,最后通过一次完整的apply得到可用的基础拓扑。

一、最小可用的VPC资源块

VPC是区域级资源,创建时必须指定IPv4 CIDR。下面这段配置定义了一个10.0.0.0/16的VPC,并开启DNS主机名与DNS解析,便于后续EC2实例通过域名访问服务:

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name        = "${var.project}-${var.environment}-vpc"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

这里没有把CIDR写死,而是引用var.vpc_cidr。原因很直接:同一份代码要能部署到dev、staging、prod,如果CIDR硬编码,环境之间就会冲突或需要复制代码。资源块中的tags也不是装饰,它决定了资源在控制台和成本报告中是否可识别,建议至少保留Name、Environment、ManagedBy三个键。

二、变量:把环境差异从代码中抽离

变量定义通常放在variables.tf中。对于VPC这一层,真正需要参数化的字段并不多,关键是CIDR、项目名、环境名和可用区列表:

variable "vpc_cidr" {
  description = "VPC IPv4 CIDR block"
  type        = string
  default     = "10.0.0.0/16"

  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "vpc_cidr must be a valid IPv4 CIDR."
  }
}

variable "project" {
  type    = string
  default = "netlab"
}

variable "environment" {
  type    = string
  default = "dev"
}

variable "azs" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b"]
}

变量不是越多越好。像VPC ID、路由表ID这类由AWS生成的值不应做成输入变量,而应通过资源引用或输出传递。变量只承载“部署前需要人为决定”的内容,例如网段规划、环境名称、可用区选择。这样变量文件才具有可读性,不会变成一张难以维护的参数表。

三、输出:让下游模块和运维人员拿到关键标识

VPC创建完成后,最常被引用的值是VPC ID、主路由表ID以及后续子网的CIDR。输出块把这些值稳定地暴露出来:

output "vpc_id" {
  description = "ID of the created VPC"
  value       = aws_vpc.main.id
}

output "vpc_cidr" {
  value = aws_vpc.main.cidr_block
}

output "default_route_table_id" {
  value = aws_vpc.main.default_route_table_id
}

输出有两个用途:一是供同一配置内的其他资源引用,二是供terraform output命令或远程状态读取。尤其在拆分模块后,子网模块需要通过module.vpc.vpc_id拿到VPC ID,而不是重新查询。输出名应保持语义稳定,避免频繁改名导致下游引用断裂。

四、状态管理:代码与真实资源的映射账本

Terraform状态文件记录了每个资源块与AWS真实资源ID的对应关系。执行terraform apply后,状态中会出现类似aws_vpc.main对应vpc-0a1b2c3d4e5f67890的映射。这个映射决定了后续plan时Terraform是“更新已有资源”还是“新建资源”。

本地状态只适合单人练习。一旦多人协作或使用CI/CD,必须把状态放到远程后端,例如S3加DynamoDB锁表。远程状态解决三个问题:状态不随本地机器丢失、多人操作时有锁避免并发写坏、状态文件中的敏感信息可通过S3加密和IAM策略控制访问。需要特别注意的是,状态文件可能包含资源属性甚至敏感值,不应提交到Git仓库。

五、基本网络拓扑部署

仅有VPC还不能承载实例。一个可用的最小拓扑至少包括:VPC、两个公有子网(分布在不同可用区)、一个互联网网关、一张公有路由表,以及子网与路由表的关联。下面用变量驱动的方式补齐这些资源:

resource "aws_subnet" "public" {
  count             = length(var.azs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = var.azs[count.index]

  tags = {
    Name = "${var.project}-${var.environment}-public-${count.index + 1}"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
}

resource "aws_route_table_association" "public" {
  count          = length(aws_subnet.public)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

这里使用cidrsubnet(var.vpc_cidr, 8, count.index)把10.0.0.0/16切成/24子网:第一个子网为10.0.0.0/24,第二个为10.0.1.0/24。这种切分方式让子网CIDR随VPC CIDR自动变化,避免手工计算错误。两个子网分布在不同可用区,为后续负载均衡和高可用部署留出空间。

Terraform 定义 VPC 基础拓扑与状态映射 AWS 区域 us-east-1 VPC 10.0.0.0/16 公有子网 A 10.0.0.0/24 us-east-1a 公有子网 B 10.0.1.0/24 us-east-1b 公有路由表 0.0.0.0/0 → IGW Internet Gateway Terraform 状态与代码映射 variables.tf vpc_cidr / project / environment / azs main.tf aws_vpc / aws_subnet / aws_igw / aws_route_table outputs.tf vpc_id / vpc_cidr / default_route_table_id terraform.tfstate aws_vpc.main → vpc-0a1b2c3d4e5f67890 变量驱动资源定义,输出暴露标识,状态记录代码与真实资源的映射

六、执行顺序与结果判断

配置写完后,标准执行顺序是terraform init、terraform fmt、terraform validate、terraform plan、terraform apply。init负责下载AWS Provider并初始化后端;fmt统一格式;validate检查语法和变量引用;plan展示将要创建的资源清单;apply才真正调用AWS API。

plan输出中应重点核对三类信息:一是资源数量,例如“Plan: 6 to add, 0 to change, 0 to destroy”;二是CIDR是否符合规划,例如VPC为10.0.0.0/16、两个子网为10.0.0.0/24和10.0.1.0/24;三是是否有意外替换,例如某个资源显示“must be replaced”。如果plan中出现大量非预期变更,应先停止apply,检查变量文件或状态是否被改动。

apply完成后,可用以下命令验证结果:

terraform output vpc_id
aws ec2 describe-vpcs --vpc-ids $(terraform output -raw vpc_id)
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$(terraform output -raw vpc_id)"

预期结果是:VPC状态为available,两个子网分别位于us-east-1a和us-east-1b,且已关联到公有路由表。此时基础网络拓扑已经具备承载公有子网实例的条件。若后续要部署私有子网,只需在同一VPC下增加私有子网、NAT网关和私有路由表,而不需要重建VPC。

变量文件建议按环境拆分,例如dev.tfvars、prod.tfvars,执行时用terraform apply -var-file=dev.tfvars。这样同一份资源代码可以部署到不同环境,而CIDR和可用区差异只体现在变量文件中。
不要手工在控制台修改由Terraform管理的VPC资源。手工改动不会写回状态文件,下一次plan时Terraform可能尝试“纠正”这些改动,甚至重建资源。所有变更都应通过代码和apply完成。