Chapter 07

Terraform Cloud 与远程后端

从本地 state 到远程协作——团队如何安全地共享和管理基础设施状态

为什么不能用本地 State?

本地 State 的根本问题

当你独自用 Terraform 管理个人项目时,terraform.tfstate 存在本地完全没问题。但一旦进入团队协作场景,本地 State 会带来三个致命问题:

状态不同步(Stale State)
Alice 执行了 apply,更新了 state。Bob 还拿着本地旧 state 执行 plan,不知道 Alice 已经创建了哪些资源,可能重复创建或产生冲突。State 文件不能放进 Git 仓库(包含敏感数据),所以 Bob 获取不到最新状态。
并发冲突(Concurrent Apply)
Alice 和 Bob 同时执行 apply(哪怕是不同资源),可能导致 state 文件损坏或产生不可预期的结果。本地 state 没有锁机制,无法防止并发操作。
状态丢失(State Loss)
state 文件在开发者的笔记本上。电脑丢失、误删或系统崩溃后,Terraform 会「忘记」它管理的所有资源——这些资源仍在云上运行,但 Terraform 不再跟踪它们,只能手动 import。
State 文件是 Terraform 的命脉

State 文件包含了所有资源的 ID、属性和依赖关系。一旦丢失且没有备份,你将失去对所有云资源的 Terraform 管理能力。远程 Backend 不是可选的——在生产环境中是必须的。

远程 Backend:S3 + DynamoDB(AWS 最佳实践)

架构原理

AWS 最常用的远程 Backend 方案是:用 S3 存储 state 文件(持久化),用 DynamoDB 提供状态锁(防并发)。这是 Terraform 官方推荐的 AWS 方案。

Terraform 执行时的 Backend 交互 terraform apply ↓ 1. 从 S3 读取最新 state(获取当前基础设施状态) ↓ 2. 在 DynamoDB 创建锁记录(防止其他人同时 apply) ↓ 3. 执行 Plan → Apply(创建/修改/删除云资源) ↓ 4. 将新 state 写回 S3(更新状态) ↓ 5. 删除 DynamoDB 锁记录(释放锁)

初始化 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"
Backend 配置不支持变量插值

Terraform 的 backend 块是最早初始化的,此时变量还没有被解析,所以不能在 backend 块中使用 var.xxxlocal.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 的核心功能

远程 State + 锁
自动管理 state 文件,无需手动设置 S3 + DynamoDB。state 加密存储,支持历史版本查看和回滚。
远程执行(Remote Execution)
plan 和 apply 在 HashiCorp 托管的运行环境中执行,而非本地机器。团队成员看到统一的执行输出,可审计执行历史。
VCS 集成
连接 GitHub/GitLab/Bitbucket,PR 自动触发 plan,合并到主分支自动 apply,是完整 GitOps 工作流的基础。
Variable Sets
在多个 workspace 间共享环境变量(如 AWS 凭证 AWS_ACCESS_KEY_ID),避免在每个 workspace 重复配置,提高安全性。
Sentinel 策略框架
在 apply 前执行合规策略检查(如"所有资源必须有 cost-center 标签""不允许创建没有加密的 S3 Bucket")。仅 Team+ 付费版可用。

配置 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。

Atlantis 工作原理
开发者提交 PR → GitHub 发送 Webhook 到 Atlantis → Atlantis 在服务端执行 terraform plan,将输出作为 PR 评论 → 审批者评论 atlantis apply → Atlantis 执行 apply → PR 合并。整个流程无需开发者在本地有 AWS 凭证。
atlantis.yaml
Atlantis 的配置文件,放在 repo 根目录,定义哪些目录是 Terraform 工程、使用哪个版本、执行前/后的自定义步骤(如 conftest 策略检查)。
# 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

优点:代码复用。缺点:只有一套代码管理所有环境,变更一次就影响所有环境,风险高。

官方推荐使用目录隔离而非 Workspace

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"
          ]
        }
      }
    }]
  })
}

本章小结

本章核心要点