为什么 Terraform 需要 State?
terraform plan 时,Terraform 默认会调用 Provider API 查询每个 State 中资源的当前真实状态(称为 Refresh),然后与 State 和 HCL 配置进行三方比较,生成变更计划。-refresh=false 可跳过 Refresh 加速 plan,但可能遗漏资源的漂移(drift)。每次执行 terraform plan 时,Terraform 经历以下流程:
- 读取 State 文件,了解当前已管理的资源列表和属性
- 调用 Provider API(Refresh),获取每个资源的真实当前状态
- 读取 HCL 配置,了解用户期望的目标状态
- 三方对比,生成 变更计划(create/update/destroy/no-op)
tfstate 文件中会以明文存储数据库密码、私钥、连接字符串等敏感信息,即使你在配置中标记了 sensitive = true(该标记只影响输出显示,不影响 State 存储)。永远不要将 tfstate 提交到 Git!将以下内容加入 .gitignore:
terraform.tfstate、terraform.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
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 配置时(如从本地切换到 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 多环境管理
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 共享代码,但环境间可能需要完全不同的资源配置(不同 region、不同 Provider 认证、不同模块组合)。生产环境最佳实践是目录隔离(每个环境有独立的 environments/dev/、environments/prod/ 目录),配合模块复用代码。Workspace 更适合临时的功能分支测试,而非长期的 dev/staging/prod 环境管理。
State 管理最佳实践
teams/platform/networking/terraform.tfstate、teams/app/backend/terraform.tfstate。terraform import 重建。本章小结
- State 是 Terraform 的核心:记录资源 ID、属性快照和依赖关系;serial 字段防止并发写入冲突;
sensitive = true只影响显示,不加密 State 中的值。 - State 文件绝不能进 Git:包含明文密码等敏感数据;始终加入 .gitignore;使用远程 Backend + KMS 加密保护。
- S3 + DynamoDB 是 AWS 最佳实践:S3 存储并版本控制 State,DynamoDB 提供分布式锁;切换后端用
terraform init -migrate-state。 - state 子命令:list/show 用于查看,rm 解除绑定(不删资源),mv 重命名(配合
moved块更安全),pull/push 用于备份和紧急恢复。 - Workspace vs 目录隔离:Workspace 适合临时分支测试;生产多环境推荐目录隔离(独立的 dev/staging/prod 目录),模块复用代码。
- Drift 检测:
terraform plan会 Refresh 真实状态;-refresh=false可跳过加速 plan,但无法检测手动改动引起的漂移。