文章

Terraform 生产级实践

Terraform 生产级实践

概述

单机 terraform apply 能做的事,在团队协作 + CI/CD 流水线里会变得危险得多。State 锁冲突、多人 apply 覆盖、误 destroy 生产环境——这些事故不是 Terraform 的 bug,是 State 管理和协作流程的 bug。本文聚焦生产环境中真正棘手的问题。

一句话:Terraform 入门是学会 planapply,入门之后的全部精力都在管 State。

State 管理深潜

State 锁:DynamoDB / GCS 的锁机制

多人同时 terraform apply 会损坏 State 文件。Terraform 通过后端锁防止并发写:

# S3 + DynamoDB 锁(AWS 标配)
terraform {
  backend "s3" {
    bucket         = "my-tfstate"
    key            = "prod/vpc/terraform.tfstate"
    region         = "ap-southeast-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

锁的完整生命周期:

terraform plan/apply
  → 客户端向 DynamoDB 写 LockID=state-path(带 ConditionExpression 防覆盖)
  → 获取锁 → 执行操作
  → 释放锁(删除 DynamoDB item)
  
锁过期机制(防止客户端崩溃后死锁):
  - DynamoDB: TTL 自动删除过期锁
  - GCS: 锁文件自带超时,GCS 定时清理
  - HTTP(Terraform Cloud): 服务端管理

State 文件损坏的 4 种场景与恢复

场景 1:apply 中途网络断开

State 处于不完整状态:部分资源已创建但 State 未记录,或 State 记录了但创建失败。

# 症状
terraform plan
# Error: Resource 'xxx' exists but is not in state

# 恢复
terraform import aws_instance.broken i-1234567890abcdef
# 手动把该资源"认领"回 State

场景 2:两人同时 apply(锁未正确配置)

State 回滚到上一个版本(S3 versioning 必备):

# 前提:S3 bucket 已开启 versioning
# 查找最近的完好版本
aws s3api list-object-versions \
  --bucket my-tfstate \
  --prefix prod/vpc/terraform.tfstate \
  --query 'Versions[?IsLatest==`false`]|[0].VersionId'

# 恢复
aws s3api get-object \
  --bucket my-tfstate \
  --key prod/vpc/terraform.tfstate \
  --version-id "abc123" \
  terraform.tfstate.restored

场景 3:手动删除了 State 文件

# 没有备份 → 只能逐个 import
# 先列出当前所有资源
terraform state list  # 空(State 已丢失)

# 在云控制台逐个找到资源 ID,import 回来
# 写一个脚本批量处理
for resource in $(cat resource-list.txt); do
  terraform import "$resource" "$(get_resource_id "$resource")"
done

⚠️ 这是最痛苦的恢复方式,预防措施:S3 versioning + 定期备份 State 到另一个 bucket。

场景 4:State 中有”幽灵资源”(Terraform 不再管理但资源还在)

# 把资源从 State 中移除但不删除
terraform state rm aws_instance.old-server

# 之后 terraform destroy 不会删它
# 手动管理或在另一个 State 中 import

State 拆分策略

单体 State 的风险:改一个 DNS 记录可能因为 State 太大而 plan 需要 10 分钟。大型基础设施必须拆分:

单体 State (bad):
  prod/terraform.tfstate  ← 2000+ resources

拆分 State (good):
  prod/vpc/terraform.tfstate          ← VPC、子网、路由
  prod/eks/terraform.tfstate          ← EKS 集群
  prod/eks-node-pools/terraform.tfstate ← 节点池
  prod/rds/terraform.tfstate          ← 数据库
  prod/dns/terraform.tfstate          ← Route53 / DNS

拆分原则:

  • “改啥只影响啥”:经常改动的(节点池、Ingress)和基本不改的(VPC、子网)分开
  • “炸了不连坐”:一组资源的故障不影响其他组的 apply
  • State 之间通过 data source 引用,不用 terraform_remote_state(耦合太强)
# vpc/ 输出
output "vpc_id" { value = aws_vpc.main.id }
output "private_subnet_ids" { value = aws_subnet.private[*].id }

# eks/ 引用
data "terraform_remote_state" "vpc" {
  backend = "s3"
  config = {
    bucket = "my-tfstate"
    key    = "prod/vpc/terraform.tfstate"
    region = "ap-southeast-1"
  }
}
# 弱耦合替代:把 output 写成 SSM Parameter 或 ConfigMap
# data "aws_ssm_parameter" "vpc_id" { name = "/prod/vpc/id" }

CI/CD 集成方案

Atlantis —— GitOps for Terraform

Atlantis 是一个专门为 Terraform 设计的 GitOps 工具。它监听 GitHub/GitLab PR,自动 plan,PR 评论中展示结果,评论 atlantis apply 触发执行。

工作流:
  Developer → Push PR (改 HCL)
  GitHub → Webhook → Atlantis
  Atlantis → terraform plan → 评论到 PR
  Reviewer → 检查 plan 输出 → 评论 "atlantis apply"
  Atlantis → terraform apply → 合并 PR
# atlantis.yaml(仓库根目录)
version: 3
projects:
  - name: vpc
    dir: prod/vpc
    workspace: prod
    autoplan:
      when_modified: ["*.tf", "*.tfvars"]
      enabled: true

  - name: eks
    dir: prod/eks
    workspace: prod
    autoplan:
      when_modified: ["*.tf"]
      enabled: true

  - name: node-pools
    dir: prod/eks-node-pools
    workspace: prod
    autoplan:
      enabled: true
    # 这个需要生产审批
    apply_requirements: ["approved", "mergeable"]

Atlantis 的服务器配置:

# docker-compose.yml
services:
  atlantis:
    image: ghcr.io/runatlantis/atlantis:latest
    environment:
      ATLANTIS_REPO_ALLOWLIST: github.com/org/*
      ATLANTIS_GH_USER: atlantis-bot
      ATLANTIS_GH_TOKEN: ${GH_TOKEN}
      ATLANTIS_ATLANTIS_URL: https://atlantis.example.com
      # State backend 凭证(从环境变量注入)
      AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
      AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
    volumes:
      - /data/atlantis:/data

GitHub Actions 流水线

name: Terraform CI

on:
  pull_request:
    paths:
      - 'prod/**/*.tf'
      - 'prod/**/*.tfvars'

jobs:
  terraform:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.8.0"

      - name: Terraform fmt
        run: terraform fmt -check -recursive
        continue-on-error: true

      - name: Terraform init
        working-directory: prod/vpc
        run: terraform init
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Terraform plan
        id: plan
        working-directory: prod/vpc
        run: |
          terraform plan -no-color -out=tfplan \
            2>&1 | tee plan-output.txt

      - name: Comment plan output
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const output = fs.readFileSync('prod/vpc/plan-output.txt', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Terraform Plan\n\n\`\`\`hcl\n${output}\n\`\`\``
            });

  apply:
    needs: terraform
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production            # GitHub Environment 审批门
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - name: Terraform apply
        working-directory: prod/vpc
        run: terraform apply -auto-approve tfplan

Terragrunt —— 消除 Terraform 的重复

Terragrunt 是 Terraform 的包装器,解决原生 Terraform 最大的痛点:后端配置重复多环境 Module 调用重复

痛点:原生 Terraform 的重复地狱

每个环境的文件夹都要复制一遍 provider.tfbackend.tf

prod/vpc/terraform.tf    ← 复制粘贴
staging/vpc/terraform.tf ← 复制粘贴
dev/vpc/terraform.tf     ← 复制粘贴

Terragrunt 解决:在根目录写一份,自动生成。

目录结构

infrastructure-live/
├── terragrunt.hcl                           # 根配置(全局)
├── prod/
│   ├── env.hcl                              # 环境变量(region、account_id)
│   └── vpc/
│       └── terragrunt.hcl                   # 只写 source + inputs
└── staging/
    ├── env.hcl
    └── vpc/
        └── terragrunt.hcl

配置文件

# terragrunt.hcl(根)
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket         = "my-tfstate"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-southeast-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"
  contents  = <<EOF
provider "aws" {
  region = "${local.env_vars.region}"
  assume_role {
    role_arn = "arn:aws:iam::${local.env_vars.account_id}:role/TerraformAdmin"
  }
}
EOF
}
# prod/env.hcl
locals {
  env_vars = {
    region     = "ap-southeast-1"
    account_id = "111111111111"
    env        = "prod"
    vpc_cidr   = "10.0.0.0/16"
  }
}
# prod/vpc/terragrunt.hcl(实际模块调用——简洁到极致)
include "root" {
  path = find_in_parent_folders()
}

