Terraform状态管理

docker

tfstate状态管理

状态文件

即期望的线上环境状态;

代码有 + 状态无 = 需要新增资源
代码无 + 状态有 = 需要删除资源

状态文件内记录着代码和输入的全部信息, 也包含输入的全部明文密码;

文件安全

  1. 支持加密存储的远程后端
  2. 敏感信息分级, 避免非常敏感的数据被存入状态

管理cli

1
2
3
4
5
# 迁移状态文件到 新定义的后端, 数据不会丢失
terraform init -migrate-state

# 重新配置后端, 不会迁移状态文件
terraform init -reconfigure -backend-config="conn_str=$PGURL"
1
2
3
4
5
6
terraform plan

  + create                                  # 创建资源
  - del                                     # 删除资源
  ~ update in-place                         # 原地更新资源属性, 安全
-/+ destroy and then create replacement     # 删除后创建

漂移处理

场景: 未通过 tf 修改了云上资源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 生成计划查看当前状态与配置的差异
terraform plan -out=tfplan

# 方法 1: 以本地为准
terraform apply

# 方法 2: 以云上为准, 逐个确认
# 生成一个计划, 告诉哪些地方发生了"漂移", 并询问是否要将这些变化写入状态文件
# 注意: 只是合并到了状态文件, tf 代码还得手动写一份
terraform plan -refresh-only

# 确保没有任何差异
terraform plan

状态管理实践

1阶段, 单人维护某项目, 本地存储 terraform.tfstate
2阶段, 状态文件可以提交到 git 但是需要扫描避免泄漏密钥等信息
3阶段, cicd串行执行 apply, 避免人工执行变更
4阶段, 远程存储+锁机制, 在加cicd串行
5阶段, 权限控制, 只允许cdcd进行 apply

企业内网环境: 可以使用 pg
gitlib/gitea 等仓库存储, 适合统一规范时

后端

其它支持: gcs oss,

local

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 默认的配置
terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}

# 数据源查询
data "terraform_remote_state" "base" {
  backend = "local"  # 使用 local 后端

  config = {
    # 相对于当前执行目录的路径
    path = "../01-base/terraform.tfstate"
  }
}

gitea

1.26+ 开始支持

Settings -> Applications 中生成一个 Access Token

state_name: 状态文件的名称(例如 pve-cluster-prod); 建议每个独立的 .tf 项目对应一个唯一的名称

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
terraform {
  backend "http" {
    # Gitea 地址, 所有者和仓库名
    address        = "https://git.services.wait/api/v1/packages/chenwx/terraform/state/local-pve"
    lock_address   = "https://git.services.wait/api/v1/packages/chenwx/terraform/state/local-pve/lock"
    unlock_address = "https://git.services.wait/api/v1/packages/chenwx/terraform/state/local-pve/lock"

    # 鉴权配置
    username = "chenwx"
    password = "564ad2cd0112723ed85730e85a4aa2115b8fedc8" # 填入生成的 Token
  }
}

postgres

注解

  1. schema 默认是 terraform_remote_state 用于区分不同项目
  2. 表名固定为 states

创建库

1
CREATE DATABASE tfstate_db OWNER wait;
1
2
3
4
5
6
terraform {
  backend "pg" {
    conn_str = "postgres://wait:passw0rd@192.168.5.9:5432/tfstate?sslmode=disable"
    # schema_name = "terraform_remote_state"
  }
}

pg 后端支持的入参方式

  1. backend.conn_str 固定

  2. init -backend-config 时传递

    1
    
    terraform init -backend-config="conn_str=postgres://user:pass@db:5432/db?sslmode=disable"
    
  3. 环境变量传递

    1
    
    export PG_CONN_STR=postgres://wait:passw0rd@192.168.5.9:5432/tfstate_db?sslmode=disable
    
  4. 使用外部配置文件 (.tfbackend)

    1
    2
    
    # secret.pg.tfbackend
    conn_str = "postgres://user:pass@db:5432/db?sslmode=disable"
    
    1
    
    terraform init -backend-config=secret.pg.tfbackend
    
  5. vault

    1
    2
    
    # 从 vault 内获取连接字符串
    export PG_CONN_STR=$(vault kv get -field=conn_str homelab/infra/postgres/tf)
    

s3

方案 1: s3 + DynamoDB
方案 2: s3 + s3 原生锁

HashiCorp 官方已经宣布 DynamoDB 做状态锁的方式会被废弃, 未来推荐用 S3 原生锁

