Terraform 生产级基础设施管理实战:从单文件混乱到模块化架构的完整进化

2026-05-18 22:27 Terraform 生产级基础设施管理实战:从单文件混乱到模块化架构的完整进化已关闭评论

Terraform 生产级基础设施管理实战:从单文件混乱到模块化架构的完整进化

我花了 3 个月把一套 3000 行的 Terraform 单体配置重构为生产级基础设施即代码架构,部署时间从 15 分钟降到 2 分钟,故障率下降 80%。这篇文章就是那 3 个月的踩坑实录。

背景:单体 Terraform 有多痛?

故事从一个典型场景开始:我们的微服务从 3 个膨胀到 12 个,单体 Terraform 配置也随之爆炸。

当时我的项目结构是这样的:

infrastructure/
├── main.tf          # 3000 行,所有资源堆在一起
├── variables.tf     # 80 个变量,命名混乱
└── terraform.tfvars # 所有环境的配置混在一个文件

痛点非常具体:

  • 部署耗时terraform plan 要跑 3-5 分钟,因为每次都要计算全部资源
  • 安全风险:开发环境的误操作能导致生产环境挂掉(同一个 state 文件)
  • 变更不可追溯:没人敢动别人的模块,因为一个改动可能影响整个系统
  • 状态文件锁定频繁:多人并发执行时,state 锁冲突是家常便饭
  • 环境配置割裂:staging 和生产环境的差异只能靠注释和人的记忆

最惨的一次:我在修复开发环境的 ECS 配置,不小心把生产环境的 ECS 集群也给重建了。因为它们在同一个 state 文件里,terraform apply 扫到了所有资源。

第一步:按环境拆分 State

第一个改造:把生产、预发、开发环境的 state 彻底隔离。

backend "s3" {
  bucket         = "my-infra-state"
  key            = "env:${var.environment}/terraform.tfstate"
  region         = "us-east-1"
  encrypt        = true
  dynamodb_table = "terraform-state-lock"
}

关键点:每个环境一个独立 state key。开发同学的误操作再也不可能波及生产。

