手动登录云控制台创建服务器、数据库、网络——这种方式在资源少时还能勉强应付,一旦环境变复杂,就会出现"改不动、删不清、说不明"的困境。基础设施即代码(Infrastructure as Code, IaC)用代码描述基础设施,让资源的创建、修改和销毁变得可版本化、可审查、可复用。Terraform 是目前最主流的 IaC 工具,从理念到实战全面梳理。
参考来源:Terraform 官方文档
一、IaC 理念:声明式 vs 命令式
IaC 的核心思想是:用代码文件描述基础设施的期望状态,由工具自动驱动实际状态向期望状态收敛。
在 IaC 领域,存在两种截然不同的范式:
| 维度 | 声明式(Declarative) | 命令式(Imperative) |
|---|---|---|
| 核心理念 | 描述"要什么"(期望状态) | 描述"怎么做"(操作步骤) |
| 代表工具 | Terraform、CloudFormation、Pulumi | Ansible(部分)、Shell 脚本、AWS CLI |
| 幂等性 | 天然幂等,重复执行结果一致 | 需自行保证幂等性 |
| 状态感知 | 工具追踪当前状态,自动计算差异 | 无状态感知,按步骤执行 |
| 可读性 | 接近配置文件,易于理解 | 接近编程逻辑,灵活但复杂 |
Terraform 采用声明式范式。你只需声明"我需要 3 台 EC2、1 个 RDS、1 个 VPC",Terraform 会自动对比当前状态和期望状态的差异,生成并执行变更计划。
声明式的核心优势:幂等性。无论执行多少次
terraform apply,最终状态一致。这意味着你可以把基础设施代码纳入 Git 版本控制,通过 PR 审查变更,实现基础设施的"代码化治理"。
二、Terraform 核心概念
Terraform 的运作围绕四个核心概念展开:
开发者编写 .tf 文件
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Provider │ ←──→ │ Resource │ │ State │
│ 云厂商插件 │ │ 资源声明 │ │ 状态文件 │
└──────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ Module │
│ 模块化复用 │
└──────────────┘
2.1 Provider(提供者)
Provider 是 Terraform 与云厂商 API 之间的适配层。每个云厂商(AWS、GCP、Azure、阿里云等)都有对应的 Provider 插件,Terraform 通过它完成资源的 CRUD 操作。
2.2 Resource(资源)
Resource 是基础设施的最小描述单元。一台 EC2、一个 S3 桶、一条 DNS 记录,都是一个 Resource。每个 Resource 有类型、名称和属性块。
2.3 State(状态)
State 是 Terraform 的"记忆"。它记录了当前管理的所有资源的真实状态(ID、属性、关系)。每次 terraform apply 后,State 文件会被更新,用于下一次变更时的差异计算。State 是 Terraform 最关键也最容易出问题的组件,后文会重点讲解 State 管理策略。
2.4 Module(模块)
Module 是一组 Resource 的封装,类似于编程中的"函数"。你可以把通用的基础设施(如标准 VPC 架构)封装为 Module,在不同环境中复用。HashiCorp 官方维护了一个 Terraform Registry,提供大量社区 Module。
三、HCL 语法基础
HCL(HashiCorp Configuration Language)是 Terraform 专用配置语言。它的设计目标是"人类可读、机器可解析"。核心语法块如下:
3.1 完整的 HCL 文件结构
# ============================================
# versions.tf — Provider 版本约束
# ============================================
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# ============================================
# providers.tf — Provider 配置
# ============================================
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
Project = "sre-wang"
ManagedBy = "Terraform"
}
}
}
# ============================================
# variables.tf — 输入变量
# ============================================
variable "environment" {
description = "部署环境名称"
type = string
default = "staging"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment 只能是 dev/staging/prod。"
}
}
variable "instance_count" {
description = "EC2 实例数量"
type = number
default = 2
}
# ============================================
# main.tf — 资源声明
# ============================================
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "web-${var.environment}-${count.index + 1}"
}
}
# 数据源:查询最新 Amazon Linux AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
# ============================================
# outputs.tf — 输出值
# ============================================
output "instance_public_ips" {
description = "EC2 公网 IP 列表"
value = aws_instance.web[*].public_ip
}
output "rds_endpoint" {
description = "RDS 连接地址"
value = aws_db_instance.main.endpoint
sensitive = true
}
3.2 关键语法要素
| 语法要素 | 作用 | 示例 |
|---|---|---|
resource | 声明基础设施资源 | resource "aws_instance" "web" {} |
data | 查询已有资源(只读) | data "aws_ami" "amazon_linux" {} |
variable | 定义输入参数 | variable "environment" {} |
output | 暴露输出值供外部引用 | output "instance_public_ips" {} |
locals | 定义局部变量 | locals { common_tags = {} } |
count / for_each | 资源批量创建 | count = 3 |
dynamic | 动态生成嵌套块 | dynamic "ingress" {} |
HCL 支持丰富的内置函数(
concat、merge、jsonencode等)和表达式(三元运算、for表达式、splat 运算符[*]),完整列表参考 Terraform Functions 文档。
四、State 管理策略
State 文件是 Terraform 的命脉——它记录了资源 ID、属性映射和依赖关系。如果 State 丢失或损坏,Terraform 将无法管理已创建的资源。
4.1 为什么不能用本地 State
默认情况下,Terraform 将 State 写入本地文件 terraform.tfstate。这在团队协作中会导致严重问题:
- 冲突:多人同时
apply会互相覆盖 State - 丢失:本地文件被误删后无法恢复
- 泄露:State 文件中可能包含密码、密钥等敏感信息
4.2 远程后端:S3 + DynamoDB 锁
生产环境必须使用远程后端(Remote Backend)存储 State。AWS 上最经典的方案是 S3 存 State 文件 + DynamoDB 实现分布式锁:
# ============================================
# backend.tf — 远程 State 配置
# ============================================
terraform {
backend "s3" {
bucket = "sre-wang-terraform-state"
key = "infra/staging/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
对应的 S3 桶和 DynamoDB 表需要提前创建(这是"引导问题"——第一批基础设施总得手动创建):
# bootstrap.tf — 手动执行一次,创建 State 后端本身
resource "aws_s3_bucket" "terraform_state" {
bucket = "sre-wang-terraform-state"
lifecycle {
prevent_destroy = true # 防止误删 State 桶
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled" # 开启版本控制,支持回滚
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256" # 服务端加密
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
这套架构的工作流程:
terraform apply
│
▼
┌─────────────────┐ 获取锁 ┌──────────────┐
│ Terraform CLI │ ──────────→ │ DynamoDB │
│ │ │ LockID 写入 │
└────────┬─────────┘ └──────────────┘
│
│ 读取/写入 State
▼
┌─────────────────┐
│ S3 Bucket │
│ terraform.tfstate│ ← 版本控制 + 加密
└─────────────────┘
| 组件 | 职责 | 关键配置 |
|---|---|---|
| S3 | 存储 State 文件 | 版本控制、服务端加密、prevent_destroy |
| DynamoDB | 分布式锁,防止并发写入 | LockID 作为分区键 |
| encrypt | State 文件加密传输 | encrypt = true |
| key | State 文件路径,支持环境隔离 | infra/staging/terraform.tfstate |
通过不同的
key路径,可以为不同环境(dev/staging/prod)维护独立的 State 文件,实现环境隔离。
4.3 State 操作常用命令
# 查看 State 中所有资源
terraform state list
# 查看某个资源的详细信息
terraform state show aws_instance.web[0]
# 将资源从 State 中移除(不删除云上资源)
terraform state rm aws_instance.web[0]
# 将外部已存在的资源导入 Terraform 管理
terraform import aws_instance.web i-0123456789abcdef0
# 强制解锁(慎用!仅在锁卡住时使用)
terraform force-unlock <LOCK_ID>
terraform force-unlock是危险操作,只有在确认没有其他 Terraform 进程在运行时才能使用。错误解锁会导致 State 文件损坏。
五、Module 模块化复用实战
当基础设施超过几十个资源时,把所有代码写在一个目录里会变得难以维护。Module 的作用是把相关资源打包成可复用单元。
5.1 目录结构
terraform/
├── modules/
│ └── vpc/
│ ├── main.tf # 资源定义
│ ├── variables.tf # 模块输入参数
│ ├── outputs.tf # 模块输出值
│ └── versions.tf # 版本约束
├── environments/
│ ├── staging/
│ │ ├── main.tf # 调用模块 + 环境特有资源
│ │ ├── variables.tf # 环境变量
│ │ └── backend.tf # State 后端配置
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ └── backend.tf
5.2 Module 内部定义
# modules/vpc/variables.tf
variable "cidr" {
type = string
default = "10.0.0.0/16"
}
variable "environment" {
type = string
}
variable "availability_zones" {
type = list(string)
default = ["ap-northeast-1a", "ap-northeast-1c"]
}
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "vpc-${var.environment}"
Environment = var.environment
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "subnet-public-${var.environment}-${count.index + 1}"
}
}
# modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "igw_id" {
value = aws_internet_gateway.main.id
}
5.3 调用 Module
# environments/staging/main.tf
module "vpc" {
source = "../../modules/vpc"
cidr = "10.1.0.0/16"
environment = "staging"
availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
}
使用 Module 后,新增一个环境只需复制目录、修改变量即可,无需重复编写 VPC 代码。这极大降低了环境间配置不一致的风险。
六、实战:AWS VPC + EC2 + RDS 完整部署
下面用一个完整示例,把前面所有概念串联起来——在 AWS 上创建一个包含 VPC、EC2 和 RDS 的 Web 应用基础架构。
# ============================================
# versions.tf
# ============================================
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "sre-wang-terraform-state"
key = "infra/webapp/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = "ap-northeast-1"
}
# ============================================
# variables.tf
# ============================================
variable "environment" {
type = string
default = "staging"
}
variable "db_password" {
type = string
sensitive = true
}
# ============================================
# main.tf — VPC 网络
# ============================================
module "vpc" {
source = "../../modules/vpc"
cidr = "10.2.0.0/16"
environment = var.environment
}
# 安全组:允许 Web 流量
resource "aws_security_group" "web" {
name = "sg-web-${var.environment}"
vpc_id = module.vpc.vpc_id
description = "Web tier security group"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"] # 仅内网 SSH
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# 安全组:数据库(仅允许 Web SG 访问)
resource "aws_security_group" "db" {
name = "sg-db-${var.environment}"
vpc_id = module.vpc.vpc_id
description = "Database security group"
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# ============================================
# main.tf — EC2 Web 服务器
# ============================================
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "web" {
count = 2
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
subnet_id = module.vpc.public_subnet_ids[count.index]
vpc_security_group_ids = [aws_security_group.web.id]
associate_public_ip_address = true
user_data = <<-EOF
#!/bin/bash
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>SRE.Wang Web Server ${count.index + 1}</h1>" > /var/www/html/index.html
EOF
tags = {
Name = "web-${var.environment}-${count.index + 1}"
}
}
# ============================================
# main.tf — RDS 数据库
# ============================================
resource "aws_db_subnet_group" "main" {
name = "db-subnet-group-${var.environment}"
subnet_ids = module.vpc.public_subnet_ids
}
resource "aws_db_instance" "main" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
storage_type = "gp3"
db_name = "webapp"
username = "admin"
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
skip_final_snapshot = false
backup_retention_period = 7
tags = {
Name = "rds-${var.environment}"
}
}
# ============================================
# outputs.tf
# ============================================
output "web_public_ips" {
description = "Web 服务器公网 IP"
value = aws_instance.web[*].public_ip
}
output "rds_endpoint" {
description = "RDS 连接地址"
value = aws_db_instance.main.endpoint
sensitive = true
}
执行流程
# 1. 初始化:下载 Provider 插件、配置后端
terraform init
# 2. 格式化检查
terraform fmt -check -recursive
# 3. 静态检查
terraform validate
# 4. 生成变更计划
terraform plan -out=tfplan
# 5. 应用变更
terraform apply tfplan
# 6. 销毁资源(测试后清理)
terraform destroy
# terraform plan 输出示例
Plan: 9 to add, 0 to change, 0 to destroy.
# aws_instance.web[0] will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0d52744d6551d851e"
+ instance_type = "t3.micro"
...
}
# aws_db_instance.main will be created
+ resource "aws_db_instance" "main" {
+ engine = "mysql"
+ instance_class = "db.t3.micro"
...
}
terraform plan是 Terraform 最有价值的能力之一——它在执行前展示完整的变更计划(+新建、~修改、-销毁),让你在提交前审查变更,避免意外。
七、好的实践总结
| 维度 | 实践要点 |
|---|---|
| State 管理 | 必须使用远程后端 + 加密 + 版本控制;按环境拆分 State 文件 |
| Module 设计 | 单一职责、输入参数校验、输出明确;优先复用 Registry 社区 Module |
| 变量安全 | 敏感值标记 sensitive = true,通过环境变量或 Secrets Manager 注入 |
| 版本控制 | required_version 锁定 Terraform 版本;required_providers 锁定 Provider 版本 |
| 代码组织 | versions.tf / variables.tf / main.tf / outputs.tf 分文件管理 |
| CI 集成 | 在 CI 中运行 terraform fmt -check + validate + plan,PR 审查后再 apply |
| 防止误删 | 关键资源加 lifecycle { prevent_destroy = true } |
| 标签策略 | Provider 级 default_tags 统一标签,便于成本分摊和资源追踪 |
Terraform 的学习曲线主要在 HCL 语法和 State 管理上。一旦理解了"声明式 + 状态收敛"的核心理念,配合 Module 复用和远程 State,就能构建出可维护、可审计的基础设施代码库。建议从本文的 VPC + EC2 + RDS 示例开始动手实践,逐步把现有手动管理的基础设施迁移为 IaC。
延伸阅读:
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Terraform 官方文档 — HashiCorp,参考了Terraform 官方文档相关内容
- Terraform Registry — Registry,参考了Terraform Registry相关内容
- Terraform Functions 文档 — HashiCorp,参考了Terraform Functions 文档相关内容
- Terraform Best Practices — HashiCorp,参考了Terraform Best Practices相关内容