Chapter 04

状态管理:state 文件与远程后端

理解 Terraform 状态管理机制,配置远程后端实现团队协作与状态安全

为什么 Terraform 需要 State?

terraform.tfstate(状态文件)
Terraform 管理的基础设施的"真相来源"——一个 JSON 文件,记录每个资源的 ID、属性快照和依赖关系。它是 HCL 配置(期望状态)与真实云资源(实际状态)之间的映射表。没有 State,Terraform 就无法知道哪些资源已经被创建,也无法执行差量更新。
Refresh(刷新)
执行 terraform plan 时,Terraform 默认会调用 Provider API 查询每个 State 中资源的当前真实状态(称为 Refresh),然后与 State 和 HCL 配置进行三方比较,生成变更计划。-refresh=false 可跳过 Refresh 加速 plan,但可能遗漏资源的漂移(drift)。
Drift(配置漂移)
真实资源状态与 State 文件记录不符的情况。通常由手动操作(在控制台直接修改资源)引起。Terraform plan 会检测漂移并显示为需要更新的变更,提醒你将配置与实际对齐。
Backend(后端)
State 文件的存储位置和锁定机制的配置。默认 Backend 是 local(存储在本地文件系统),生产环境应配置远程 Backend(如 S3、GCS、Terraform Cloud)实现团队协作。

每次执行 terraform plan 时,Terraform 经历以下流程:

  1. 读取 State 文件,了解当前已管理的资源列表和属性
  2. 调用 Provider API(Refresh),获取每个资源的真实当前状态
  3. 读取 HCL 配置,了解用户期望的目标状态
  4. 三方对比,生成 变更计划(create/update/destroy/no-op)
State 文件包含明文敏感数据

tfstate 文件中会以明文存储数据库密码、私钥、连接字符串等敏感信息,即使你在配置中标记了 sensitive = true(该标记只影响输出显示,不影响 State 存储)。永远不要将 tfstate 提交到 Git!将以下内容加入 .gitignoreterraform.tfstateterraform.tfstate.backup*.tfvars(可能含密码)、.terraform/(Provider 二进制)

State 文件内部结构详解

// terraform.tfstate 结构示意(简化版)
{
  "version": 4,              // State 格式版本(当前为4),不同版本不兼容
  "terraform_version": "1.9.0",  // 生成此 State 的 Terraform 版本
  "serial": 42,              // 单调递增序号,每次成功 apply 后 +1(防并发冲突)
  "lineage": "550e8400-uuid", // State 的唯一标识符,创建后永不改变
  "outputs": {               // terraform output 的值
    "vpc_id": {
      "value": "vpc-0abc123",
      "type": "string",
      "sensitive": false     // sensitive output 在此处也会显示值,只是 terraform output 命令隐藏
    },
    "db_password": {
      "value": "明文密码!",  // sensitive = true 不会加密 State 中的值
      "type": "string",
      "sensitive": true
    }
  },
  "resources": [
    {
      "mode": "managed",       // "managed" = resource 块;"data" = data source 块
      "type": "aws_instance",
      "name": "web",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "id": "i-0abc123def456",   // AWS 分配的资源 ID(核心字段)
            "ami": "ami-0abcdef",
            "instance_type": "t3.medium",
            "private_ip": "10.0.1.5",   // 运行时分配的属性,不在 .tf 中定义
            "public_ip": "54.1.2.3",     // 每次 apply 可能变化
            "key_name": "deployer"
          },
          "sensitive_attributes": [],   // 标记为 sensitive 的属性列表(仍明文存储)
          "private": "eyJl...",          // Provider 私有状态(base64 编码),不应手动修改
          "dependencies": [             // 资源依赖关系(由 Terraform 自动追踪)
            "aws_subnet.public",
            "aws_security_group.web"
          ]
        }
      ]
    }
  ]
}

serial 字段:防止并发冲突的机制