s3 原生锁

  1. 会通过自动创建和删除 .tflock 文件进行状态锁定
  2. 桶必须在创建时就启用对象锁(Object Lock)功能和版本功能

原生锁的原理
通过 If-None-Match 机制在文件不存在时才创建, 第一个任务执行成功时获取到了文件锁, 执行完成后会删除此文件;
但是如果执行到一半退出, 不一定会释放成功;

对象锁: 目的为了避免意外删除;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 创建启用对象锁的存储桶
aws s3api create-bucket --bucket your-terraform-state-bucket --region us-east-1 \
  --object-lock-enabled-for-bucket

# 启用版本控制 (原生锁的强依赖)
aws s3api put-bucket-versioning --bucket your-terraform-state-bucket \
  --versioning-configuration Status=Enabled

# 对于已经存在的桶-启用对象锁
aws s3api put-object-lock-configuration --bucket your-existing-bucket \
    --object-lock-configuration '{"ObjectLockEnabled": "Enabled"}'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
terraform {
  backend "s3" {
    bucket         = "your-terraform-state-bucket"
    # 多环境可以共用一个桶, 但 key 需要区分开
    key            = "path/to/your/statefile.tfstate"
    region         = "us-east-1"
    encrypt        = true
    use_lockfile   = true   # 启用 S3 原生锁定
  }
}

DynamoDB 锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# create bucket
aws s3 mb s3://my-terraform-state --region ap-northeast-1

# 启用版本控制
aws s3api put-bucket-versioning --bucket my-terraform-state \
    --versioning-configuration Status=Enabled

# DynamoDB 表必须预先创建并配置主键 LockID
aws dynamodb create-table \
    --table-name terraform-state-locks \
    --attribute-definitions AttributeName=LockID,AttributeType=S \
    --key-schema AttributeName=LockID,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    region         = "ap-northeast-1"
    key            = "prod/vpc/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "terraform-state-locks"    # 状态锁表
    encrypt        = true                       # 启用加密
	# kms_key_id     = "alias/terraform-kms"
	# profile        = "terraform" 		    # 可选
    # 可选, 使用 assume role
    # role_arn       = "arn:aws:iam::ACCOUNT_ID:role/TerraformRole"
  }
}

s3 管理cli

启用版本控制时的 快速回滚

1
2
3
4
5
6
7
8
9
# 查看状态版本历史
aws s3api list-object-versions --bucket my-terraform-state \
    --prefix network/terraform.tfstate

# 回滚到特定版本
aws s3api get-object --bucket my-terraform-state \
    --key network/terraform.tfstate \
    --version-id <version-id> \
    terraform.tfstate

意外锁定时的处理流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

# 1. 查看资源锁定状态
aws ec2 describe-vpc-attribute --vpc-id vpc-0abcd1234ef567890 \
    --attribute enableDhcpOptions

# 2. 如果是 Terraform 状态锁定, 检查 DynamoDB
aws dynamodb get-item --table-name terraform-locks \
    --key '{"LockID": {"S": "project-terraform"}}'

# 子目录时
aws dynamodb get-item --table-name terraform-state-lock \
  --key '{"LockID":{"S":"my-terraform-state-bucket/prod/terraform.tfstate-md5"}}'


# 扫描 DynamoDB 表中 LockID
aws dynamodb scan --table-name terraform-lock

# 3. 强制解锁(仅在确认无其他 Terraform 进程时)
terraform force-unlock <lock-id>

minio

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
terraform {
  backend "s3" {
    bucket         = "terraform"
    key            = "homeLab/proxmox/terraform.tfstate"
    region         = "us-east-1"
    endpoints = {
        s3 = "https://minio.chenwx.top"
    }
    skip_requesting_account_id = true       # 不尝试获取 AWS 账号 ID
    skip_credentials_validation = true
    skip_metadata_api_check     = true

    # MinIO 通常需要使用路径风格, 即 bucket 在 路径中而不是子域名里面
    force_path_style            = true
    use_lockfile                = true      # 原生文件锁
    encrypt                     = false     # 本地没有加密

    access_key=""
    secret_key=""
  }
}
1
2
export AWS_ACCESS_KEY_ID=
export AWS_SECRET_ACCESS_KEY=

HCP Terraform

官方远程状态管理

免费额度: 500元, 按资源数计费

1
2
3
4
5
6
7
8
terraform {
  cloud {
    organization = "my-org"
    workspaces {
      name = "dev-workspace"
    }
  }
}

