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 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和可用区差异只体现在变量文件中。