serial 字段是一个单调递增的序号。每次成功的 apply 都会将它加 1。当使用远程 Backend 时,如果两个工程师同时执行 apply,第二个人提交时 Backend 会检测到 serial 冲突(提交的 serial 与服务端当前 serial 不匹配),直接拒绝写入。这就是为什么还需要 DynamoDB 提供分布式锁——在 apply 期间锁定整个操作,而不仅依靠 serial 的最终一致性检查。

State 操作命令全集

# ===== 查看操作 =====

# 列出 State 中管理的所有资源
terraform state list
# 输出示例:
# aws_vpc.main
# aws_subnet.public[0]
# aws_subnet.public[1]
# module.vpc.aws_internet_gateway.this

# 查看特定资源的详细状态(所有属性,包括运行时属性)
terraform state show aws_instance.web
terraform state show 'aws_subnet.public[0]'  # 注意:含特殊字符需引号
terraform state show 'module.vpc.aws_vpc.this'

# 以 JSON 格式输出完整 State(用于脚本处理)
terraform show -json | jq '.values.root_module.resources[] | .address'

# ===== 修改操作(谨慎使用!修改前备份 State)=====

# 重命名资源(代码重构时,避免触发 destroy+create)
# 场景:将 aws_instance.web 重命名为 aws_instance.web_server
terraform state mv aws_instance.web aws_instance.web_server
# 同时更新 .tf 文件中的资源名称,否则下次 plan 会重新出现差异

# 在 State 文件间迁移资源(如将资源从一个 workspace 移到另一个)
terraform state mv \
  -state-out=../other-project/terraform.tfstate \
  aws_s3_bucket.assets \
  aws_s3_bucket.assets

# 将资源从 Terraform 管理中"解除绑定"(不删除真实资源!)
# 场景:某资源不再由 Terraform 管理,或需要手动接管
terraform state rm aws_instance.old_server
terraform state rm 'aws_subnet.public[0]'

# ===== 高级操作 =====

# 手动拉取远程 State 到本地(用于检查或备份)
terraform state pull > backup-$(date +%Y%m%d-%H%M%S).tfstate

# 强制推送本地 State 到远程(紧急修复时用,极其危险!)
# 会覆盖远程 State,可能导致数据丢失
terraform state push -force emergency-backup.tfstate

# 强制解锁被卡住的 State(CI Job 异常终止后可能留下锁)
# LOCK_ID 在 apply 被锁定时的错误信息中显示
terraform force-unlock LOCK_ID_FROM_ERROR_MESSAGE

# 替换资源(等价于先 taint 再 plan,强制重建)
# 用于修复损坏的资源,而不修改配置
terraform apply -replace aws_instance.web
state mv/rm 操作必须同步修改 .tf 文件

terraform state mv 只修改 State 文件中的资源地址,不会修改 HCL 代码。执行 state mv 后必须同步修改 .tf 文件中对应的资源名称,否则下次 plan 仍会显示差异(旧地址的资源将被 destroy,新地址将被 create)。在 Terraform 1.1+ 中,建议优先使用 moved 块来代替手动 state mv,因为 moved 块会被记录在代码中,团队成员执行 plan 时会自动处理。

moved 块:更安全的资源重命名(Terraform 1.1+)

# 使用 moved 块代替手动 terraform state mv
# 优势:记录在代码中,团队成员 plan 时自动处理;可提交到 Git
moved {
  from = aws_instance.web
  to   = aws_instance.web_server
}

# 同时修改资源定义
resource "aws_instance" "web_server" {  # 从 "web" 改为 "web_server"
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"
  # ...
}

# 模块内资源迁移
moved {
  from = aws_security_group.web
  to   = module.web_server.aws_security_group.this
}

# 提示:团队所有人 plan 后,可以删除 moved 块(但保留一段时间便于过渡)

远程 Backend 配置

本地 State 文件存在三个核心问题:无法多人协作(两人同时 apply 会损坏 State)、不安全(含敏感数据存在开发者磁盘上)、无备份。远程 Backend 是生产环境的标配。

S3 + DynamoDB 远程后端(AWS 最佳实践)

