前言
我第一次接触Terraform是在三年前,当时公司要把基础设施从手动管理切换到代码化管理。之前的运维同事每次部署都要手动在控制台点来点去,环境一多就容易出错,而且出了问题很难回滚。那次迁移之后,我算是真正体会到了什么叫“基础设施即代码”。
Terraform的好处,用过的人都知道:版本控制、回滚方便、环境一致、协作透明。但真正上手时会发现,想要用好Terraform并不是简单写几个配置文件就行的。状态管理、模块设计、多环境配置、团队协作……这些才是真正的挑战。
这篇文章,我想从自己的踩坑经验出发,讲讲Terraform的使用心得。文章内容会比较偏实战,不会有太多官方文档里都能查到的概念解释,更多是告诉你“为什么要这样做”以及“我在实际工作中是怎么做的”。

一、为什么选择Terraform
在说Terraform之前,先聊聊基础设施即代码这个概念。传统的运维模式是登录控制台手动操作,好处是直观,坏处是:
- 环境不一致:开发、测试、生产环境配置可能完全不同
- 不可重复:手动操作难以保证每次部署结果相同
- 难以回滚:出问题只能手动恢复,容易出错
- 知识丢失:只有操作者知道怎么配置的,换人就抓瞎
IaC(Infrastructure as Code)就是来解决这些问题的。把基础设施的定义写成代码,版本控制系统管理起来,每次部署从代码创建,环境完全一致,出问题也能通过回滚代码来解决。
Terraform是HashiCorp公司开发的IaC工具,跟其他方案相比,它有几个显著优势:
声明式配置。你描述的是最终状态,而不是操作步骤。Terraform会自动计算如何从当前状态到达目标状态。比如你声明要有3台服务器,Terraform会自动创建,不会让你手动去数。
多云支持。AWS、GCP、Azure、阿里云、腾讯云……主流云厂商基本都支持。一套配置管理所有云资源,不用学多套工具。
Plan预览。执行前会先显示计划,让你确认要做什么改动,避免误操作。
状态管理。Terraform会跟踪资源状态,知道哪些需要创建、修改还是删除。
当然,Terraform也有缺点:学习曲线相对陡峭,状态文件管理需要额外注意,对于一些云厂商的高级功能支持可能不够及时。但总体来说,Terraform是当前最成熟的IaC解决方案,值得投入时间学习。
二、核心概念快速入门
2.1 HCL语法基础
Terraform使用HCL(HashiCorp Configuration Language)作为配置文件格式。HCL设计得很直观,对开发者友好。
变量定义:
hcl
variable "region" {
description = "AWS区域"
type = string
default = "us-east-1"
}
variable "instance_type" {
description = "EC2实例类型"
type = string
default = "t3.micro"
}
资源定义:
hcl
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
tags = {
Name = "web-server"
Environment = "production"
ManagedBy = "terraform"
}
}
输出值:
hcl
output "instance_public_ip" {
description = "实例公网IP地址"
value = aws_instance.web_server.public_ip
}
HCL的基本语法很简洁:类型关键字 + 名称 + 大括号内的配置。学过JSON或者YAML的话,应该很快能上手。
2.2 Provider是什么
Provider是Terraform与各云服务商之间的桥梁。每个Provider负责管理对应的资源类型。比如AWS Provider知道怎么创建EC2、RDS、S3等AWS资源,Azure Provider知道怎么创建Azure虚拟机、存储账户等。
使用Provider很简单,在配置文件中声明即可:
hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.region
# 可以配置多个账号、角色等
alias = "prod"
}
terraform init时,Terraform会自动下载对应的Provider插件。第一次初始化会慢一些,之后会缓存到本地。
2.3 Terraform工作流
Terraform的标准工作流分为四步:
第一步,编写配置。创建.tf文件,定义基础设施。
第二步,terraform init。初始化工作目录,下载Provider插件。
第三步,terraform plan。生成执行计划,Preview要做的改动。
第四步,terraform apply。执行计划,创建/修改/删除资源。
plaintext
# 基本工作流示例
$ terraform init
$ terraform plan -out=tfplan
$ terraform apply tfplan
plan阶段的输出非常重要,建议仔细阅读,确认改动的资源和数量是否符合预期。特别是生产环境,养成先plan再apply的习惯。
三、实战:从创建一台EC2说起
3.1 最小化配置
先用最少的配置创建一台EC2实例:
hcl
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "terraform-example"
}
}
output "instance_id" {
value = aws_instance.example.id
}
保存为main.tf,然后执行:
bash
terraform init
terraform plan
terraform apply
输入yes确认后,Terraform会创建EC2实例。完成后,输出会显示实例ID。
3.2 添加更多配置
基础的EC2用处不大,通常还需要:
- 安全组:控制端口访问
- 弹性IP:固定公网IP
- 用户数据:初始化脚本
hcl
# 安全组
resource "aws_security_group" "web" {
name = "web-sg"
description = "Web服务器安全组"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
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"]
}
tags = {
Name = "web-sg"
}
}
# EC2实例
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
yum install -y httpd
systemctl start httpd
systemctl enable httpd
EOF
tags = {
Name = "web-server"
}
}
# 弹性IP
resource "aws_eip" "web" {
instance = aws_instance.web.id
}
output "public_ip" {
value = aws_eip.web.public_ip
}
这个配置创建了一台带安全组和弹性IP的Web服务器。安全组开放了80和443端口,用户数据脚本自动安装了Apache并启动服务。
3.3 常用命令一览
Terraform的命令行工具很丰富,以下是日常使用最频繁的几个:
| 命令 | 用途 |
|---|---|
| terraform init | 初始化工作目录 |
| terraform plan | 生成执行计划 |
| terraform apply | 执行计划 |
| terraform destroy | 删除所有资源 |
| terraform show | 查看当前状态 |
| terraform output | 查看输出值 |
| terraform refresh | 同步远程状态 |
| terraform validate | 验证配置语法 |
| terraform fmt | 格式化配置文件 |
bash
# 常用命令组合
terraform init && terraform plan # 初始化并查看计划
terraform apply -auto-approve # 自动确认执行
terraform destroy -target=aws_instance.web # 只删除特定资源
terraform output public_ip # 查看输出值
destroy命令很危险,生产环境执行前一定要确认。可以用-target参数限定删除范围,或者提前备份状态文件。
四、状态管理:Terraform的精髓
4.1 为什么要管理状态
Terraform用状态文件(terraform.tfstate)来跟踪资源信息。每次执行plan或apply,Terraform会用当前配置与状态文件对比,计算需要做什么改动。
状态文件不仅记录资源ID,还缓存了资源的属性信息。比如EC2实例的私有IP、公有IP等信息都会保存在状态里,不需要每次都查询云API。
本地状态只适合个人学习或单机环境。团队协作和生产环境必须用远程状态:
- S3 + DynamoDB(AWS原生方案)
- Azure Blob Storage
- Google Cloud Storage
- Terraform Cloud
- MinIO(私有化部署)
4.2 配置远程状态
以AWS S3为例,配置远程状态:
hcl
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
这里同时配置了DynamoDB表来管理锁,防止多人同时操作导致状态冲突。
创建S3 Bucket和DynamoDB表的配置:
hcl
# S3 Bucket
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state"
lifecycle {
prevent_destroy = true
}
}
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"
}
}
}
# DynamoDB表(用于状态锁)
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
配置好远程状态后,每次terraform apply都会自动锁定状态,其他成员无法同时操作,解决了团队协作的问题。
4.3 状态隔离策略
大型项目通常需要多个环境(开发、测试、生产),每个环境应该有独立的状态文件。推荐的结构:
plaintext
.
├── envs/
│ ├── dev/
│ │ ├── main.tf
│ │ └── variables.tf
│ ├── staging/
│ │ ├── main.tf
│ │ └── variables.tf
│ └── prod/
│ ├── main.tf
│ └── variables.tf
└── modules/
├── vpc/
├── ec2/
└── rds/
每个环境的配置放在独立目录,通过不同的backend key区分状态文件。modules目录存放可复用的模块,供各环境引用。
五、模块化设计:复用与封装
5.1 为什么要用模块
Terraform的模块(Module)是对一组资源的封装。不用模块的话,每次创建类似的资源都要重复写配置,修改也要改多处。用了模块,一个地方改,所有引用都生效。
举一个实际的例子:Web服务通常需要EC2 + 安全组 + 弹性IP的组合。不用模块时:
hcl
# 环境A
resource "aws_instance" "web_a" { ... }
resource "aws_security_group" "web_a" { ... }
resource "aws_eip" "web_a" { ... }
# 环境B
resource "aws_instance" "web_b" { ... }
resource "aws_security_group" "web_b" { ... }
resource "aws_eip" "web_b" { ... }
用模块后:
hcl
# 定义模块
# modules/web-server/main.tf
variable "name" {}
variable "instance_type" {}
resource "aws_instance" "this" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
# ...
}
# 引用模块
module "web_a" {
source = "../../modules/web-server"
name = "web-a"
instance_type = "t3.micro"
}
module "web_b" {
source = "../../modules/web-server"
name = "web-b"
instance_type = "t3.small"
}
模块让代码更简洁,配置更一致,修改更方便。
5.2 模块设计原则
好的模块设计应该:
单一职责。每个模块只做一件事。比如VPC模块只创建网络,RDS模块只创建数据库。组合使用时再拼装成完整的基础设施。
合理暴露参数。模块内部应该隐藏细节,只暴露必要的配置项。暴露太少模块不灵活,暴露太多使用复杂。通常只暴露业务相关的参数,如实例数量、实例规格等,技术细节由模块内部处理。
hcl
# modules/web-cluster/main.tf
variable "cluster_name" {
description = "集群名称"
type = string
}
variable "instance_count" {
description = "实例数量"
type = number
default = 2
}
variable "instance_type" {
description = "实例规格"
type = string
default = "t3.micro"
}
# 内部资源,对外隐藏
resource "aws_launch_template" "this" { ... }
resource "aws_autoscaling_group" "this" { ... }
resource "aws_security_group" "this" { ... }
output "asg_name" {
value = aws_autoscaling_group.this.name
}
版本管理。Terraform支持模块版本控制。使用远程模块时指定版本:
hcl
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
}
Terraform Registry上有大量官方和社区维护的模块,如terraform-aws-modules/vpc、terraform-aws-modules/ecs等,开箱即用,可以参考学习。
六、变量与条件逻辑
6.1 变量使用技巧
Terraform的变量系统很灵活,支持多种类型和默认值:
字符串变量:
hcl
variable "environment" {
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
对象变量:
hcl
variable "web_server" {
type = object({
instance_type = string
ami_id = string
volume_size = number
})
default = {
instance_type = "t3.micro"
ami_id = "ami-0c55b159cbfafe1f0"
volume_size = 20
}
}
# 使用
resource "aws_instance" "web" {
instance_type = var.web_server.instance_type
ami = var.web_server.ami_id
root_block_device {
volume_size = var.web_server.volume_size
}
}
Map和列表变量:
hcl
variable "environment_config" {
type = map(object({
instance_type = string
desired_capacity = number
}))
default = {
dev = {
instance_type = "t3.micro"
desired_capacity = 1
}
prod = {
instance_type = "t3.large"
desired_capacity = 3
}
}
}
# 使用
locals {
config = var.environment_config[terraform.workspace]
}
6.2 条件表达式
Terraform支持条件表达式,可以根据变量值决定创建什么资源:
hcl
# 根据环境决定是否创建公有IP
resource "aws_eip" "web" {
count = var.enable_public_ip ? 1 : 0
instance = aws_instance.web.id
}
# 根据条件选择不同的配置
locals {
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
}
6.3 循环与动态块
Terraform支持count和for_each两种循环方式:
count方式:
hcl
resource "aws_security_group_rule" "ingress" {
count = length(var.allowed_ports)
type = "ingress"
from_port = var.allowed_ports[count.index]
to_port = var.allowed_ports[count.index]
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.main.id
}
for_each方式(推荐):
hcl
resource "aws_security_group_rule" "ingress" {
for_each = toset(var.allowed_ports)
type = "ingress"
from_port = each.value
to_port = each.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.main.id
}
for_each比count更清晰,删除某个元素时不会导致其他资源重建。Terraform 0.12之后的版本应该优先使用for_each。
动态块:当需要根据列表生成嵌套配置时使用:
hcl
resource "aws_lambda_function" "this" {
# ... 基础配置
dynamic "environment" {
for_each = var.environment_variables
content {
variables = environment.value
}
}
}
七、工作区与多环境管理
7.1 Terraform工作区
Terraform内置了工作区(Workspace)功能,可以在一份配置下管理多个环境的状态:
bash
terraform workspace list # 列出所有工作区
terraform workspace new dev # 创建新工作区
terraform workspace select dev # 切换工作区
terraform workspace show # 显示当前工作区
配合不同的backend key,工作区可以实现状态隔离:
hcl
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "${terraform.workspace}/terraform.tfstate"
region = "us-east-1"
}
}
dev环境的状态会保存在dev/terraform.tfstate,prod环境的状态保存在prod/terraform.tfstate。
7.2 工作区的注意事项
工作区适合环境间配置差异较小的情况。如果环境间差异很大,比如生产环境用不同规格的实例、更严格的安全策略等,硬要用工作区会导致配置里充满条件判断,代码变得难维护。
对于差异大的环境,推荐使用目录隔离(前面提到的envs结构)。每个环境独立配置,共享的逻辑抽成模块复用。两种方式各有适用场景,根据实际情况选择。
八、最佳实践总结
8.1 版本控制
- 所有.tf文件都加入Git管理
- .gitignore排除.tfstate文件和敏感信息
- 每次重大变更单独提交,方便回滚
- 使用语义化的提交信息
8.2 敏感信息处理
不要把密钥、密码等敏感信息写在配置文件里。Terraform支持从环境变量或密钥管理服务读取:
hcl
# 从环境变量读取
provider "aws" {
secret_key = var.aws_secret_key
token = var.aws_session_token
}
# 从AWS Secrets Manager读取
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db-password"
}
resource "aws_db_instance" "main" {
# ...
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
8.3 团队协作规范
- 代码Review:所有变更经过Review再合并
- 环境分离:开发和测试环境可以随意操作,生产环境必须谨慎
- 状态锁:确保backend配置了状态锁
- 文档:复杂逻辑添加注释,重要决策记录在README
结语
Terraform这几年用下来,最大的感受是:它真的把运维从“体力活”变成了“技术活”。以前部署一个环境要一整天,现在可能只需要几十分钟;以前出了问题手忙脚乱,现在一个terraform destroy就能回到干净的状态。
当然,Terraform不是银弹,它有学习成本,有状态管理的复杂度,有云厂商API的限制。但相比带来的收益,这些问题都是可以接受的。
如果你是第一次接触Terraform,建议从简单的单资源开始练习,熟悉了基本操作后再尝试多资源组合、多环境管理。遇到问题多查官方文档,Terraform的文档质量很高,基本能覆盖所有常见场景。
基础设施即代码是大势所趋,早点掌握早点受益。希望这篇教程能帮你少走一些弯路。
相关资源推荐:

发表回复