但这里有个坑——IAM 权限还要配合。我给每个环境的 Terraform 执行角色配置了不同的 S3 路径权限:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-infra-state/env:${environment}/*"
    }
  ]
}

注意: state 拆分后,不同环境之间共享的数据(比如 VPC ID)必须通过 SSM Parameter Store 或 Terraform Data Source 来获取,不能再直接引用。

第二步:按服务模块化

拆分 state 只是第一步。真正质的飞跃来自模块化。

这是我重构后的目录结构:

terraform/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   │   └── ...
│   └── prod/
│       └── ...
├── modules/
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── ecs-cluster/
│   ├── ecs-service/
│   ├── rds/
│   ├── redis/
│   ├── alb/
│   ├── route53/
│   └── iam/
└── global/
    └── iam-base/         # IAM 基础角色,全局共享

每个模块都有清晰的输入输出边界。比如 ECS Service 模块:

# modules/ecs-service/main.tf
variable "service_name" {
  description = "服务名称"
  type        = string
}

variable "container_port" {
  type        = number
  description = "容器监听端口"
  validation {
    condition     = var.container_port > 0 && var.container_port < 65536
    error_message = "端口必须在 1-65535 之间。"
  }
}

variable "desired_count" {
  type        = number
  default     = 2
  description = "期望运行实例数"
}

variable "environment" {
  type        = string
  description = "部署环境"
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "环境只能是 dev、staging 或 prod。"
  }
}

使用模块时,生产环境和开发环境的区别只是一个 .tfvars 文件:

# environments/prod/terraform.tfvars
environment   = "prod"
desired_count = 5
instance_type = "r6i.large"
min_capacity  = 3
max_capacity  = 20
enable_autoscaling = true
# environments/dev/terraform.tfvars
environment   = "dev"
desired_count = 1
instance_type = "t3.micro"
min_capacity  = 1
max_capacity  = 2
enable_autoscaling = false

模块化的好处立竿见影——团队可以并行开发不同模块,代码 review 也变成了真正的 review,而不是扫读 3000 行找差异。

第三步:Remote State 数据共享

模块化之后,不同模块之间需要交换信息。ECS 服务需要知道 ALB 的 ARN,而 ALB 又需要知道 VPC 的 ID。

用 Remote State Data Source 解决:

# 在 ECS 模块中获取 VPC 和 ALB 的信息
data "terraform_remote_state" "vpc" {
  backend = "s3"
  config = {
    bucket = "my-infra-state"
    key    = "env:${var.environment}/vpc/terraform.tfstate"
    region = "us-east-1"
  }
}

data "terraform_remote_state" "alb" {
  backend = "s3"
  config = {
    bucket = "my-infra-state"
    key    = "env:${var.environment}/alb/terraform.tfstate"
    region = "us-east-1"
  }
}

这里有个坑:远程 state 读取会创建隐式依赖。VPC 模块还没部署的话,ECS 模块的 terraform plan 就会失败。解决方案是定义好部署顺序,并通过 CI 流程强制执行。

第四步:Terraform Workspace vs 多目录

这里我踩过一个很纠结的选择题:用 Terraform Workspace 还是多目录?

最终我选了多目录方案。原因很简单——Workspace 虽然减少了重复代码,但让状态管理变得不透明:

# Workspace 方案:状态文件结构和目录脱节
terraform workspace select prod
terraform apply  # 当前是 prod,但目录结构看不出

而且 Workspace 的权限控制更麻烦——所有 Workspace 共享同一个后端的执行角色,无法做到"开发只能操作 dev workspace"的细粒度权限。

多目录 + 独立后端的方案,每个环境的配置都在 git 里清晰可见,CI/CD 也容易对接:

# .github/workflows/terraform-deploy.yml
jobs:
  terraform:
    strategy:
      matrix:
        environment: [dev, staging, prod]
    steps:
      - uses: actions/checkout@v4
      - run: |
          cd terraform/environments/${{ matrix.environment }}
          terraform init
          terraform plan

第五步:CI/CD 管道中的 Terraform

我想重点说说把 Terraform 接入 CI/CD 时踩的坑

Plan 结果需人工确认

我们采用了两阶段 pipeline:

  1. PR 阶段:自动跑 terraform plan,把结果作为 PR 评论发布
  2. Merge 阶段:跑 terraform apply,但只有特定角色可以触发
  3. # PR 阶段 - 自动 Plan
    - name: Terraform Plan
      id: plan
      run: |
        terraform plan -no-color -out=tfplan 2>&1 | tee plan_output.txt
      continue-on-error: true
    
    - name: Post Plan Comment
      uses: actions/github-script@v7
      with:
        script: |
          const fs = require('fs');
          const plan = fs.readFileSync('plan_output.txt', 'utf8');
          await github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: '## Terraform Plan\n```\n' + plan.slice(0, 60000) + '\n```'
          });
    

注意: continue-on-error: true 很重要——plan 命令本身如果返回非零退出码(比如配置错误),pipeline 不会中断,PR 评论依然能发出去,团队可以直观看到失败原因。

State Lock 超时处理

多人协作时,state 锁冲突是家常便饭。我们在 CI 中加了个小改动就解决了:

terraform {
  backend "s3" {
    # ...
    dynamodb_table = "terraform-state-lock"
    max_retries = 5
  }
}

同时在 CI 脚本中增加了锁超时提醒:

terraform plan -lock-timeout=60s || {
  echo "State 锁获取超时,可能有人正在执行 terraform"
  echo "如有必要,运行: terraform force-unlock <LOCK_ID>"
  exit 1
}

第六步:State 文件安全与备份

State 文件包含明文敏感信息,这是很多人忽略的。Terraform state 里会有数据库密码、API Key 等——即使你在配置里标记了 sensitive = true

我们的安全策略:

resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-infra-state"
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

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 = "AES256"
    }
  }
}

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
}

这四点组合拳打上之后,state bucket 的安全性基本达标了。加上 DynamoDB 的锁表,就能放心地让团队自由操作了。

踩坑实录 Top 3

坑 1:`terraform destroy` 误执行

某天凌晨,同事在开发环境跑 CI,不小心给 terraform destroy 输了个 yes。开发环境所有资源瞬间蒸发。

解决方案:给生产环境加 prevent_destroy

resource "aws_s3_bucket" "critical_bucket" {
  # ...
  lifecycle {
    prevent_destroy = true
  }
}

同时给 CI 加了一层保护:

if [ "$ENVIRONMENT" = "prod" ] && [ "$1" = "destroy" ]; then
  echo "❌ 禁止在 CI 中对生产环境执行 destroy"
  exit 1
fi

坑 2:Secrets 明文泄漏

我们曾把 RDS 密码直接写在 terraform.tfvars 里,结果同事不小心把文件贴到了 Slack 上。

解决方案:全面迁移到 AWS Secrets Manager,Terraform 只引用不存储:

data "aws_secretsmanager_secret" "db_password" {
  name = "rds-password-${var.environment}"
}

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = data.aws_secretsmanager_secret.db_password.id
}

resource "aws_db_instance" "main" {
  # ...
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
}

配置文件里再也没有明文密码了。

坑 3:模块版本管理混乱

多个服务引用同一个 VPC 模块,有人改了模块代码,其他服务莫名其妙就挂了。

解决方案:给模块打 tag,通过 Registry 方式引用:

module "vpc" {
  source = "git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.0"
  
  environment = var.environment
  vpc_cidr    = var.vpc_cidr
}

这样改模块时必须更新版本号,下游服务手动升级才能收到变更。谁都不会被突然改动的模块坑到。

最后的效果

经过两个月的逐步改造,基础设施管理的面貌彻底变了:

指标 改造前 改造后
terraform plan 时间 3-5 分钟 15-30 秒
部署故障次数/月 4-5 次 0-1 次
新服务上线时间 2-3 天 2-3 小时
团队并行开发能力 1-2 人 全团队
安全事件 发生过密码泄漏 零事件

延伸思考

如果你已经完成了上述改造,下一步可以考虑:

  1. Terragrunt:如果你的模块调用层还有很多重复代码,Terragrunt 的 generateinclude 能进一步消除样板代码
  2. Policy as Code:用 Sentinel 或 OPA 在 plan 阶段自动拦截不合规的资源创建(比如禁止所有 public S3 bucket)
  3. 自定义 Provider:我们后来给内部 CI/CD 工具写了一个自定义 Terraform Provider,把发布流程也纳入了 IaC 管理

基础设施即代码是一条渐进的路,别想一步到位。先拆分 state,再模块化,然后上 CI/CD——每一步都能带来立竿见影的改善。

你可能感兴趣的文章

来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
转载请注明出处: https://teachcourse.cn/4146.html ,谢谢支持!

资源分享

分类:Android 标签:
Kubernetes 网络策略与Cilium实战配置指南 Kubernetes 网络策略与Cilium实
python异常SyntaxError Non-UTF-8 code starting with ‘xe9’ in file python异常SyntaxError Non-U
浅谈Eclipse插件ADT 浅谈Eclipse插件ADT
浅谈SimpleCursorAdapter 浅谈SimpleCursorAdapter

评论已关闭!