阿里云 oss

  1. 创建 alicloud_oss_bucket, alicloud_ots_instance, alicloud_ots_table 资源
  2. 将代码配置添加到一个名为 backend.tf 的新 Terraform 配置文件中, 接着运行 terraform init
  3. 不需要考虑冲突时, 也可以不配置 tablestore_endpoint 和 tablestore_table

表格存储: 实例和表需要提前创建好, 表里必须有一个主键列, 名称为 LockID, 类型为字符串(String)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
terraform {
  backend "oss" {
    bucket = "bucket-for-terraform-state"
    prefix   = "tf/state"
    key   = "pro.tfstate"
    region = "cn-beijing"

    tablestore_endpoint = "https://terraform-remote.cn-hangzhou.ots.aliyuncs.com"
    tablestore_table = "statelock"
  }
}

读取另一个项目的状态数据文件

terraform_remote_state 是专门为远程后端设计的数据源

一般数据源方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
data "terraform_remote_state" "base" {
  backend = "oss"
  config = {
    bucket = "my-terraform-state"
    key    = "dev/01-base/terraform.tfstate"
    region = "cn-beijing"
  }
}

resource "alicloud_instance" "web" {
  vswitch_id = data.terraform_remote_state.base.outputs.vswitch_id
  # ...
}

http

http 远程后端 - 只读;

需要自己去维护这个 http 远程文件是否是最新状态

1
2
3
4
5
6
7
data "terraform_remote_state" "base" {
  backend = "http"

  config = {
    address = "http://localhost:8080/terraform.tfstate"
  }
}

导入元数据

方案一: 小批量逐个导入

核心逻辑是在配置文件中先声明一个空的资源块,
然后通过命令将远程资源的状态关联进来, 最后再根据实际属性补齐配置

适合导入单个资源

1
2
3
# 注意: 括号内先留空, 不写任何参数
resource "alicloud_vpc" "pro-vpc" {}
resource "alicloud_instance" "app-2" {}

执行导入

1
2
3
# 命令格式: terraform import resource_type.resource_name 线上实际的资源ID
terraform import alicloud_vpc.pro-vpc vpc-abc12345
terraform import alicloud_instance.app-2 i-2ze0kcfjzxynv4545yuz

补齐代码

1
2
3
4
5
6
7
8
9
# 查看当前状态文件内的资源的属性, 并将属性信息选择性拷贝到代码中
terraform show
terraform state show alicloud_vpc.pro-vpc
terraform state show alicloud_instance.app-2

# 补齐代码内的内容, 剔除只读参数(如 id, creation_time 等)

# 直到运行显示 "No changes" 为止
terraform plan

方案二: 小批量逐个导入 - 模块方式导入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module "testmodule" {
  source           = "../modules/instance"
  environment      = "xxx"
  vswitch_info     = "xxxx"
  vpc_id           = "xxxx"
  instance_type    = "xxxx"
  security_groups  = "xxxx"
  service          = "xxxx"
  instance_count   = "xxxxx"
  system_disk_size = "xxxxx"
  system_disk_category = "xxxxx"
  alicloud_availability_zones = "xxxxx"
}
1
2
3
4
terraform import module.testmodule.alicloud_instance.instance i-xxxxxxxxxxxxxx

# 如果该服务有多个实例,则要指定下标
terraform import module.testlistmodule.alicloud_instance."instance[0]" i-xxxxxxxxxxxxxx

方案三: import 代码块导入

优点是导入这个动作被记录在了代码中

