路线图

22-Terraform

星辉 2026-07-02 阅读 5 min 949 字 路线图
22-Terraform 封面

基础设施即代码(Infrastructure as Code, IaC)是把基础设施的创建、变更、销毁用代码描述并版本化管理的实践。Terraform 是 HashiCorp 出品的 IaC 工具,曾一度是这个领域的事实标准。本章覆盖 Terraform 的核心概念、工作流、State 管理、模块化、多环境实践与常见陷阱,并从 Terragrunt 规模化实践中提取工程化要点。

IaC 解决什么问题#

没有 IaC 之前,基础设施状态散落在工程师脑子里、过时的 Wiki 文档里、控制台的历史记录里。带来几个致命问题:

  • 无法复现:生产环境出了问题,无法在测试环境精确复现,因为两个环境配置已悄悄漂移。
  • 变更追溯困难:安全合规要求"三个月前这个端口怎么开放的",没人知道。
  • 团队协作摩擦:新人不知道为什么某个资源这样配置,不敢改不敢删,技术债越积越多。

IaC 用代码描述期望状态,用 Git 管理版本,用 CI/CD 执行变更。每次变更有历史记录、代码审查、自动化测试。

核心概念#

Provider#

Provider 是 Terraform 与各种 API 通信的桥梁。AWS、GCP、阿里云、GitHub、Kubernetes 都有对应的 Provider:

hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"      # 允许 5.x,不跨大版本
    }
  }
  required_version = ">= 1.6.0"
}

provider "aws" {
  region = var.aws_region
  default_tags {
    tags = {
      ManagedBy   = "terraform"
      Environment = var.environment
    }
  }
}

Resource#

Resource 是 Terraform 管理的最小单元,对应一个真实的基础设施资源:

hcl
# 语法:resource "<类型>" "<本地名称>" { ... }
resource "aws_s3_bucket" "logs" {
  bucket = "my-company-logs-${var.environment}"
  tags = {
    Name = "Application Logs"
  }
}

# 引用其他资源的属性
resource "aws_s3_bucket_versioning" "logs" {
  bucket = aws_s3_bucket.logs.id    # 引用上面 bucket 的 id
  versioning_configuration {
    status = "Enabled"
  }
}

Module#

Module 是可复用的 Terraform 代码单元,类似函数。把通用基础设施(VPC、EKS、RDS)封装成 Module,多个环境引用同一份 Module 传不同参数:

hcl
module "eks" {
  source  = "../../modules/eks-cluster"
  # 或使用 Terraform Registry 公共模块
  # source  = "terraform-aws-modules/eks/aws"
  # version = "~> 20.0"

  cluster_name    = "prod-cluster"
  cluster_version = "1.30"
  node_groups = {
    general = {
      instance_types = ["m5.xlarge"]
      min_size       = 2
      max_size       = 20
      desired_size   = 3
    }
  }
}

State#

State 是 Terraform 的核心,记录"Terraform 认为真实世界现在是什么状态"。它的作用:

  • 记录 Terraform 管理的资源列表及其 ID
  • 计算 plan 时的 diff(期望状态 vs 当前状态)
  • 追踪资源依赖关系

State 是敏感数据:可能包含数据库密码、私钥等,不能放到 Git 里。

基本工作流#

bash
# 初始化:下载 Provider 插件
terraform init

# 格式化代码
terraform fmt -recursive

# 语法检查
terraform validate

# 预览变更(最重要的命令,必须仔细看)
terraform plan -out=tfplan

# 应用变更
terraform apply tfplan

# 销毁资源(危险!生产环境谨慎使用)
terraform destroy

plan 的输出要仔细看:

text
# aws_instance.web will be updated in-place    ← 原地更新,低风险
~ resource "aws_instance" "web" {
  ~ instance_type = "t3.small" -> "t3.medium"
  }

# aws_db_instance.main must be replaced        ← 销毁重建!高风险
-/+ resource "aws_db_instance" "main" {
  ~ identifier = "prod-db" -> "prod-db-v2"
  }

