

















最近,我参与了一个 AWS Serverless 项目。业务需求本身并不复杂:几个 Lambda 函数、一个 S3 存储、一个 EventBridge 定时触发,再加上精细化的 IAM 权限控制。在部署这套服务的过程中,我先后尝试了三种方案,几乎踩遍了云基础设施管理中的经典巨坑:
AWS Console:一开始为了快,直接上控制台点鼠标。创建 S3 Bucket、手动上传 Lambda zip 包、配置 EventBridge 规则。问题是:项目要部署到 dev、staging、prod 三个环境,在控制台里点一遍要花半小时,三个环境配置完一个半小时。两个月后,当有人问“这个安全组是谁配的,为什么开了 443端口”时,没人说得清。
SAM:手动上传 zip 太繁琐,同事推荐了 SAM,通过一个 template.yaml 定义 Lambda 和权限,结合 sam build && sam deploy 确实省心。但当面对 VPC、安全组、S3 高级配置、更精细的 IAM 策略时,SAM 显得力不从心。于是我变成了“混合打法”:SAM 管理 Lambda,其余资源还是在控制台管理。两个工具,两套流程,让人身心俱疲。
Terraform:运维同事终于看不下去了:“你这些资源应该用 Terraform 管理起来”。我一开始是抗拒的——一个 S3 Bucket 要写好几行代码,还要声明 Provider、定义变量,控制台里点几下搞定的事,为什么要大费周章写代码?
但是,很快一次意外说服了我。某天,有人在控制台里偷偷修改了路由表,却没有同步到代码中。第二天跑 terraform plan,配置漂移直接暴露。那一刻我才真正顿悟:Terraform 不仅仅是帮你创建资源的工具,更是帮你锁定“期望状态”的终极防线。
故事到这里,你以为我全面拥抱 Terraform 了?并没有。对于 Lambda 函数的代码部署,我最后还是选择用 GitHub Actions + AWS CLI。因为:Terraform 擅长管理静态的基础设施,而高频的代码迭代更适合放在专业的 CI/CD 流水线中。所以,我最终的工程实践是:Terraform 掌管资源拓扑,CI/CD 流水线负责代码交付。架构没有银弹,关键在于各司其职。
先说结论:用代码描述你期望的基础设施最终状态,然后让工具帮你自动实现,这就是基础设施即代码(IaC, Infrastructure as Code)的核心思想。我们可以对比一下两种思维模式:
# 命令式:按顺序下达指令
aws s3api create-bucket --bucket my-bucket --region us-west-2
aws s3api put-bucket-versioning --bucket my-bucket --versioning-configuration Status=Enabled
# 声明式:只描述期望的最终状态
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-bucket"
}
resource "aws_s3_bucket_versioning" "my_bucket" {
bucket = aws_s3_bucket.my_bucket.id
versioning_configuration {
status = "Enabled"
}
}
当你执行 terraform apply 命令时,Terraform 会自动查漏补缺,智能处理以下三种场景:
从“命令式指令”向“声明式状态”的思维转变,是开发者接触 Terraform 时需要跨越的第一道门槛。
第一次看 Terraform 代码,你可能会被一堆配置搞得眼花缭乱。其实,学习 Terraform 不需要死记硬背,牢牢把握住三个核心概念即可:Resource(资源)、Provider(服务商) 和 State(状态文件)。
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-unique-bucket-name"
}
其中:
核心避坑点 —— 代码中的逻辑名称 my_bucket 与云端真实的资源名称 my-unique-bucket-name 是两码事:
provider "aws" {
region = "us-west-2"
}
Provider 扮演着“翻译官”的角色。当你在代码中声明“我需要一个 S3 存储桶”时,Provider 负责将你的意图翻译成对应云厂商的底层的 API 调用:
aws_s3_bucket 资源只能由 AWS Provider 来解析和驱动azurerm_storage_container 资源则只能由 Azure Provider 来解析和驱动当你运行 terraform apply 命令后,它会在本地或远程生成一个名为 terraform.tfstate 的状态文件。这个文件记录了:
状态文件是 Terraform 的心脏。唯有依靠它,Terraform 才能精准计算出“云端实际状态”与“代码期望状态”之间的偏差。这与 Git 对比两次提交差异的底层逻辑相通:Git 比较的是代码的历史版本,而 Terraform 比较的则是本地代码与云端环境。
举例说明:假设代码中声明了 bucket = "my-bucket"。下次运行 terraform plan 命令时,Terraform 读取状态文件后得知"此前已管理过一个存储桶,云端 ID 为 my-bucket”,随即向 AWS 发起实时校验。如配置完全一致,终端便会输出:No changes。一旦状态文件丢失,Terraform 将会“失忆”,误以为这是一份全新的配置,进而尝试在云端重新创建资源,最终导致名称冲突甚至意外覆盖。。
让我们从最简单的配置开始,创建 main.tf文件:
provider "aws" {
region = "us-west-2"
}
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-tutorial-bucket-2026"
}
接下来,便是标准的 Terraform 三板斧 命令:
# 1. 初始化:下载所需的 provider 插件
terraform init
# 2. 预览:对比差异,预览将要发生的变更(不会真正动云端资源)
terraform plan
# 3. 部署:将变更真正应用到云端
terraform apply
在执行 plan 时,终端会输出清晰的差异对比图:
Terraform will perform the following actions:
# aws_s3_bucket.my_bucket will be created
+ resource "aws_s3_bucket" "my_bucket" {
+ bucket = "my-tutorial-bucket-2026"
+ id = (known after apply)
+ arn = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
此时,必须小心谨慎地对待以下变更差异符号:
+ 创建:表示创建了新资源- 删除:表示删除了某个资源~ 修改:表示在原资源上更新特定属性-/+ 破坏性替换:由于修改了不可变参数,,Terraform 必须先删后建。在生产环境中可能到这个符号务必小心,这意味着可能会删除原有数据,属于危险操作执行完成后,你会看到目录中多出了 terraform.tfstate 文件,里面已经记录了当前存储桶的信息。
在上面的例子中,我们把 region 和 bucket 名称直接写死了,这在软件工程中属于硬编码
provider "aws" {
region = "us-west-2" # hardcode
}
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-tutorial-bucket-2026" # hardcode
}
考虑到代码的可维护性,我们引入一个新的文件 variables.tf,将可变参数抽离出来:
# variables.tf
variable "region" {
type = string
default = "us-west-2"
}
variable "bucket_name" {
type = string
}
然后在 main.tf 中通过 var.xxx 的形式动态引用变量:
# main.tf
provider "aws" {
region = var.region
}
resource "aws_s3_bucket" "my_bucket" {
bucket = var.bucket_name
}
在实际执行时,有三种常见的方式注入这些变量的值:
# 1. 命令行
terraform apply -var="bucket_name=my-prod-bucket"
# 2. 变量文件(推荐)
terraform apply -var-file="prod.tfvars"
# 3. 环境变量
export TF_VAR_bucket_name="my-prod-bucket"
terraform apply
其中 prod.tfvars 通常是类似下面这样的键值对形式:
bucket_name = "my-prod-bucket"
region = "us-west-2"
在需要多环境管理的生产级项目中,更推荐使用 workspaces 目录 + tfvars.json 实现多环境配置管理:
workspaces/
├── dev.tfvars.json
├── staging.tfvars.json
└── prod.tfvars.json
在 AWS 体系中,资源的 ARN(Amazon Resource Name,唯一资源标识) 是非常核心的概念。例如在编写 IAM 权限策略时,频繁需要引用其他资源的 ARN。当 Terraform 创建完资源后,我们如何将资源的 ID、ARN 等属性安全地暴露给下游服务或团队呢?这就需要用到 outputs.tf:
output "bucket_arn" {
description = "The ARN of the bucket"
value = aws_s3_bucket.my_bucket.arn
}
运行 terraform apply 命令后,终端将会打印出类似下面的信息:
Outputs:
bucket_arn = "arn:aws:s3:::my-tutorial-bucket-2026"
Output 的两大核心场景:
terraform_remote_state 远程读取,实现模块解耦variables 负责处理来自外部的输入参数,它可以通过 tfvars 或者 tfvars.json 文件传入,而内部复杂的逻辑计算和复用,则应该交给 locals。例如,我们希望规范资源命名,自动为所有资源带上环境后缀,并统一注入 Tag 标签:
# local.tf
locals {
bucket_name = "my-app-${var.env}"
tags = {
Environment = var.env
ManagedBy = "terraform"
}
}
# main.tf
resource "aws_s3_bucket" "my_bucket" {
bucket = local.bucket_name
tags = local.tags
}
什么时候该用 locals?
我们来看一个稍微复杂的场景:为刚刚创建的 S3 存储桶配置一条 Bucket Policy。首先,我们在独立的文件 policies/s3-policy.json 中定义好标准的 JSON 策略模板,并预留占位符:
policies/s3-policy.json:{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "${bucket_arn}/*"
}
]
}
main.tf 中用 templatefile 函数渲染该 JSON,并建立资源关联:resource "aws_s3_bucket" "my_bucket" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_policy" "my_policy" {
bucket = aws_s3_bucket.my_bucket.id
policy = templatefile("${path.module}/policies/s3-policy.json", {
bucket_arn = aws_s3_bucket.my_bucket.arn
})
}
注意看这里的 aws_s3_bucket.my_bucket.arn:在代码里,我们并没有手动去指定谁先创建、谁后创建。但 Terraform 在解析代码时,会通过这一行引用自动建立一张有向无环图(DAG)。它知道 Policy 依赖 Bucket 的 ARN,因此会极其智能地先创建存储桶,拿到 ARN 后再创建策略。这就是声明式编程带来的巨大红利。
随着项目规模的扩大,把所有基础设施全部写在一个 main.tf 里会变成一场灾难。优秀的实践是将资源按职责和生命周期拆分成独立的子目录。每个目录拥有自己独立的状态文件,这样可以极大程度地控制爆炸半径(Blast Radius)——哪怕你在改 Lambda 代码时配置写错了,绝对不会波及核心的网络和 IAM 权限:
infra/
├── iam/ # IAM 角色和策略
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── workspaces/
│ └── prod.tfvars.json
└── lambda/ # Lambda 函数
├── main.tf
├── variables.tf
└── workspaces/
└── prod.tfvars.json
在部署时,各自独立执行:
cd iam && terraform apply
cd lambda && terraform apply
此时面临一个新挑战:lambda 目录在配置函数时,需要引用 iam 目录刚刚创建出来的角色 ARN。它们不仅文件独立,连状态文件都不在一个地方,该怎么打通?答案是利用 terraform_remote_state 数据源,跨边界直接读取对方的部署产物:
data "terraform_remote_state" "iam" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "iam/terraform.tfstate"
region = "us-west-2"
}
}
resource "aws_lambda_function" "my_function" {
role = data.terraform_remote_state.iam.outputs.lambda_role_arn
# ...
}
架构痛点与思考:使用这种方式,你需要在代码里硬编码对方的远程 S3 路径。当子目录繁多时,这些路径依赖会交织成一张复杂的蜘蛛网,维护成本极高。这解释了为什么在团队规模扩大后,往往需要引入 Terraform Cloud、Atlantis 或 Terragrunt 等高级编排工具的原因——它们能够帮你统一托管 Backend 和拓扑依赖,告别手动配置。
万变不离其宗,掌握 Terraform 的核心其实就三件事:
至于变量、输出、局部变量、数据源、模块等概念,统统是围绕这三件事衍生出来的效率工具。例如,当一个资源由你来控制时,你应该使用 resource 关键字描述资源;当一个资源已经存在时,你应该使用 data 关键字查询资源。
什么时候用什么工具?
| 业务场景 | 推荐选型 | 选用理由 |
|---|---|---|
| 临时测试、一次性调研、做完就忘 | AWS 控制台 | 纯可视化,直观方便,无额外心智负担 |
| 临时运维小脚本、快速查询或单步销毁 | AWS CLI | 轻量快捷,不需要初始化环境和声明状态 |
| 纯粹的 Serverless / Lambda 应用开发 | AWS SAM / Serverless 框架 | 针对 FaaS 深度优化,代码打包与热编译体验极佳 |
| 多云环境、多层资源交织、团队协作、长期演进 | Terraform | 完善的状态管理、声明式依赖拓扑、变更计划预览 |
在实际的工业界项目中,混用(Hybrid)才是常态:我们用 Terraform 锁死 VPC、数据库、IAM 等底层重型基础设施;用 SAM 或专有 CI/CD 流水线去高频发布 Lambda 业务代码;用 AWS CLI 临时处理线上紧急巡检。
最后,请允许我向 Terraform 新手们提供一份生存指南:
-/+,说明有资源要被干掉重建,核对好该资源是否有未备份的持久化数据!如果你发现自己正在重复敲着一堆类似的 AWS CLI 命令,或者在控制台频繁为新客户克隆同一套配置,那么,请不要怀疑,你该用 Terraform 啦!
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。