# backend.tf — 远程后端配置
# 注意:backend 块内不能使用变量或表达式,必须硬编码
terraform {
  backend "s3" {
    bucket  = "my-company-terraform-state"  # S3 bucket 名称(必须提前创建)
    key     = "prod/us-east-1/terraform.tfstate"  # State 在 bucket 中的路径
    region  = "us-east-1"
    encrypt = true           # 使用 SSE-S3 加密(或配合 kms_key_id 使用 KMS)

    # DynamoDB 表提供分布式锁(防止多人同时 apply)
    dynamodb_table = "terraform-state-lock"

    # 使用 KMS 加密(比默认 SSE-S3 更安全,可以审计密钥使用)
    kms_key_id = "arn:aws:kms:us-east-1:123456789:key/mrk-abc123"

    # 指定认证 profile(如不使用环境变量)
    profile = "terraform-backend"
  }
}
# bootstrap/main.tf — 创建 State 存储基础设施
# 这个配置本身使用本地 State(鸡生蛋问题),单独维护

# S3 Bucket(存储 State 文件)
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-company-terraform-state"

  # 防止意外删除存储 State 的 bucket
  lifecycle {
    prevent_destroy = true
  }
}

# 启用版本控制:每次 apply 都保存 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"   # 基本加密;或使用 "aws:kms"
    }
  }
}

# 阻止公共访问(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 表(提供分布式锁)
# 表名需与 backend 配置中的 dynamodb_table 匹配
resource "aws_dynamodb_table" "terraform_lock" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"  # 按需计费,无需预置容量

  # 主键必须是 "LockID"(Terraform S3 Backend 的固定要求)
  hash_key = "LockID"

  attribute {
    name = "LockID"
    type = "S"   # S = String 类型
  }

  lifecycle {
    prevent_destroy = true
  }
}

GCS 后端(GCP)

terraform {
  backend "gcs" {
    bucket  = "my-terraform-state-bucket"
    prefix  = "prod/state"   # State 路径前缀
    # GCS 内置对象锁定,无需像 AWS 那样额外配置 DynamoDB
    # 认证:使用 GOOGLE_APPLICATION_CREDENTIALS 或 ADC
  }
}

# 创建 GCS bucket(需在配置远程 Backend 前手动或通过 bootstrap 脚本创建)
resource "google_storage_bucket" "terraform_state" {
  name          = "my-terraform-state-bucket"
  location      = "US"
  force_destroy = false

  versioning {
    enabled = true
  }

  uniform_bucket_level_access = true   # 使用 IAM 统一管理访问权限
}

Azure Storage Backend

terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "mytfstatesa"
    container_name       = "tfstate"
    key                  = "prod/terraform.tfstate"

    # Azure Blob Storage 内置 Blob 租约机制实现锁定
    # 无需额外配置
  }
}

Terraform Cloud / HCP Terraform Backend

# Terraform Cloud 是 HashiCorp 托管的远程执行和状态管理平台
# 免费计划支持无限状态存储,付费计划支持团队协作功能
terraform {
  cloud {
    organization = "my-org"
    workspaces {
      name = "production"
      # 也可以用 tags 匹配多个 workspace
      # tags = ["production", "us-east"]
    }
  }
}

# 本地认证 Terraform Cloud:
# terraform login → 浏览器打开并生成 API token 保存到 ~/.terraform.d/credentials.tfrc.json
切换 Backend 时需要 terraform init -migrate-state

当你更改 backend 配置时(如从本地切换到 S3,或从 S3 迁移到 Terraform Cloud),执行 terraform init -migrate-state 会自动将现有 State 迁移到新 Backend。Terraform 会提示确认是否复制现有 State。迁移前务必手动备份:cp terraform.tfstate terraform.tfstate.backup

sensitive 属性的常见误区

# 误区1:sensitive = true 会加密 State 中的值
# 实际:sensitive 只控制 plan/apply/output 的显示,State 中仍明文存储!