1
2
3
4
import {
  to = alicloud_vpc.vpc1                # 本地新的资源名称
  id = "vpc-2zekv3mnatn04rognc9sy"      # 真实线上资源ID
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 生成指定的资源配置文件 tf
# Terraform 会读取远程 VPC 的属性, 并自动生成一个包含完整参数定义的 generated_vpc.tf 文件
# 注意, 这里并不会更改状态文件; 只是生成了代码
# 这里这个代码文件不一定完全合适, 可能还需要手工调整一下
terraform plan -generate-config-out=generated_vpc.tf

# 检查生成的配置文件内容, 确认无误后

# 将真实资源关联到状态文件 - 即写入状态文件
terraform import alicloud_vpc.vpc1 vpc-2zekv3mnatn04rognc9sy

# 验证 - 此时应该是 No changes
terraform plan

terraform state list

方案四: Terraformer

1
2
3
https://github.com/GoogleCloudPlatform/terraformer

terraformer-alicloud-linux-amd64
1
2
3
4
5
6
7
8
import  # 将当前状态导入 Terraform configuration; 默认输出在 generated 目录下
plan    # 预览导入

--resources         # 指定要导入的资源集合
--regions           # 指定资源所在地域
--path-pattern      # 生成的目录模式
--path-output       # 目录文件路径
--compact           # 将资源都输出到 resources.tf 一个文件中
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
terraformer-alicloud-linux-amd64 plan alicloud list

# 扫描阿里云北京区域的所有 ECS 实例
# 会在本地生成一个包含 terraform.tfstate 和 *.tf 文件的文件夹
terraformer import alicloud --resources=ecs --regions=cn-beijing

# 指定安全组
terraformer import alicloud --resources=sg --ids=sg-abc12345

# 区域的全部安全组
terraformer import alicloud --resources=sg --regions=cn-beijing

# 导入多种资源
terraformer import alicloud --resources=vpc,sg --regions=cn-beijing

aws

1
2
3
4
5
6
terraformer import aws -r sg,vpc \
    --filter Type=sg;Name=vpc_id;Value=VPC_ID --filter Type=vpc;Name=id;Value=VPC_ID

terraformer import aws --resources=vpc,subnet --filter=vpc=myvpcid --regions=eu-west-1

terraformer import aws --resources=s3 --filter="Name=tags.Abc" --regions=eu-west-1
1
terraformer plan google --resources=networks,firewall --projects=my-project --regions=europe-west1-d

方案五: aliterraformer 阿里云

是 Terraformer 的阿里云版本

1
2
3
https://help.aliyun.com/zh/terraform/import-resources-into-terraform

wget https://terraform-share.oss-cn-hangzhou.aliyuncs.com/aliterraformer/aliterraformer_linux_amd64.zip

配置供应商

1
2
3
4
5
6
7
8
terraform {
  required_providers {
    alicloud = {
      source  = "aliyun/alicloud"
      version = "~>1.272.1"
    }
  }
}

aliterraformer cli

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 若本地的 Terraform 版本 >=0.13.0,需要执行以下命令来对状态文件进行修改以适配新版本 Terraform
terraform state replace-provider -auto-approve "registry.terraform.io/-/alicloud" "aliyun/alicloud"


# 查看当前版本支持导入的产品列表
aliterraformer products

# 查看当前版本某一产品下支持导入的资源列表
aliterraformer resources -p <productName>

# 查看导入参数
aliterraformer import alicloud -h


# 导入 vpc、vswitch、ECS 实例
aliterraformer import alicloud -r=vpc,vswitch,instance

# 导入 ACK、ALB 产品下所有资源
aliterraformer import alicloud --products=ACK,ALB

# 导入 ACK、ECS 产品下所有资源以及所有 vpc、vswitch
aliterraformer import alicloud --products=ACK,ECS -r=vpc,vswitch

# 导入 ACK 下除 alicloud_cs_kubernetes 外所有资源
aliterraformer import alicloud --products=ACK --excludes=cs_kubernetes

# products 输入 ALL 代表所有的产品
aliterraformer import alicloud --products=ALL

# 导入除 VPNGateway,WAF 外的所有产品
aliterraformer import alicloud --products=ALL --excludes-products=VPNGateway,WAF

# 导入除 VPNGateway,WAF 外的所有产品,排除 alicloud_cs_kubernetes 资源
aliterraformer import alicloud --products=ALL --excludes-products=VPNGateway,WAF --excludes=cs_kubernetes

实践

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32


export ALICLOUD_REGION=cn-beijing

# 查看 ack 模块可以导入的资源
aliterraformer resources -p ACK

# 多个资源 导入 - 有失败的
aliterraformer import alicloud --products=ACK,ALB,AutoScaling,VPC,EIP,ECS,NATGateway,OSS,CDN,RAM,Alidns,SLS


# 按模块单独导入
aliterraformer resources -p VPC
aliterraformer import alicloud --products=VPC --excludes=vpc_ha_vip

aliterraformer import alicloud --products=ALB --excludes=alb_listener_additional_certificate_attachment

aliterraformer resources -p ECS
aliterraformer import alicloud --products=ECS

aliterraformer resources -p EIP
aliterraformer resources -p NATGateway
aliterraformer import alicloud --products=EIP,NATGateway

aliterraformer resources -p OSS
aliterraformer import alicloud --products=OSS

aliterraformer resources -p Alidns
aliterraformer import alicloud --products=Alidns

aliterraformer resources -p CDN
aliterraformer import alicloud --products=CDN
Licensed under CC BY-NC-SA 4.0
转载或引用本文时请遵守许可协议,知会作者并注明出处
不得用于商业用途!
最后更新于 2025-09-20 00:00 UTC