4.15 Terraform 基础设施即代码入门
预计阅读时间:15 分钟
📖 目录
学习目标
完成本章后,你将能够:
- 理解基础设施即代码(IaC)的核心概念与价值
- 掌握 HCL 语法,编写 Provider、Resource、Data Source 定义
- 独立执行
init → plan → apply全流程 - 理解 State 文件的作用并配置远程 backend
- 设计可复用的 Terraform 模块
- 区分 Terraform 与 Ansible 的适用场景并配合使用
核心知识
| 概念 | 说明 |
|---|---|
| IaC | 用代码定义和管理基础设施,替代手动点击云控制台 |
| HCL | HashiCorp Configuration Language,Terraform 的声明式配置语言 |
| Provider | 连接 Terraform 与云平台(阿里云、AWS、Azure)的插件 |
| Resource | Terraform 管理的基础设施对象(VPC、ECS、RDS) |
| Data Source | 读取已有云资源信息,不创建新资源 |
| State | 存储资源 ID 与真实云资源的映射关系,Terraform 的"大脑" |
| Module | 可复用的 Terraform 配置包,类似编程语言的函数库 |
| Plan / Apply | Plan 预览变更,Apply 执行变更 |
知识关联
- 前置知识:6.10:云网络基础 云网络基础(VPC/CIDR 概念)、4.1:Ansible 自动化 Ansible 自动化配置管理入门(IaC 对比参照)
- 后续影响:6.7:GitOps 入门 GitOps 入门(Git 驱动基础设施)、6.10:云网络基础 云网络基础(Terraform 管理云网络资源)
- 配套技术:Terraform + Ansible 互补(Provisioning vs Configuration),State 远程 backend(OSS/S3)实现团队协作
原理讲解
声明式 vs 命令式:Terraform 采用声明式模型——你定义"目标状态",Terraform 自动计算当前状态到目标状态的 diff 并执行。Ansible 则用命令式 Playbook 描述"怎么做"。
State 机制:Terraform 通过 terraform.tfstate 跟踪已创建的资源。每次 apply 时,Terraform 读取 State 文件对比 .tf 配置,生成增量变更计划。State 是 Terraform 的命脉——损坏或丢失 State 会导致 Terraform "失忆",无法管理已有资源。
工作流:init(安装 Provider 插件)→ plan(预览,只读)→ apply(执行,写入 State)。多人团队需远程 backend 共享 State 并加锁防止并发冲突。
核心概念关系图
Terraform 的五个核心概念环环相扣,理解它们的关系比背命令更重要:
- Provider 是"翻译官":把统一的 HCL 声明翻译成云平台 API 调用(阿里云、AWS、Azure、腾讯云各有各的 provider 插件)。一个配置里可以声明多个 provider,统一管理多云资源。
- Resource 是"资产清单":每个
resource块对应一朵真实的云资源(VPC、交换机、ECS、RDS、安全组)。 - Data Source 是"只读查询":读取已有资源的信息供 Resource 引用——查镜像 ID、查现有 VPC、查可用区列表。它不创建、不修改任何东西,只提供数据。
- State 是"账本":保存每朵资源的唯一 ID 和属性快照。apply 时 Terraform 用 State 对比配置算出 diff——没有 State 就不知道"现在有什么"。
- Module 是"函数库":把一组 Resource 打包成可复用模块,用输入参数控制差异。
典型引用链:data.alicloud_images.ubuntu 查到镜像 ID → 传给 alicloud_instance.web.image_id → 实例创建后输出 IP → 交给 Ansible inventory 做配置管理。这五者的配合构成了 IaC 的完整闭环。
Plan / Apply 执行流程
terraform apply 内部并非"照着配置直接建",而是分六步:
- Refresh:向云 API 查询 State 中记录资源的真实状态;
- Diff:把 .tf 配置与 State 对比,找出新增/修改/删除的资源;
- 依赖排序:按引用关系拓扑排序(VPC 先于交换机,交换机先于 ECS);
- 输出计划:plan 展示每步动作(+ 创建 / ~ 更新 / - 删除),人工确认;
- Apply:按序调用 API,每步成功才更新 State,失败即停止(部分 Provider 支持
-target精确执行); - 输出结果:apply 成功后打印 output 值(如实例公网 IP)。
terraform plan # 输出: Plan: 3 to add, 0 to change, 0 to destroy.
terraform apply --auto-approve # 输出: Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
terraform apply -target=alicloud_instance.web # 只执行某个资源(慎用,可能破坏依赖顺序)
Terraform vs Ansible 职责划分
| 维度 | Terraform | Ansible |
|---|---|---|
| 定位 | Provisioning(建资源) | Configuration Management(管配置) |
| 模型 | 声明式(定义目标状态) | 命令式 Playbook(描述执行步骤) |
| 有状态? | 有(State 记录资源 ID) | 无(幂等执行,每次从头跑) |
| 生命周期 | 创建/更新/销毁资源 | 不销毁资源,只管内部配置 |
| 典型任务 | VPC、ECS、RDS、安全组、DNS 记录 | 装软件、改配置、起服务、下发密钥 |
| 执行顺序 | 依赖图自动排序 | 按 Playbook 书写顺序 |
| 云平台集成 | 原生 Provider 插件 | 通过 cloud modules 间接支持 |
推荐组合:Terraform 创建 ECS 并输出 IP → Ansible dynamic inventory(AWS EC2 plugin 或 terraform-inventory)读取实例列表 → Ansible 安装 Nginx、部署代码、下发配置。一条流水线:terraform apply && ansible-playbook -i inventory.ini site.yml。
为什么 Terraform 要用状态文件
Terraform 的 State 文件(terraform.tfstate)是其声明式模型的核心基础设施。没有 State,Terraform 无法回答三个关键问题:"现在有什么?"——State 记录了每个 Resource 的唯一 ID(如 i-0abc123),没有 ID 就无法调用云 API 查询或修改资源;"上次改了什么?"——apply 时 Terraform 对比 .tf 配置与 State 中记录的上一次状态,计算出增量 diff(新增/修改/删除),而非每次重新创建所有资源;"资源之间什么关系?"——State 保存了资源的属性值(如 VPC 的 CIDR、ECS 的 IP),其他 Resource 可以通过 alicloud_vpc.main.id 引用这些值。如果 State 丢失或损坏,Terraform 对已有资源"失忆"——它不知道这些资源的存在,再次 apply 可能尝试重复创建。因此 State 文件必须版本化存储(远程 backend + 锁),并禁止手动编辑。
Terraform vs Pulumi vs CloudFormation:IaC 方案对比
| 维度 | Terraform | Pulumi | CloudFormation |
|---|---|---|---|
| 语言 | HCL(专用 DSL) | 通用语言(Python/TypeScript/Go) | YAML/JSON(模板) |
| 云覆盖 | 多云(AWS/Azure/GCP/阿里云) | 多云(通过 Provider) | 仅 AWS |
| 状态管理 | State 文件(本地/远程) | State 文件(Pulumi Cloud/本地) | CloudFormation 服务端托管 |
| 学习曲线 | 中(需学 HCL) | 低(用熟悉的编程语言) | 低(YAML 模板) |
| 可编程性 | 有限(HCL 循环/条件有限) | 高(完整编程语言能力) | 低(模板函数有限) |
| 社区生态 | 最大(Registry 3000+ Provider) | 增长中 | 仅 AWS 生态 |
| 适用场景 | 多云、团队协作、主流选择 | 需要复杂逻辑的 IaC | 纯 AWS、CloudFormation 深度集成 |
选型建议:多云或需要最大社区支持 → Terraform;团队熟悉编程语言且需要复杂逻辑 → Pulumi;纯 AWS 且希望零运维 → CloudFormation。
为什么 Terraform 推荐 init → plan → apply 三步工作流
很多初学者觉得 terraform apply --auto-approve 更方便,但官方推荐严格遵循三步工作流:init——下载 Provider 插件和模块依赖,确保本地环境与配置匹配;plan——生成变更计划(+ 创建 / ~ 更新 / - 删除),这是唯一的安全检查点——在真正执行前预览所有变更,避免误操作;apply——确认 plan 结果后执行变更。plan 的价值在于"只读预览":它向云 API 查询资源真实状态,与 .tf 配置对比后输出精确的 diff。如果跳过 plan 直接 apply,你无法在执行前知道 Terraform 将要创建、修改或删除哪些资源——这在生产环境中是不可接受的风险。团队协作时 plan 输出还可以作为变更审批的依据。
示例代码
# Provider 配置(声明使用阿里云,region 决定资源部署地域)
provider "alicloud" {
region = "cn-hangzhou"
}
# Data Source:查询可用镜像(只读查询,不创建资源)
data "alicloud_images" "ubuntu" {
name_regex = "^ubuntu_22.*"
}
# Resource:创建 VPC(虚拟私有云,类似机房的逻辑隔离网络)
resource "alicloud_vpc" "main" {
vpc_name = "my-vpc"
cidr_block = "10.0.0.0/8"
}
# Resource:创建交换机(VPC 内的子网,引用 VPC 的 ID 建立依赖关系)
resource "alicloud_vswitch" "main" {
vpc_id = alicloud_vpc.main.id
cidr_block = "10.0.1.0/24"
zone_id = "cn-hangzhou-b"
}
# Resource:创建 ECS 实例(引用 Data Source 查到的镜像 ID 和交换机 ID)
resource "alicloud_instance" "web" {
image_id = data.alicloud_images.ubuntu.images[0].id
instance_type = "ecs.t6-c1m1.large"
vswitch_id = alicloud_vswitch.main.id
instance_name = "web-server"
}
# Output:apply 成功后打印结果(如实例公网 IP)
output "instance_ip" {
value = alicloud_instance.web.public_ip
}
# 模块化示例:modules/vpc/main.tf
variable "env" {
type = string
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
resource "alicloud_vpc" "this" {
vpc_name = "vpc-${var.env}"
cidr_block = var.vpc_cidr
}
resource "alicloud_vswitch" "public" {
count = 2
vpc_id = alicloud_vpc.this.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
zone_id = "cn-hangzhou-${count.index == 0 ? "b" : "c"}"
}
output "vpc_id" {
value = alicloud_vpc.this.id
}
# 使用模块
module "vpc_dev" {
source = "./modules/vpc"
env = "dev"
vpc_cidr = "10.10.0.0/16"
}
# Data Source 示例:查询已有的 VPC
data "alicloud_vpcs" "existing" {
name_regex = "my-vpc"
}
output "existing_vpc_id" {
value = data.alicloud_vpcs.existing.vpcs[0].id
}
完整实战:AWS EC2 + 安全组
以 AWS 为例走一遍完整生命周期(云厂商只是 provider 差异,概念与阿里云一一对应):
# main.tf —— VPC + 子网 + 安全组 + EC2 实例
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1" # 东京区域
}
# Data Source:查询最新的 Ubuntu 24.04 官方镜像
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical 官方账号
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-24.04-amd64-server-*"]
}
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "main-vpc" }
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "Allow HTTP/SSH from office"
vpc_id = aws_vpc.main.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["203.0.113.0/24"] # 只放行办公网段,禁止 0.0.0.0/0
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.small"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
key_name = "ops-key"
user_data = <<-EOF
#!/bin/bash
apt update && apt install -y nginx
EOF
tags = { Name = "web-01" }
}
output "web_ip" {
value = aws_instance.web.public_ip
}
# 完整生命周期操作
terraform init # 输出: Terraform has been successfully initialized!
terraform plan # 输出: Plan: 5 to add, 0 to change, 0 to destroy.
terraform apply # 输入 yes 执行,输出: Apply complete! ... web_ip = 54.x.x.x
curl http://$(terraform output -raw web_ip) # 验证 nginx 欢迎页
# 增量变更:给安全组加 443 端口后再 apply
# 输出: Plan: 0 to add, 1 to change, 0 to destroy. ~ aws_security_group.web
# 销毁前必须先预览影响面
terraform plan -destroy # 输出: Plan: 0 to add, 0 to change, 5 to destroy.
terraform destroy # 输入 yes,资源按依赖逆序删除
常见错误
- State 文件被手动修改或丢失:永远不要手动编辑
terraform.tfstate。误操作后可用terraform state pull检查,或用terraform import重新导入资源。 - 多人同时 apply 未加锁:本地 backend 不锁,多人同时操作会覆盖 State。必须配置远程 backend(OSS/S3)天然支持锁。
- 硬编码敏感信息:把 AccessKey、密码写在 .tf 文件中。改用变量 +
sensitive = true或环境变量TF_VAR_xxx。 - 不理解 destroy 的风险:
terraform destroy会删除 State 中记录的所有资源。先用terraform plan -destroy预览再执行。 - 在 .tf 中使用 Tab 缩进:HCL 要求缩进统一。推荐编辑器安装 Terraform 插件自动格式化。
最佳实践
变量管理:variable / tfvars / 环境变量优先级
变量让同一份配置复用于 dev/staging/prod。取值优先级从高到低:
-var命令行参数(最高)*.auto.tfvars自动加载的变量文件terraform.tfvars默认变量文件- 环境变量
TF_VAR_<变量名> - variable 块内的
default值
# variables.tf
variable "env" {
type = string
description = "环境名"
validation {
condition = contains(["dev", "staging", "prod"], var.env)
error_message = "env 只能是 dev/staging/prod"
}
}
variable "instance_type" {
type = string
default = "t3.small"
}
variable "db_password" {
type = string
sensitive = true # apply 输出时打码,但注意 State 文件里仍是明文
}
# terraform.tfvars(明确不入库,敏感值走环境变量)
env = "prod"
instance_type = "t3.large"
# export TF_VAR_db_password='S3cr3t!' # 用环境变量传敏感值
# 命令行覆盖(临时覆盖默认值)
terraform apply -var="instance_type=t3.xlarge" -var-file="prod.tfvars"
# 变量引用方式
resource "aws_instance" "web" {
instance_type = var.instance_type
tags = { Env = var.env }
}
State 管理:远程存储与迁移
单机开发时 state 存在本地 terraform.tfstate;团队协作必须放到远程,用锁防止并发 apply 互相覆盖。AWS 用 S3 + DynamoDB 锁:
# backend.tf
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "prod/web/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-locks" # 锁表,防止并发冲突
}
}
# 已有本地 state 时,init 会询问是否迁移
terraform init # 输出: Do you want to copy existing state to the new backend? [yes/no]
# State 常用运维命令
terraform state list # 输出: aws_instance.web / aws_security_group.web ...
terraform state show aws_instance.web # 查看某个资源的完整属性
terraform state mv aws_instance.web module.web.aws_instance.web # 重构模块后迁移 state 路径
terraform state rm aws_security_group.web # 从 state 移除(云上资源保留,不再托管)
terraform import aws_instance.web i-0abc123 # 把已存在的实例纳入托管(需先写同名 resource 块)
# 事故恢复:远程 state 损坏/丢失时
terraform state pull > tfstate.backup.json # 备份当前 state
terraform state push tfstate.backup.json # 恢复(危险操作,push 会覆盖远端,务必先备份)
团队规范:state 文件禁止提交进 git(.gitignore 加 *.tfstate*);按环境分目录(dev/ prod/ 各自独立的 state key),或使用 terraform workspace 实现同名 state 隔离。
模块化:目录结构与发布
# 模块标准结构(modules/web/)
modules/web/
├── main.tf # 资源定义
├── variables.tf # 输入参数(必须显式声明)
├── outputs.tf # 对外暴露的结果(IP、ID)
└── README.md # 用法说明
# 模块文件内不要写 provider 配置,由根模块统一注入
# 根模块引用本地模块
module "web_prod" {
source = "./modules/web"
env = "prod"
instance_type = "t3.large"
}
# 发布到 Git 后用 git URL 引用,任何同事都能复用
module "web_prod" {
source = "git::https://github.com/my-org/terraform-modules//modules/web?ref=v1.2.0"
env = "prod"
}
# 模块输出值的使用
output "web_public_ip" {
value = module.web_prod.public_ip
}
模块设计经验:①变量全部显式声明,禁止模块内写死;②敏感输出标记 sensitive;③版本用 Git tag(ref=v1.2.0)而非 main 分支,保证可复现;④一个模块只做一件事(vpc 模块、ecs 模块、rds 模块分开);⑤公共模块可发布到 Terraform Registry,公开或私有均可。
- 远程 backend + 锁:团队协作必配 OSS/S3 backend,锁定 State 防止并发冲突。
terraform {
backend "oss" {
bucket = "my-terraform-state"
key = "prod/network/terraform.tfstate"
region = "cn-hangzhou"
encrypt = true
}
}
- 模块化组织:按环境(dev/staging/prod)和组件(vpc/ecs/rds)拆分目录结构:
terraform/
├── modules/
│ ├── vpc/
│ ├── ecs/
│ └── rds/
├── dev/
│ ├── main.tf
│ └── terraform.tfvars
├── staging/
└── prod/
- 锁定 Provider 版本:用
required_providers锁定大版本,避免无意升级破坏兼容性。
terraform {
required_providers {
alicloud = {
source = "aliyun/alicloud"
version = "~> 1.200.0"
}
}
}
- 变量与输出:所有可配参数用
variable,不要硬编码;敏感信息标记sensitive = true。 - Terraform + Ansible 分工:Terraform 建资源 → 输出 IP → Ansible dynamic inventory 接管配置,两者配合而非替代。
练习题
- 写一个
main.tf,用阿里云 Provider 创建一个 VPC(CIDR172.16.0.0/16)和一个交换机(CIDR172.16.1.0/24),并输出 VPC ID。 - 给上面的配置加上 remote backend(OSS),在本地验证
terraform init是否能正常初始化。 - 写一个 Data Source 查询当前账号已有的安全组,输出安全组 ID。
- 把 VPC 定义提取到
modules/vpc/中,然后在根模块引用它,传递不同的 CIDR 给 dev 和 staging 环境。 - 解释以下场景你选择 Terraform 还是 Ansible:①创建 RDS 实例;②在 ECS 上安装 Nginx;③管理 DNS 记录;④配置 PostgreSQL 参数。
点击查看答案
- 参考 chapter 内的 VPC 示例。output 块:
output "vpc_id" { value = alicloud_vpc.main.id }。terraform apply后terraform output vpc_id显示 ID。 - backend 配置:
terraform { backend "oss" { bucket="tf-state" key="prod/network" region="cn-hangzhou" } }。terraform init会提示是否迁移 state。 data "alicloud_security_groups" "all" {},output 显示data.alicloud_security_groups.all.groups[*].id。- 模块结构:
modules/vpc/main.tf中定义 variable "cidr",根模块module "dev" { source = "./modules/vpc" cidr = "172.16.0.0/16" }。 - ① 选 Terraform(创建云资源);② 选 Ansible(配置已有 ECS 软件);③ 选 Terraform(DNS 记录是 IaC 资源);④ 选 Ansible(数据库参数是配置管理)。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释 Terraform 的声明式语法和状态管理 | 尝试向他人讲解 |
| 命令操作 | 能不查文档完成 Terraform 初始化、规划、应用 | 在终端实际执行 |
| 原理掌握 | 能说出 Terraform 的资源依赖图和状态文件作用 | 画出流程图 |
| 故障排查 | 能独立排查 Terraform 执行错误、状态冲突、Provider 问题 | 模拟故障并修复 |
| 最佳实践 | 能说明为什么需要为 Terraform 配置远程状态存储和版本控制 | 对比不同方案 |
本章总结
Terraform 是现代云基础设施管理的标准工具——声明式、有状态、模块化。
速查表
| 命令 | 用途 |
|---|---|
terraform init | 初始化工作目录,安装 Provider 插件 |
terraform plan | 预览变更(只读) |
terraform apply | 执行变更并写入 State |
terraform destroy | 销毁所有托管的资源 |
terraform state list | 列出 State 中管理的所有资源 |
terraform import | 将已有资源纳入 Terraform 管理 |
入门三步:
- 写
.tf文件定义 Provider、Resource、Data Source terraform init && terraform plan初始化和预览terraform apply执行并写入 State
团队协作务必配置远程 backend + 锁,避免多人同时修改 State。牢记 Terraform 管"建"、Ansible 管"配"的分工原则。
延伸阅读
- Terraform 官方文档 —— 完整的 Provider、CLI、语言文档
- Terraform Registry —— 官方和社区的 Provider 与 Module 市场
- tfenv —— Terraform 版本管理器,团队项目锁定统一版本
- Terraform Best Practices —— 社区最佳实践指南
- 本书章节:4.1:Ansible 自动化 Ansible 自动化配置管理入门(配置管理对比)· 6.7:GitOps 入门 GitOps 入门(Git 驱动基础设施)· 6.10:云网络基础 云网络基础(VPC/CIDR 概念)