variable "db_password" {
  type      = string
  sensitive = true   # plan 输出中显示为 (sensitive value)
}

output "connection_string" {
  value     = "postgresql://user:${var.db_password}@host/db"
  sensitive = true   # terraform output 命令输出 "(sensitive value)"
  # 但是!State 文件中连接字符串仍然明文可见
}

# 误区2:只要不在 output 中输出就安全
# 实际:所有 resource 和 data source 的属性都存在 State 中
resource "aws_db_instance" "main" {
  password = var.db_password   # State 的 attributes.password 明文可见
  # ...
}

# 正确做法:依靠 Backend 层加密保护 State
# AWS:S3 Backend 配置 encrypt=true + KMS
# GCP:GCS 默认 Google 管理的密钥加密;可配置 CMEK
# Azure:Storage Account 加密(默认开启)
# 进阶:使用 SOPS 或 Vault 在应用层加密敏感值后再写入 State

Workspace 多环境管理

Workspace(工作区)
同一 Terraform 配置在不同 State 文件下的独立实例。默认 workspace 名为 default。不同 workspace 共享代码,但有各自独立的 State,因此可以管理不同的基础设施(如 dev 和 staging 各自的 EC2 实例)。
# 列出所有 workspace(* 标记当前使用的)
terraform workspace list
# * default
#   staging
#   production

# 创建并切换到新 workspace
terraform workspace new staging
terraform workspace new production

# 切换 workspace
terraform workspace select production

# 查看当前 workspace
terraform workspace show

# 删除 workspace(需先切到其他 workspace,workspace 内不能有资源)
terraform workspace select default
terraform workspace delete staging

# S3 Backend 下不同 workspace 的 State 路径:
# env:/staging/prod/terraform.tfstate
# env:/production/prod/terraform.tfstate
# 在配置中利用 terraform.workspace 区分不同环境
locals {
  env = terraform.workspace   # 当前 workspace 名称字符串

  # 用 map 查找当前环境对应的配置值
  config = {
    default = {
      instance_type    = "t3.micro"
      instance_count   = 1
      deletion_protection = false
    }
    staging = {
      instance_type    = "t3.small"
      instance_count   = 2
      deletion_protection = false
    }
    production = {
      instance_type    = "t3.large"
      instance_count   = 3
      deletion_protection = true
    }
  }

  # 获取当前环境的配置(不存在时用 default)
  current_config = lookup(local.config, local.env, local.config.default)
}

resource "aws_instance" "app" {
  count         = local.current_config.instance_count
  instance_type = local.current_config.instance_type
  tags = { Environment = local.env }
}
Workspace 适合简单隔离,生产环境推荐目录隔离

Workspace 共享代码,但环境间可能需要完全不同的资源配置(不同 region、不同 Provider 认证、不同模块组合)。生产环境最佳实践是目录隔离(每个环境有独立的 environments/dev/environments/prod/ 目录),配合模块复用代码。Workspace 更适合临时的功能分支测试,而非长期的 dev/staging/prod 环境管理。

State 管理最佳实践

State 隔离
不同应用、不同环境使用不同的 State 文件(通过不同的 key 路径区分)。一个 State 文件管理的资源越少,plan 越快,故障影响范围越小。常见结构:teams/platform/networking/terraform.tfstateteams/app/backend/terraform.tfstate
定期备份 State
即使使用 S3 版本控制,也应定期将 State 备份到独立的备份存储。State 损坏或丢失可能导致无法管理已有基础设施,需要手动 terraform import 重建。
最小化 State 中的敏感数据
尽量避免将密码直接存储在 State 中。使用 AWS Secrets Manager 或 SSM Parameter Store 等外部密钥管理系统存储密码,Terraform 中使用 Data Source 读取,这样 State 中只存储密钥的 ARN/名称而非密码本身。
State Locking
始终配置状态锁定(AWS 需 DynamoDB,GCS/Azure/Terraform Cloud 内置)。即使是单人项目也应配置,防止 CI/CD 管道与本地操作并发冲突。

本章小结

本章核心要点