看到 must be replaced 要非常小心——某些资源(RDS、ElastiCache)重建会有停机时间。

State 管理#

Remote State(S3 + DynamoDB)#

生产环境 State 必须远端存储并加锁,防止多人并发操作:

hcl
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/eks/terraform.tfstate"
    region         = "us-west-2"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:us-west-2:123456789012:key/mrk-xxx"
    use_lockfile   = true                     # S3 原生锁(1.10 引入、1.11 起默认推荐)
    # dynamodb_table = "terraform-state-lock" # DynamoDB 锁已于 1.11 弃用,未来版本移除
  }
}

创建 backend 资源(这部分不能用 Terraform 管理自己的 backend):

bash
aws s3api create-bucket --bucket my-terraform-state --region us-west-2 \
  --create-bucket-configuration LocationConstraint=us-west-2
aws s3api put-bucket-versioning --bucket my-terraform-state \
  --versioning-configuration Status=Enabled

# S3 原生锁(use_lockfile)无需额外资源;下面的 DynamoDB 锁表仅为兼容旧配置,
# 1.11 起 dynamodb_table 已弃用,新项目直接用 use_lockfile = true
aws dynamodb create-table --table-name terraform-state-lock \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

阿里云 OSS 后端:

hcl
backend "oss" {
  bucket   = "my-terraform-state"
  prefix   = "prod/eks"
  region   = "cn-hangzhou"
  encrypt  = true
  tablestore_endpoint = "https://xxx.cn-hangzhou.ots.aliyuncs.com"
  tablestore_table    = "terraform-lock"
}

State 操作#

bash
# 查看 state 中的资源列表
terraform state list

# 查看单个资源的 state 详情
terraform state show aws_eks_cluster.main

# 将已有资源导入 state(资源已存在,但不在 state 中)
terraform import aws_s3_bucket.legacy my-existing-bucket-name

# 移动资源(重构代码时)
terraform state mv aws_security_group.old_name aws_security_group.new_name

# 从 state 中移除资源(不删除真实资源,只停止 Terraform 管理)
terraform state rm aws_instance.temporary

模块化与多环境#

目录结构#

text
infrastructure/
├── modules/                # 可复用模块
│   ├── eks-cluster/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── rds-instance/
│   └── networking/
├── environments/           # 环境配置
│   ├── dev/
│   │   ├── main.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   └── prod/
└── global/                 # IAM、Route53 等全局资源

Workspace vs 独立目录#

Workspace 允许同一套代码管理多个环境的 State,但实际生产中更推荐用独立目录隔离环境:

  • Workspace:同一份代码 + 同一目录,通过 terraform workspace select dev/prod 切换。风险是容易误操作——在 prod workspace 里跑 dev 的配置。
  • 独立目录:每个环境一个目录,各自有 backend 配置和 tfvars。隔离彻底,不会误操作。

经验:Workspace 适合临时环境(PR preview),生产环境用独立目录。

漂移检测与修正#

有人直接在控制台改了资源,导致真实状态和 State 不一致:

bash
# 检测漂移(不做任何变更,只刷新 state)
terraform plan -refresh-only

# 将真实状态同步到 State(不修改真实资源)
terraform apply -refresh-only

常见坑#

1. State 漂移#

手动改控制台导致漂移。解决:CI 定期跑 terraform plan -refresh-only,有 diff 就告警。

2. Destroy 顺序问题#

Terraform 通常能自动处理依赖顺序,但某些情况需手动指定 depends_on:

hcl
resource "aws_eks_fargate_profile" "coredns" {
  depends_on = [aws_eks_addon.coredns]   # 必须等 EKS 集群完全就绪
}

3. Provider 版本漂移#

不锁版本每次 terraform init 可能下载不同版本:

bash
# 生成 .terraform.lock.hcl 后提交到 Git
terraform providers lock \
  -platform=linux_amd64 \
  -platform=darwin_arm64

4. 敏感值泄露到 State#

数据库密码、私钥如果放在 resource 里,会明文存在 State 中:

