Terraform 标准化技能
此技能引导代理将原始的、棕地的、导入的(如来自 Terraformer 或 terraform import)或遗留的 Terraform 代码重构为模块化、生产就绪的 HashiCorp 标准架构。
何时使用此技能
在以下情况下激活此技能:
- 用户要求“标准化”、“重构”、“清理”或“模块化” Terraform 配置。
- 原始 Terraform 代码包含硬编码字符串、魔法数字或静态云 ID(
ami-*、vpc-*、subnet-*、sg-*、arn:*)。 - 单一文件的整体配置(
main.tf包含所有代码)需要分离成标准的多文件布局(provider.tf、variables.tf、locals.tf、outputs.tf、backend.tf)。 - 云资源需要标准化的标签分类(例如
Environment、Project、ManagedBy = "terraform")。 - 代码库缺少变量定义、类型约束或描述块。
标准化工作流程
标准化任何 Terraform 代码库时,请遵循以下 5 个阶段:
┌────────────────────────────────────────────────────────┐
│ 阶段 1:审计并识别代码异味 │
│ • 检测硬编码的文字量、ID、区域和 CIDR │
│ • 检查现有文件结构与标准布局的差异 │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 阶段 2:建立标准文件架构 │
│ • 提取 provider.tf、versions.tf 和 backend.tf │
│ • 创建 variables.tf、locals.tf 和 outputs.tf │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 阶段 3:将文字量参数化为变量和本地值 │
│ • 提取带类型的环境/区域变量 │
│ • 构建 local.common_tags 以实现统一的资源标签 │
│ • 创建 terraform.tfvars.example │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 阶段 4:动态引用链接和重命名 │
│ • 用动态引用替换静态资源 ID │
│ • 用数据源替换硬编码的 AMI/子网 │
│ • 标准化资源标签(snake_case,无类型) │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 阶段 5:格式化、验证和确认 │
│ • 运行 `terraform fmt -recursive` │
│ • 运行 `terraform validate` │
│ • 使用 `terraform plan` 确认(零漂移) │
└────────────────────────────────────────────────────────┘
分步说明
1. 阶段 1:审计与发现
在进行编辑之前,扫描目标目录以识别:
- CLI 可用性:运行
terraform version(或 OpenTofu 的tofu version)以确定阶段 5 是否可以运行 CLI 格式化、验证和计划检查。 - 缺失的标准文件:检查
versions.tf、provider.tf、variables.tf、locals.tf和outputs.tf是否存在。 - 硬编码的 ID 和文字量:
- 云标识符:
ami-[a-f0-9]+、vpc-[a-f0-9]+、subnet-[a-f0-9]+、sg-[a-f0-9]+、igw-[a-f0-9]+、完整的 GCP 自链接(https://www.googleapis.com/compute/...)、静态的projectID - CIDR 块:
"10.0.0.0/16"、"0.0.0.0/0" - 实例类型:
"t3.micro"、"m5.large"、"e2-standard-4" - 静态区域和账号:
"us-east-1"、"us-central1"、"123456789012" - 原始凭据或未加密的密钥
- 云标识符:
- 格式化基线:如果安装了 Terraform,检查文件是否符合
terraform fmt -check -recursive。
2. 阶段 2:创建标准核心文件
确保目标配置被拆分为不同的、单一职责的文件:
provider.tf / versions.tf
集中 Terraform 设置、最低 CLI 版本和所需的 provider(适配 AWS、GCP 或 Azure):
# versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
# GCP 示例:
# google = {
# source = "hashicorp/google"
# version = "~> 5.5"
# }
}
}
# provider.tf (AWS)
provider "aws" {
region = var.aws_region
default_tags {
tags = local.common_tags
}
}
# provider.tf (GCP 替代方案)
# provider "google" {
# project = var.project_id
# region = var.region
# default_labels = local.common_labels
# }
单一标签/标签策略:在 provider 级别使用
default_tags/default_labels(仅在单个资源上放置资源特定的键)或在每个资源上使用merge(local.common_tags, ...)/merge(local.common_labels, ...)——切勿同时使用两者。
backend.tf(如果使用远程状态)
分离状态存储配置(AWS 的 s3,GCP 的 gcs,Azure 的 azurerm):
terraform {
backend "s3" {
bucket = "myorg-terraform-state"
key = "services/app/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
# GCP 示例:
# terraform {
# backend "gcs" {
# bucket = "myorg-terraform-state"
# prefix = "services/app"
# }
# }
3. 阶段 3:将纯字符串转换为变量和本地值
将所有硬编码参数提取到 variables.tf 和 locals.tf 中:
variables.tf 规则:
- 始终声明
type:(string、number、bool、list(string)、map(string))。 - 始终包含
description:解释目的和预期格式。 - 仅在合理时提供
default:像environment或aws_region这样的配置选项可以有默认值,但敏感值不得有硬编码的默认值。 - 将密钥标记为
sensitive = true:密码、令牌和密钥变量必须标记为敏感。
variables.tf 示例:
variable "aws_region" {
type = string
description = "将预配资源的 AWS 区域。"
default = "us-east-1"
}
variable "environment" {
type = string
description = "部署环境名称(例如 dev、staging、prod)。"
default = "production"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "环境必须是以下之一:dev、staging、production。"
}
}
variable "instance_type" {
type = string
description = "应用程序集群的 EC2 实例大小。"
default = "t3.medium"
}
variable "vpc_cidr" {
type = string
description = "虚拟私有云的基础 CIDR 块。"
default = "10.0.0.0/16"
}
locals.tf 规则:
使用 locals 处理内部派生值、环境前缀和公共标签:
locals {
name_prefix = "${var.project_name}-${var.environment}"
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
}
}
terraform.tfvars.example
始终创建一个示例变量值文件,以便团队成员知道如何提供值:
aws_region = "us-east-1"
environment = "production"
project_name = "checkout-service"
4. 阶段 4:用动态引用替换静态值(零漂移防护)
将静态云 ID 和不连续的资源引用转换为原生 HCL 表达式,同时保护现有基础设施免遭意外替换:
不可变性及 ForceNew 防护(对棕地 / Terraformer 至关重要)
- 区分 Terraform 地址和云
name属性:使用moved {}块重命名 Terraform 资源标签(tfer--prod-main-vpc→main)是安全的且零停机。更改云name属性(例如google_compute_network.name、google_compute_subnetwork.name、google_compute_firewall.name、google_compute_instance.name或aws_s3_bucket.bucket)会导致ForceNew并且会销毁并重新创建现有基础设施。相反,应在variables.tf/terraform.tfvars中将现有云名称参数化,而不是将其重写为"${local.name_prefix}-..."。 - 在棕地导入上固定导入的镜像/AMI:将硬编码的
ami-*或debian-12-bookworm-v20240515切换为most_recent = true/ 系列data查询,如果存在更新的镜像则会替换现有 VM。当需要零漂移时,通过变量/data过滤器固定导入的确切镜像/AMI 名称或 ID,并且仅在用户选择镜像升级时才启用most_recent = true。 - 将安全强化与零漂移重构分开:在零漂移迁移期间,不要静默更改行为/安全属性(例如
http_tokens"optional"→"required"、public_access_prevention"inherited"→"enforced"或 VM OAuthscopes)。将强化视为明确的选项,或清楚地列出每个有意的差异。 - 迁移时保留旧有的标签/标签键:如果现有资源使用
env = "prod",除非获得替换键的批准,否则保留现有键(或在其旁边添加标准化键)。
Terraformer 特定清理清单
- 剥离计算出的只读状态属性(AWS 上的
tags_all;GCP 上的effective_labels和terraform_labels)以及空/默认属性(""、[]、{}、false、0)。 - 移除冲突的状态转储子块(例如,当设置了
subnet_id时,移除 EC2 主network_interface)。 - 将遗留的
${data.terraform_remote_state.*.outputs.*}插值和完整自链接 URL 替换为直接资源引用(例如google_compute_network.main.id)或data查询。 - 将编号的重复资源(
vm-01、vm-02)折叠为单个for_each资源,并配以moved块(to = google_compute_instance.api["01"])。 - 将内联的
\n转义user_data/startup-script字符串提取到scripts/文件中,通过file()或templatefile()加载。 - 在变更 GCP VM
scopes或machine_type时,如果打算原地更新,确保设置allow_stopping_for_update = true。
反模式(原始 / 硬编码):
# 错误:硬编码的 ID 和文字字符串
resource "aws_instance" "tfer--i-0123456789abcdef0" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
subnet_id = "subnet-01234567"
vpc_security_group_ids = [
"sg-0abcdef1234567890"
]
}
标准模式(重构后):
# 1. 状态保留:告知 Terraform 此资源已被重命名(防止销毁和重新创建)
moved {
from = aws_instance.tfer--i-0123456789abcdef0
to = aws_instance.web
}
# 2. 基础镜像的动态查询(零漂移棕地固定确切名称,绿地把 most_recent 设为 true)
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = [var.ami_name_pattern]
}
}
# 3. 使用动态引用的标准化资源定义
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.private[0].id
vpc_security_group_ids = [aws_security_group.web.id]
# 如果 provider.tf 已设置 default_tags,则在此省略 common_tags
tags = {
Name = var.instance_name
}
}
5. 阶段 5:格式化、漂移审计和验证
-
计划前漂移审计清单(始终执行,尤其是在本地未安装
terraformCLI 时):- 将每个资源的云
name、image/ami、location/zone、tags/labels以及安全/网络设置与导入的源进行比较。 - 验证没有
ForceNew字段被意外更改,并且每个tfer--*资源都有匹配的moved {}块。 - 将任何有意的差异(例如安全强化)与零漂移重构分开报告。
- 将每个资源的云
-
运行 Terraform CLI 检查(当已安装时):
# 1. 格式化代码为标准规范 HCL 缩进
terraform fmt -recursive
# 2. 初始化 provider 插件(在未使用状态进行离线验证时使用 -backend=false)并验证语法
terraform init -backend=false
terraform validate
# 3. 如果在具有状态的活动环境中工作,确保计划不产生意外差异
terraform plan
- 总结
terraform plan更改(当检测到任何差异时): 如果terraform plan报告的内容不是No changes,在推荐apply之前停止,并提供结构化摘要:- 计划总计:说明确切的资源数量(
X to add, Y to change, Z to destroy)以及任何moved状态转换。 - 首先列出高风险更改(
-/+替换,-销毁):突出显示每个标记为替换或删除的资源,指出导致该更改的ForceNew属性(例如name、image、boot_disk),并解释如何修复(例如添加缺失的moved {}块或在tfvars中恢复原始值)。 - 就地更新(
~)和创建(+):列出每个受影响的资源和正在更改的确切属性(例如labels、tags、scopes、public_access_prevention)。 - 有意的与无意的漂移:清晰标记哪些差异是有意的(选择加入的安全强化或标签标准化)与无意的棕地漂移,以便用户批准或恢复它们。
- 计划总计:说明确切的资源数量(