OpenTofu:安装、资源变更与 State 加密恢复
OpenTofu 用配置描述资源,经 Provider 调用目标平台,再把资源地址与实际对象的对应关系保存到 State。服务器、网络和数据库都可以这样管理;学习基本流程时,一个本地文件就足够观察创建、修改、漂移和销毁。
第一次使用可从安装与文件实验开始;已有 Terraform 项目可先看“迁移现有 Terraform 项目”;准备共享运行环境时,重点阅读 Provider 锁定、Backend 和 State 加密。通用资源管理可对照 Terraform,复杂状态修复见 State、漂移与销毁防护。
安装 OpenTofu 并创建第一个资源
准备 Linux 环境
以下使用 Linux x86-64、Bash、OpenTofu 1.12.6 和 Local Provider 2.5.3。实验只操作新建的临时目录,不需要云账号。普通用户执行 tofu;安装系统依赖时才使用 sudo。网络需要允许 HTTPS 访问 GitHub、OpenTofu Registry 及其 Provider 下载地址。
Ubuntu/Debian 先准备下载、解压和 JSON 工具:
sudo apt-get update
sudo apt-get install -y ca-certificates curl unzip jq
uname -m预期架构为 x86_64。ARM64 应改用官方 linux_arm64 资产及其校验值,不能复用下面的 AMD64 摘要。容器基础可从 Docker Engine 的安装与身份说明补齐。
安装到当前用户目录:
set -euo pipefail
TOFU_VERSION=1.12.6
TOFU_DOWNLOAD=$(mktemp -d -t tofu-download-XXXXXX)
TOFU_ZIP="$TOFU_DOWNLOAD/tofu.zip"
curl -q --fail --location --retry 3 \
"https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_linux_amd64.zip" \
--output "$TOFU_ZIP"
printf '%s %s\n' \
'5dc43da4f750f33873dc25e94587128709e819e544b7be9016b255316153c3a8' \
"$TOFU_ZIP" | sha256sum --check --status
unzip -q "$TOFU_ZIP" -d "$TOFU_DOWNLOAD/unpacked"
install -d -m 0755 "$HOME/.local/bin"
install -m 0755 "$TOFU_DOWNLOAD/unpacked/tofu" "$HOME/.local/bin/tofu"
export PATH="$HOME/.local/bin:$PATH"
command -v tofu
tofu version校验失败会终止执行,先检查下载内容和目标架构,不继续安装。正常显示 OpenTofu v1.12.6、on linux_amd64。下载可以使用组织批准的代理;CA 错误应通过安装正确根证书处理,不使用 curl -k。摘要对应 1.12.6 官方发行资产;发布者身份验证继续按 独立安装说明 检查签名,摘要本身只核对文件字节。
建立独立工作目录
后续命令继续使用同一个 Bash 会话:
umask 077
TOFU_LAB=$(mktemp -d -t tofu-lab-XXXXXX)
cd "$TOFU_LAB"
printf '工作目录:%s\n' "$PWD"不要使用已有项目目录。Local Provider 会覆盖配置指向的现有文件;新目录将实验对象限定为其中的 out/demo.txt。local_sensitive_file 资源说明
创建 main.tf,内容如下,也可下载相同的 main.tf 示例:
terraform {
required_version = "= 1.12.6"
required_providers {
local = {
source = "registry.opentofu.org/hashicorp/local"
version = "2.5.3"
}
}
}
variable "content" {
type = string
default = "synthetic-v1"
sensitive = true
}
resource "local_sensitive_file" "demo" {
filename = "${path.module}/out/demo.txt"
content = var.content
file_permission = "0600"
directory_permission = "0700"
}
output "file_path" {
value = local_sensitive_file.demo.filename
}配置可以直接拆成五部分:
main.tf
├─ required_version 可运行该配置的 CLI 版本
├─ required_providers.local Provider 的来源与版本
├─ variable.content 输入:文件内容
├─ local_sensitive_file.demo 资源地址:由 local Provider 管理
│ ├─ filename 执行机器上的文件路径
│ └─ file_permission 新建文件的权限
└─ output.file_path 输出:其他人或模块需要读取的结果terraform 是配置块名称,OpenTofu 仍使用它。local 是当前模块中的 Provider 名称;完整来源含 Registry 主机、命名空间与类型。省略主机时,OpenTofu 默认使用 registry.opentofu.org,迁移项目时应检查完整来源。Provider 要求
初始化、检查与执行
tofu fmt -check
tofu init -input=false
tofu validate
export TF_VAR_content='synthetic-v1'
tofu plan -input=false -out=first.plan
test ! -e out/demo.txt
tofu apply -input=false first.plan
cat out/demo.txt
printf '\n'
tofu state list
tofu output -raw file_path
printf '\n'init 下载 Provider 并生成 .terraform.lock.hcl,validate 检查配置。计划应显示 1 个创建、0 个修改、0 个销毁;执行前文件仍不存在。执行后内容为 synthetic-v1,State 中有 local_sensitive_file.demo,输出路径为 ./out/demo.txt。sensitive 隐藏常规输出中的内容,文件本身仍能被当前用户读取。
| 文件或目录 | 保存什么 | 如何处理 |
|---|---|---|
main.tf | 期望配置 | 纳入版本控制 |
.terraform.lock.hcl | Provider 版本和包校验信息 | 纳入版本控制并审查变化 |
.terraform/ | 初始化数据与下载的 Provider 等 | 不提交;更换 Backend 前检查初始化配置 |
terraform.tfstate | 配置地址与对象属性 | 限制访问、备份,不提交公开仓库 |
first.plan | 此次保存的执行计划 | 按敏感制品保护,过期后清理 |
out/demo.txt | 实际创建的对象 | 通过 apply 或销毁计划管理 |
本地文件适合练习,也适合在明确的执行器上生成配置。换到另一台机器后,文件不存在会再次进入创建计划;State 不会把原机器上的文件搬过来。
使用 Docker 运行
OpenTofu 从 1.10 起不再支持直接使用原先的完整官方运行镜像。官方 minimal 镜像用于构建时复制二进制,运行环境由使用者准备,见 Docker 安装说明。也可以将前面校验过的 Linux 二进制放入自建镜像。
先为镜像新建目录,复制已校验的二进制;该目录不放 State、计划或云凭据:
TOFU_IMAGE_DIR=$(mktemp -d -t tofu-image-XXXXXX)
cp "$TOFU_DOWNLOAD/unpacked/tofu" "$TOFU_IMAGE_DIR/tofu"
cd "$TOFU_IMAGE_DIR"在这里创建以下 Dockerfile,也可下载相同的 Dockerfile:
FROM ubuntu:24.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
COPY --chmod=0755 tofu /usr/local/bin/tofu
USER 10001:10001
WORKDIR /work
ENTRYPOINT ["/usr/local/bin/tofu"]# 在 Dockerfile 所在目录构建
docker build -t tofu-lab-cli:1.12.6 .
# 回到之前创建的实验目录
cd "$TOFU_LAB"
docker run --rm --user "$(id -u):$(id -g)" \
--mount "type=bind,src=$PWD,dst=/work" \
tofu-lab-cli:1.12.6 version
docker run --rm --user "$(id -u):$(id -g)" \
--mount "type=bind,src=$PWD,dst=/work" \
-e TF_VAR_content \
tofu-lab-cli:1.12.6 plan -input=false宿主 UID/GID 让生成文件归当前用户所有;项目挂载需可写,首次初始化需出网和有效 CA。容器内 /work 对应宿主实验目录,Local Provider 创建的文件留在宿主目录。后续加密还需显式传入 TF_VAR_state_passphrase。不挂载 Docker Socket 或整个用户目录,运行 OpenTofu 本身不需要它们。
理解计划、资源地址与共享执行
保存计划后输入发生变化
plan 默认读取资源现状并计算差异,-out 将计划保存给后续 apply;计划本身不创建文件。plan 命令
以下反例通过子 Shell 新建另一份实验,退出后回到原目录,不回退主实验的 State:
(
set -euo pipefail
TOFU_INPUT_LAB=$(mktemp -d -t tofu-input-XXXXXX)
cp "$TOFU_LAB/main.tf" "$TOFU_INPUT_LAB/main.tf"
cd "$TOFU_INPUT_LAB"
export TF_VAR_content='synthetic-v1'
tofu init -input=false
tofu plan -input=false -out=first.plan
export TF_VAR_content='synthetic-v2'
if tofu apply -input=false first.plan; then
printf '异常:输入不一致却未被拒绝\n' >&2
exit 1
fi
test ! -e out/demo.txt
export TF_VAR_content='synthetic-v1'
tofu apply -input=false first.plan
tofu plan -destroy -input=false -out=destroy.plan
tofu apply -input=false destroy.plan
test ! -e out/demo.txt
)1.12.6 在该实验中报告 Mismatch between input and plan variable value,不会创建文件;恢复原输入才成功。流水线应保存配置、锁文件、输入来源与计划的对应关系。审批后输入需要变化,就重新生成并审查计划,不能依赖“新环境变量一定会被忽略”。
回到主实验,修改输入并生成第二份计划:
cd "$TOFU_LAB"
export TF_VAR_content='synthetic-v2'
tofu plan -input=false -out=second.plan
tofu show -plan=second.plan
tofu apply -input=false second.plan
cat out/demo.txt
printf '\n'
if tofu plan -input=false -detailed-exitcode; then
printf '没有剩余差异\n'
else
plan_status=$?
printf 'plan 退出码:%s\n' "$plan_status"
exit "$plan_status"
fiLocal Provider 把内容变化表示为文件替换,执行后读到 synthetic-v2。下一次计划应退出 0;带 -detailed-exitcode 时,2 表示有差异,1 表示错误,CI 应分别处理。show -plan=... 读取计划;show -json -state 展开当前状态,包括敏感值,勿将完整输出上传普通日志。show 命令
外部修改为什么进入创建计划
printf '%s' 'manual-drift' > out/demo.txt
tofu plan -input=false -out=repair.plan
tofu show -plan=repair.plan
tofu apply -input=false repair.plan
test "$(cat out/demo.txt)" = 'synthetic-v2'Local Provider 用内容摘要识别文件。外部修改后,原来记录的对象不再匹配,刷新发现这一点,再按配置创建文件。因此“只改了内容”也可能看到创建动作,要结合资源类型解释计划符号。
真实平台的外部变更需要先判断是否保留:合法扩容应更新配置,不应直接套用旧容量;误操作则审查修复计划后恢复。-refresh=false 跳过现状读取,不能用来掩盖漂移。
资源重命名、Module 与批量资源
资源地址参与身份匹配。将配置中的 demo 改名为 document 时,同时修改输出引用,并增加:
moved {
from = local_sensitive_file.demo
to = local_sensitive_file.document
}在独立副本中计划,应看到地址移动,而非删除后重建;执行后检查 State 列表中的新地址。省略 moved 时,旧地址消失与新地址出现会被解释为不同资源。资源重构说明
多个资源可用 for_each,例如以 api、worker 为键管理配置文件。键进入地址 local_file.config["api"],应选择稳定业务标识,不使用密码等敏感值;更换键也需要处理地址迁移。for_each
Module 用输入和输出封装相关配置,应固定来源和版本。Provider 锁文件不锁远端 Module,模块版本或 Git 提交须单独固定。Import 用于接管已有对象,但资源是否支持导入、ID 采用什么格式由 Provider 决定;导入后仍需计划确认配置是否会修改现有对象。
Provider 锁定与多平台执行
.terraform.lock.hcl 保存 Provider 选择与校验值。常规 CI 复用已有选择;明确升级时才更新锁文件。它不包含 CLI,也不锁远端 Module。依赖锁文件
tofu providers
tofu providers lock -platform=linux_amd64 -platform=windows_amd64
tofu init -input=false -lockfile=readonly在已建 Git 仓库的项目中,用 git diff -- .terraform.lock.hcl 检查变化。初始化日志会显示签名状态,首次信任需核对发布者;多平台 runner 预先补齐平台校验值。镜像缺包或摘要不匹配时停止初始化,不应自动重写锁文件继续。
CLI、Provider、Module 分开升级。Provider Schema 或默认值变化可能导致资源替换,应先在非关键环境比较计划,再让共享执行器使用新锁文件。
Backend 与流水线分工
本地 State 适合单人实验。共享环境中,选择满足访问控制、版本恢复和锁需求的 Backend,让不同环境使用独立状态位置。锁支持取决于 Backend 实现;支持锁的后端会在相应写操作时自动获取锁。State locking
提交配置与锁文件
→ CI 初始化、validate、plan
→ 保存计划与对应输入,审查资源变化
→ 当前环境的唯一写作业执行 apply
→ 查询实际对象,核对新 State 和后续计划目标平台凭据决定能修改哪些资源,Backend 凭据决定能读取哪些状态,两者分别授权。能读 State 的人可能获得其中的连接信息;能改资源的人未必应该访问其他环境的历史状态。CI 优先使用短期身份,不复制长期云管理员密钥。
进程退出与远端 API 操作结束可能不同时发生。遇到锁冲突,先确认持锁作业是否仍运行;只有确认属于自己的残留锁,才依据锁 ID 处理。force-unlock 不撤销已发出的 API 请求,不能用 -lock=false 作为常规并发加速方式。
加密 State、验证恢复并迁移项目
sensitive 与 State 加密保护什么
主实验的常规输出隐藏了文件内容,明文 State 仍含合成值。先验证并保存受控副本:
grep -q 'synthetic-v2' terraform.tfstate
TOFU_BACKUP=$(mktemp -d -t tofu-backup-XXXXXX)
chmod 0700 "$TOFU_BACKUP"
cp terraform.tfstate "$TOFU_BACKUP/plaintext.tfstate"
chmod 0600 "$TOFU_BACKUP/plaintext.tfstate"State 加密保护保存下来的状态与计划;执行时仍需解密,out/demo.txt 也不会自动加密。密钥丢失会使密文无法恢复,密钥与状态备份要分别设计。State 与 Plan 加密
将已有明文状态迁移到加密状态
新建 encryption.tf,也可把 迁移配置 保存为这个文件名。下载的 .hcl 文件本身不会作为 .tf 配置自动加载。
variable "state_passphrase" {
type = string
sensitive = true
}
terraform {
encryption {
key_provider "pbkdf2" "lab" {
passphrase = var.state_passphrase
}
method "aes_gcm" "lab" {
keys = key_provider.pbkdf2.lab
}
method "unencrypted" "previous" {}
state {
method = method.aes_gcm.lab
fallback {
method = method.unencrypted.previous
}
}
plan {
method = method.aes_gcm.lab
fallback {
method = method.unencrypted.previous
}
}
}
}新写入使用 AES-GCM,读取时暂时允许明文回退。已有明文 State 需要这个迁移通道;新建项目可以直接使用下一节的强制配置。PBKDF2 实验口令至少 16 个字符,实际应具有足够随机性。生产可根据身份系统选择受支持的 KMS 或密钥服务。
输入口令而不将它写进命令历史:
read -r -s -p '输入实验 State 口令:' TF_VAR_state_passphrase
printf '\n'
export TF_VAR_state_passphrase
test "${#TF_VAR_state_passphrase}" -ge 16
tofu plan -input=false -out=encrypt.plan
tofu apply -input=false encrypt.plan
jq 'keys' terraform.tfstate
if grep -q 'synthetic-v2' terraform.tfstate; then
printf '异常:State 仍含明文内容\n' >&2
exit 1
fi在 1.12.6 的本地实验中,State 顶层变为 serial、lineage、meta、encrypted_data、encryption_version,文件内容不变,也不需要重建资源。密文字节不必相同,随机材料会影响结果。
历史计划、State 备份和 .backup 文件不会追溯加密。它们仍按原敏感等级保管,到期限后逐项清理,不因当前 State 已加密就公开整个项目目录。
关闭明文回退,验证错误密钥
确认读取方可以使用正确口令后,将 encryption.tf 完整替换为 强制加密配置:
variable "state_passphrase" {
type = string
sensitive = true
}
terraform {
encryption {
key_provider "pbkdf2" "lab" {
passphrase = var.state_passphrase
}
method "aes_gcm" "lab" {
keys = key_provider.pbkdf2.lab
}
state {
method = method.aes_gcm.lab
enforced = true
}
plan {
method = method.aes_gcm.lab
enforced = true
}
}
}不要同时保留两份变量声明。用错误口令运行子进程,原 Shell 的正确口令保持不变:
tofu plan -input=false -detailed-exitcode
STATE_HASH_BEFORE=$(sha256sum terraform.tfstate | cut -d ' ' -f 1)
if TF_VAR_state_passphrase='deliberately-wrong-passphrase' \
tofu plan -input=false; then
printf '异常:错误密钥未被拒绝\n' >&2
exit 1
fi
test "$STATE_HASH_BEFORE" = "$(sha256sum terraform.tfstate | cut -d ' ' -f 1)"
tofu plan -input=false -detailed-exitcode预期错误密钥无法解密,磁盘 State 摘要不变;正确口令仍可生成无变化计划。还需检查失败原因,网络或 Provider 下载失败不能算密钥测试成功。
从备份读取并拒绝旧明文
先保存当前密文,再在本地实验中短暂换入旧明文;无其他写入者,实际文件也不修改。不要把这组覆盖命令搬到共享 Backend:
cp terraform.tfstate "$TOFU_BACKUP/encrypted.tfstate"
(
set -euo pipefail
trap 'cp "$TOFU_BACKUP/encrypted.tfstate" "$TOFU_LAB/terraform.tfstate"' EXIT
cp "$TOFU_BACKUP/plaintext.tfstate" terraform.tfstate
if tofu plan -input=false; then
printf '异常:强制加密仍接受明文 State\n' >&2
exit 1
fi
)
cmp terraform.tfstate "$TOFU_BACKUP/encrypted.tfstate"
tofu plan -input=false -detailed-exitcode
tofu show -json -state | jq -e \
'.values.root_module.resources[0].values.content == "synthetic-v2"'预期先拒绝明文,子 Shell 退出时恢复密文,再正常解密,最后只输出 true。完整 show -json 包含敏感内容,因此只通过管道提取判断,不保存到公开目录。
这验证了密文备份、配置与正确口令的组合可读。生产恢复还要核对 Backend 版本、资源 ID 和平台对象:恢复旧 State 不会撤销后来的创建或删除,状态越旧,需要核对的变化越多。
轮换密钥时先让新方法写入,并保留旧方法读取历史数据;重写确认后才移除回退。备份可能仍依赖旧密钥,不能立即删除它。加密配置标识符也不要随手改名,密文元数据需要找到相应读取配置。
迁移现有 Terraform 项目
先确认 CLI、Provider、Module 和 Backend 的具体版本组合,按 官方迁移说明 在副本验证。共同使用 HCL 不代表所有扩展都支持双向读取,加密状态尤其需要单独处理。
- 停止旧流水线写入,备份配置、锁文件和最新状态,记录实际版本与 Backend 位置。
- 在隔离目录选择 OpenTofu,核对 Registry 来源;连接明确选定的测试状态副本。
init -reconfigure只重新初始化 Backend 配置,不会创建隔离副本。 - 生成计划并解释非预期差异。即使只 plan,Provider 也可能读取真实 API,需要明确只读身份与目标环境。
- 正式切换只保留一个写入口;首次应用后保存新 State 版本,查询实际对象和下一份计划。
- 回退先确认旧 CLI 能否读取新格式。若已有新资源或数据写入,先与实际对象核对并补齐管理关系,不直接覆盖切换前的旧 State。
CLI 升级、Provider 升级、Backend 迁移和启用加密分开处理,才能分辨差异来自语法、资源 Schema、状态位置还是密钥。
定位故障并安全结束资源管理
按失败阶段处理
| 现象 | 先检查什么 | 下一步 |
|---|---|---|
| init 下载失败 | Registry、代理、DNS、CA、包来源 | 修复批准的访问路径,不跳过 TLS 或摘要检查 |
| 锁文件不匹配 | 版本约束、平台和包摘要 | 比较已批准锁文件,有意升级时才重新生成 |
| validate 失败 | 目录、变量声明、Provider Schema | 修复配置,不直接尝试 apply |
| 计划出现替换 | 地址变化、字段行为、外部修改 | 对照对象 ID,决定 moved、改配置或实际替换 |
| 密文读不出 | 配置、密钥版本、Backend 对象版本 | 保留原文件,在副本只读检查,不覆盖为空 State |
| 锁一直占用 | 持锁进程与远端异步操作 | 原操作结束后处理自己的残留锁 |
| apply 部分失败 | 哪些 API 成功、哪些对象写入 State | 查询平台真实 ID,接管遗漏对象,再计划 |
| 对象已改但状态写回失败 | CLI 恢复文件、Backend 当前版本 | 保留材料,停并发写入,核对后受控恢复 |
执行失败可能留下部分成功资源。清空 State 重跑容易重复创建,恢复旧 State 也可能丢掉新映射。先查询目标平台,再决定继续、导入或恢复。tofu state pull 可以导出当前可读状态用于检查,但应存入受限位置;导出还可能升级状态格式,原始 Backend 版本也应保留。state pull
诊断通常只需要 CLI 版本、资源地址、错误类型和请求标识。需要详细日志时,限制文件权限并检查变量、令牌和请求内容,不将整份日志发到公共问题单。
销毁实验文件
确认仍在主实验目录,正确口令仍在当前 Shell:
test "$PWD" = "$TOFU_LAB"
tofu state list
tofu plan -destroy -input=false -out=destroy.plan
tofu show -plan=destroy.plan
# 确认计划只删除 local_sensitive_file.demo 后执行
tofu apply -input=false destroy.plan
test ! -e out/demo.txt
test -z "$(tofu state list)"
unset TF_VAR_content TF_VAR_state_passphrase预期删除一个文件,State 资源列表为空。计划、备份和 Provider 缓存不会全部自动清理,它们位于前面输出的专用目录,按保留需要逐项处理。关闭 Shell 后口令不再可用,保留加密备份前确认密钥保存方式。
生产销毁还需核对数据库备份、对象存储保留、域名和固定地址等用途。state rm 只解除管理关系,不能替代资源删除;退出 OpenTofu 时,明确对象是迁交、保留还是销毁,再撤销不用的执行凭据。