hcl
# 不要这样做
resource "aws_db_instance" "main" {
  password = "my-hardcoded-password"   # 会出现在 state 里!
}

# 用 random_password 生成 + Secrets Manager 存储
resource "random_password" "db" {
  length  = 32
  special = false
}
resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id     = aws_secretsmanager_secret.db.id
  secret_string = random_password.db.result
}

5. 改了 identifier 触发重建#

某些资源改了不可变字段会触发销毁重建(RDS、ElastiCache),生产环境会有停机。plan 看到 must be replaced 要特别警惕。

规模化实践(从 Terragrunt 提取)#

当 state 超过 20 个时,纯 Terraform 的管理会变得痛苦——backend.tf 到处复制、provider 配置到处复制、跨 state 引用要手写 terraform_remote_state、批量 apply 要手动 cd。

Terragrunt 是 Terraform 的 wrapper,解决规模化问题:

DRY 的 backend 和 provider#

一份根 terragrunt.hcl 定义 backend 和 provider,所有子单元通过 include 继承:

hcl
# live/terragrunt.hcl(根配置)
remote_state {
  backend = "s3"
  generate = { path = "backend.tf", if_exists = "overwrite" }
  config = {
    bucket         = "my-company-tfstate"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "us-west-2"
    encrypt        = true
    use_lockfile   = true      # S3 原生锁;dynamodb_table 已于 TF 1.11 弃用
  }
}
hcl
# live/prod/us-west-2/vpc/terragrunt.hcl(子单元)
include "root" {
  path = find_in_parent_folders()
}
terraform {
  source = "git::git@github.com:org/terraform-modules.git//vpc?ref=v1.5.0"
}
inputs = {
  cidr_block = "10.0.0.0/16"
}

跨 state 依赖#

hcl
# live/prod/us-west-2/eks/terragrunt.hcl
dependency "vpc" {
  config_path = "../vpc"
  mock_outputs = {
    vpc_id             = "vpc-mock"
    private_subnet_ids = ["subnet-mock-1", "subnet-mock-2"]
  }
  mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}

inputs = {
  vpc_id     = dependency.vpc.outputs.vpc_id
  subnet_ids = dependency.vpc.outputs.private_subnet_ids
}

Terragrunt 在 apply eks 之前会先检查 ../vpc 是否已 apply,从其 state 读 outputs 注入到当前 inputs。mock_outputs 让 plan/validate 阶段不报错。

铁律:mock_outputs_allowed_terraform_commands 只列 ["validate", "plan"],永远不要加 apply。

批量操作#

bash
# 在整个 live/prod 目录下按依赖顺序 plan 所有 state
cd live/prod
terragrunt run --all plan

# apply 所有(限并发避免打爆 API)
terragrunt run --all apply --parallelism 4

CLI 重构后,旧的 run-all 子命令与 --terragrunt- 前缀参数已废弃(legacy 参数在 v0.85.0 移除),改用 run --all + --parallelism。

run --all 递归扫描所有 terragrunt.hcl,解析 dependency 构建 DAG,按拓扑顺序调用 Terraform(无依赖的并发)。

什么时候用 Terragrunt#

  • State 少于 10 个:直接写 Terraform 更简单。
  • State 超过 20 个 + 多环境/多 region:Terragrunt 的甜蜜区。
  • 完全 Pulumi 体系:Pulumi 有自己的 Stack + Component,Terragrunt 管不了。

小结#

Terraform 的核心工作流是 init → plan → apply,plan 是最重要的命令——看到 must be replaced 要警惕。State 是敏感数据,必须远端存储(S3/OSS + 锁表),不能进 Git。多环境用独立目录隔离比 Workspace 更安全。模块化是复用的关键,目录结构按 modules + environments 分层。规模化场景(state 超过 20 个)引入 Terragrunt 解决 DRY、跨 state 依赖、批量操作三大痛点。核心实践不变:state 远端存储 + lock + PR 驱动的 plan + 强制 policy 检查。