include "env" {
  path = "${find_in_parent_folders("env.hcl")}"
}

terraform {
  source = "git::git@github.com:org/terraform-modules.git//vpc?ref=v2.1.0"
}

inputs = {
  name       = "${local.env_vars.env}-vpc"
  cidr_block = local.env_vars.vpc_cidr

  # 依赖其他 Terragrunt 模块的输出
  transit_gateway_id = dependency.tgw.outputs.tgw_id
}

dependency "tgw" {
  config_path = "../tgw"    # 自动处理模块间依赖顺序
}

常用命令:

# 对所有环境执行 plan(预览但不操作)
terragrunt run-all plan

# 只对 prod 环境 apply
cd prod && terragrunt run-all apply

# 查看模块依赖图
terragrunt graph-dependencies

# hclfmt(格式化所有 terragrunt.hcl)
terragrunt hclfmt

terraform import —— 把”手动建的”变成”代码管的”

危险性

terraform import 的流程是反直觉的:

1. 在 .tf 文件中写 resource(此时 State 为空,这个 resource 还不存在)
2. terraform import <tf-resource> <cloud-id>
3. terraform plan → 检查差异是否正确
4. 如果 plan 不是 "No changes",说明你的 HCL 描述的和云上实际不一致
   → 修改 HCL 直到 plan 显示 "No changes"
   → 然后才能放心地 apply

如果在 import 后没有让 plan 显示 “No changes” 就直接 apply,Terraform 会尝试”修复”云上资源——删除它认为多余的配置,添加它认为缺失的配置。这就是 destroy 生产环境的常见方式。

安全 Import 流程(SOP)

# Step 1: 写 resource 块
cat > import-target.tf << 'EOF'
resource "aws_security_group" "imported" {
  name        = "placeholder"     # 会被真实值覆盖
  description = "placeholder"
  vpc_id      = "placeholder"
}
EOF

# Step 2: import
terraform import aws_security_group.imported sg-12345678

# Step 3: 读取导入后的 state
terraform state show aws_security_group.imported
# 复制上面的真实值 → 写到 .tf 文件

# Step 4: 验证
terraform plan
# 必须显示 "No changes. Your infrastructure matches the configuration."
# 如果不是 → 修改 HCL,回到 Step 3

# Step 5: 只有在 plan 为空时才提交

关联知识

参考资源

学习时间

阶段时间备注
生产级实践2026-07-02State 恢复、Atlantis、Terragrunt、Import SOP

状态: 🌱 学习中 下次复习日期: 2026-07-09