为什么不能用本地 State?
本地 State 的根本问题
当你独自用 Terraform 管理个人项目时,terraform.tfstate 存在本地完全没问题。但一旦进入团队协作场景,本地 State 会带来三个致命问题:
State 文件包含了所有资源的 ID、属性和依赖关系。一旦丢失且没有备份,你将失去对所有云资源的 Terraform 管理能力。远程 Backend 不是可选的——在生产环境中是必须的。
远程 Backend:S3 + DynamoDB(AWS 最佳实践)
架构原理
AWS 最常用的远程 Backend 方案是:用 S3 存储 state 文件(持久化),用 DynamoDB 提供状态锁(防并发)。这是 Terraform 官方推荐的 AWS 方案。
初始化 Backend 基础设施
在使用 S3 Backend 之前,需要先创建 S3 Bucket 和 DynamoDB 表。这通常用另一个小型 Terraform 项目(或手动)来创建:
# bootstrap/main.tf — 创建 Backend 所需的基础设施
# 注意:这个 bootstrap 项目本身使用本地 state
provider "aws" {
region = "us-east-1"
}
# S3 Bucket:存储 state 文件
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-company-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"
}
}
# 服务端加密:保护 state 中的敏感数据
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 = "aws:kms" # 使用 KMS 加密(比 AES256 更安全)
}
}
}
# 封锁公共访问:state 文件绝对不能公开
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# DynamoDB 表:提供状态锁
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST" # 按需计费,成本极低
hash_key = "LockID" # 必须是这个名称
attribute {
name = "LockID"
type = "S"
}
lifecycle {
prevent_destroy = true
}
}
在业务项目中配置 Backend
# backend.tf(不能使用变量!Backend 配置是静态的)
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "environments/production/terraform.tfstate"
region = "us-east-1"
encrypt = true # 启用加密(配合 S3 SSE)
dynamodb_table = "terraform-locks" # 状态锁表名
kms_key_id = "alias/terraform" # 可选:指定 KMS key
}
}
# 不同环境使用不同的 key(state 路径隔离):
# dev: "environments/dev/terraform.tfstate"
# staging: "environments/staging/terraform.tfstate"
# production: "environments/production/terraform.tfstate"
Terraform 的 backend 块是最早初始化的,此时变量还没有被解析,所以不能在 backend 块中使用 var.xxx、local.xxx 或任何表达式。如果需要动态 backend 配置,使用 terraform init -backend-config="key=value" 命令行参数或 -backend-config=file.hcl 文件。
部分 Backend 配置(partial configuration)
# backend.tf(只写静态部分)
terraform {
backend "s3" {
# 只写通用配置,动态部分通过命令行传入
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# 初始化时传入剩余配置
terraform init \
-backend-config="bucket=my-company-terraform-state" \
-backend-config="key=environments/${ENV}/terraform.tfstate"
# 或者使用配置文件
# prod.backend.hcl
# bucket = "my-company-terraform-state"
# key = "environments/prod/terraform.tfstate"
terraform init -backend-config=prod.backend.hcl
Terraform Cloud(HCP Terraform)
Terraform Cloud 的核心功能
配置 Terraform Cloud Backend
# terraform.tf — 使用 Terraform Cloud 作为 Backend
terraform {
cloud {
organization = "my-company" # TFC 组织名
workspaces {
name = "production-aws" # 指定单个 workspace
# 或使用 tag 匹配多个 workspace:
# tags = ["production", "aws"]
}
}
}
# 1. 登录 Terraform Cloud(获取 API Token 并存储到本地)
terraform login
# 2. 初始化(迁移 state 到 TFC)
terraform init
# 3. 执行 plan(在 TFC 远程环境中运行,输出流传回本地)
terraform plan
# 4. 查看 TFC UI:app.terraform.io
# 可以看到 plan 输出、cost estimation、apply 确认
GitHub Actions CI/CD 完整工作流
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
pull_request:
branches: [main]
paths: ['terraform/**']
push:
branches: [main]
paths: ['terraform/**']
env:
TF_WORKING_DIR: ./terraform/environments/prod
jobs:
terraform:
name: Terraform
runs-on: ubuntu-latest
permissions:
id-token: write # 使用 OIDC 获取临时凭证
contents: read
pull-requests: write
defaults:
run:
working-directory: ${{ env.TF_WORKING_DIR }}
steps:
- uses: actions/checkout@v4
# 使用 OIDC 认证(不需要长期 AWS 凭证)
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubTerraformRole
aws-region: us-east-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.9.0"
- name: Terraform Format Check
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
if: github.event_name == 'pull_request'
continue-on-error: true
# 在 PR 中自动添加 plan 输出评论
- name: Comment Plan on PR
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
\`\`\`\n
${{ steps.plan.outputs.stdout }}
\`\`\`
*Pushed by: @${{ github.actor }}*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
Atlantis:自托管 GitOps 方案
Atlantis 是 Terraform 的开源 GitOps 工具,适合不想使用 Terraform Cloud 的团队。它运行在你自己的基础设施上(如 ECS、Kubernetes),监听 GitHub/GitLab PR 的 Webhook,自动在服务端执行 plan/apply。
terraform plan,将输出作为 PR 评论 → 审批者评论 atlantis apply → Atlantis 执行 apply → PR 合并。整个流程无需开发者在本地有 AWS 凭证。# atlantis.yaml — Atlantis 配置文件
version: 3
automerge: false # apply 后不自动合并 PR(需人工合并)
delete_source_branch_on_merge: true
projects:
- name: prod-networking
dir: environments/prod/networking
workspace: default
autoplan:
when_modified: ["*.tf", "modules/**/*.tf"] # 哪些文件变化时自动 plan
enabled: true
terraform_version: v1.9.0
apply_requirements: # apply 需满足的条件
- mergeable # PR 无冲突
- approved # 至少一个 Review Approved
- name: prod-app
dir: environments/prod/app
workflow: custom_workflow # 使用自定义工作流
workflows:
custom_workflow:
plan:
steps:
- init
- run: conftest test --policy policy/ plan.json # 自定义策略检查
- plan
apply:
steps:
- apply
terraform_remote_state:跨项目读取输出
当多个 Terraform 项目(如 networking 和 application)需要共享数据时,terraform_remote_state 数据源允许一个项目读取另一个项目的 Output 值:
# application/main.tf — 读取 networking 项目的输出
# 读取另一个项目的 State(只能读取该项目的 output,不能访问内部资源)
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-company-terraform-state"
key = "environments/prod/networking/terraform.tfstate"
region = "us-east-1"
}
}
# 使用 networking 项目的输出
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
vpc_security_group_ids = [data.terraform_remote_state.networking.outputs.app_sg_id]
# ...
}
# 注意:terraform_remote_state 依赖于 networking 项目暴露的 output
# networking/outputs.tf 必须包含:
# output "private_subnet_ids" { value = aws_subnet.private[*].id }
# output "app_sg_id" { value = aws_security_group.app.id }
# 替代方案:使用 SSM Parameter Store 共享数据(更灵活,不依赖 State 格式)
data "aws_ssm_parameter" "vpc_id" {
name = "/networking/prod/vpc-id"
}
# networking 项目负责写入 SSM:
# resource "aws_ssm_parameter" "vpc_id" {
# name = "/networking/prod/vpc-id"
# type = "String"
# value = aws_vpc.main.id
# }
Workspace 管理策略
Workspace vs 目录结构
有两种常见的多环境管理策略,各有优缺点:
方案一:目录隔离(推荐)
environments/
├── dev/
│ └── main.tf # 调用公共模块
├── staging/
│ └── main.tf
└── prod/
└── main.tf # 各环境独立 State
优点:各环境完全隔离,不同环境可以使用不同模块版本,出错不互相影响。
方案二:Workspace 切换
# 同一套代码,切换 workspace
terraform workspace new dev
terraform workspace select prod
terraform plan
优点:代码复用。缺点:只有一套代码管理所有环境,变更一次就影响所有环境,风险高。
HashiCorp 官方建议:对于不同环境(dev/staging/prod),使用目录隔离而非 workspace。Workspace 更适合用于同一环境的临时变更分支(如测试某个功能变更后销毁)。
IAM 权限最小化原则
Terraform 执行 plan/apply 需要访问 AWS API。为 CI/CD 系统配置最小必要权限是安全的关键:
// terraform-ci-policy.json — 最小权限策略示例
{
"Version": "2012-10-17",
"Statement": [
{
// S3 Backend:读写 State 文件
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:GetBucketVersioning"
],
"Resource": [
"arn:aws:s3:::my-terraform-state",
"arn:aws:s3:::my-terraform-state/environments/prod/*"
]
},
{
// DynamoDB:State 锁操作
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/terraform-locks"
}
// 还需要添加业务资源的权限(EC2、VPC、RDS 等)
]
}
# 为 GitHub Actions 配置 OIDC 身份提供商(只需配置一次)
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
# IAM 角色:允许特定 GitHub 仓库的 GitHub Actions 工作流扮演此角色
resource "aws_iam_role" "github_terraform" {
name = "GitHubTerraformRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = {
# 限制:只允许特定仓库的特定分支
"token.actions.githubusercontent.com:sub" = [
"repo:my-org/infra:ref:refs/heads/main",
"repo:my-org/infra:pull_request"
]
}
}
}]
})
}
本章小结
- 远程 Backend 是团队协作的必要条件:解决状态同步、并发冲突和状态丢失三大问题
- S3 + DynamoDB 是 AWS 环境的最佳实践:S3 存储和版本控制 state,DynamoDB 提供状态锁;S3 Bucket 必须开启加密、禁止公共访问、启用版本控制
- Backend 块不支持变量:可以用部分配置(partial configuration)配合 -backend-config 参数解决动态需求
- Terraform Cloud 提供托管的 Backend + 远程执行:小团队免费层够用,大团队可使用 Sentinel 策略和 VCS 集成
- GitHub Actions CI/CD:使用 OIDC 认证(无长期凭证);PR 触发 plan 并评论输出;合并到 main 分支时自动 apply
- 多环境用目录隔离而非 workspace:各环境独立 state 文件,安全隔离,推荐做法