基础设施自动化:编写基础设施声明与计划门禁
本篇以Terraform 状态与计划审阅为例;前置是backend、workspace、变量与资源地址。源码与输入与下文配置共用同一份文件,示例未执行。
核对日期2026-10-17;适用范围:Terraform >=1.10.0,<1.11.0,内建terraform_data;无外部provider和云账号。固定教学族不是2026最新版本或生产升级建议,状态/锁按所选backend另行核对。示例未执行;真实环境仍需验证产品配置、权限、连通与恢复结果。先读起点理解前置,再按章节完成推演。
声明与计划文件
IaC完整文件包括main.tf、dependencies.txt、两份人工plan JSON和inspect_plan.py,另有忽略状态/计划的.gitignore。示例未执行,未调用terraform init/validate/plan;JSON只保留课堂审阅所需字段,不能说是实际show输出。main.tf无provisioner、无云provider、不创建真实基础设施,读者以后可在独立目录体验单资源状态生命周期。
保守审阅与拒绝条件
IaC计划门禁只接受地址terraform_data.release、managed模式与内建类型,动作限create/update/no-op,after.input.environment必须已知且为lab。删除、替换、外来资源和缺失resource_changes都退出2,不用未知值猜安全。门禁不会执行Terraform或连接云;它不处理全部plan字段、权限与成本,不能扩大为生产策略引擎。读者对真实计划仍需独立审核。
成功失败与销毁推演
IaC预期推演中plan-create.json通过并返回executed=false,plan-delete.json拒绝;只是代码设计预期。真实读者初次apply预期创建一个terraform_data.release,第二次无配置变化plan无差异;销毁需单独plan -destroy并人工审核。不要为让删除fixture通过而放宽门禁,销毁是独立授权动作,创建审批不自动覆盖破坏性变更。
完整源码、配置与依赖
下列文件与README共用同一份仓库内容,保持输入、失败与清理合同一致。示例未执行;以下命令仅供读者在满足前置后自行运行。
dependencies.txt
Terraform >=1.10.0,<1.11.0 teaching family; built-in terraform_data only.
No external provider, no cloud identity, no provisioner, no provider installation required for this example.
Python >=3.11,<3.14 for optional offline plan JSON review, standard library only.main.tf
terraform {
required_version = ">= 1.10.0, < 1.11.0"
}
variable "environment" {
type = string
default = "lab"
validation {
condition = var.environment == "lab"
error_message = "This teaching example accepts only lab."
}
}
resource "terraform_data" "release" {
input = {
environment = var.environment
revision = "v1"
}
}
output "release" {
value = terraform_data.release.output
}plan-create.json
{
"format_version": "1.2",
"terraform_version": "1.10.5",
"resource_changes": [
{
"address": "terraform_data.release",
"mode": "managed",
"type": "terraform_data",
"name": "release",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"input": {
"environment": "lab",
"revision": "v1"
}
},
"after_unknown": {
"id": true,
"output": true
}
}
}
]
}plan-delete.json
{
"format_version": "1.2",
"terraform_version": "1.10.5",
"resource_changes": [
{
"address": "terraform_data.release",
"mode": "managed",
"type": "terraform_data",
"name": "release",
"change": {
"actions": [
"delete"
],
"before": {
"input": {
"environment": "lab",
"revision": "v1"
}
},
"after": null
}
}
]
}inspect_plan.py
"""Conservative offline teaching gate for saved plan JSON; never invokes Terraform."""
import argparse
import json
import sys
from pathlib import Path
def inspect(plan):
if not isinstance(plan, dict):
raise ValueError("plan must be an object")
if plan.get("format_version", "").split(".")[0] != "1":
raise ValueError("unsupported plan format")
changes = plan.get("resource_changes")
if not isinstance(changes, list):
raise ValueError("resource_changes must be an explicit list")
for change in changes:
if not isinstance(change, dict) or not isinstance(change.get("change"), dict):
raise ValueError("each resource change must be an object with a change object")
if change["address"] != "terraform_data.release" or change["type"] != "terraform_data" or change["mode"] != "managed":
raise ValueError("unexpected resource")
actions = change["change"]["actions"]
if actions not in [["create"], ["update"], ["no-op"]]:
raise ValueError("deletion or replacement requires a separate review")
after = change["change"].get("after")
if not isinstance(after, dict) or not isinstance(after.get("input"), dict) or after["input"].get("environment") != "lab":
raise ValueError("after environment must be known and lab")
return {"changesReviewed": len(changes), "executed": False}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("plan", type=Path)
args = parser.parse_args()
try:
result = inspect(json.loads(args.plan.read_text(encoding="utf-8")))
except (OSError, ValueError, TypeError, KeyError) as error:
print(str(error), file=sys.stderr)
return 2
print(json.dumps(result))
return 0
if __name__ == "__main__":
raise SystemExit(main()).gitignore
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
crash.log
*.auto.tfvarsREADME.md
# Terraform 本地状态与计划审阅
示例未执行。依赖见dependencies.txt;main.tf只用内建terraform_data,无云provider、无凭据、无provisioner。Terraform1.10族是固定教学范围,不声称最新或维护推荐。两份plan JSON为人工编写的最小审阅fixtures,不伪称terraform show的完整实测输出。
读者自跑前置:自己准备Terraform1.10隔离目录,将main.tf复制进去;确认目录无生产state、无远端backend与自动加载变量。读者以后可按顺序自行执行 `terraform init`、`terraform plan -out=lab.tfplan`、人工审核后 `terraform apply lab.tfplan`;本轮连validate也未执行。预期初次创建一个terraform_data.release并输出environment=lab/revision=v1;第二次无改动plan应无差异。仅更改input的revision预期update,不应凭名字推断云重建。plan文件与state都可能含秘密,即使输出标记sensitive也不能公开。
纯离线可选程序:`python inspect_plan.py plan-create.json`预期退出0且executed=false;`python inspect_plan.py plan-delete.json`预期退出2,拒绝删除或替换;缺失resource_changes、未知after环境或外来资源也拒绝。它只是此单资源课堂门禁,不处理完整策略、drift/权限/成本,不能单独批准生产apply。
恢复与销毁:真实读者需先保存隔离state副本,先 `terraform plan -destroy -out=destroy.tfplan` 人工核对只删除课堂资源,再 `terraform apply destroy.tfplan`;门禁会拒绝destroy,销毁需要单独人工审阅流程,不能修改门禁来冒充创建审批。terraform_data无云对象,但state仍应按保留规则处置。确认销毁完成后删除本实验计划及状态副本,不删除共享backend。丢state不会删除真实对象;误force-unlock可能造成并发写,先证实持锁进程结束并确认lock ID。不要将.terraform.lock.hcl依赖锁误当state写锁;此例无外部provider所以不伪造锁文件。本篇练习与参考解答
练习:plan-delete.json被门禁拒绝,是否应改门禁允许?
参考解答
不应为通过示例而放宽。销毁是独立审阅动作,门禁刻意仅允许本地单资源的创建/更新/无改动。
核对资料与相邻阅读
继续阅读
- 知识专题
- 知识专题