Community编程与开发github.com

daily-dreamer/terraform-standardize

标准化原始的、棕地(brownfield)或由 Terraformer 导入的 Terraform 代码(AWS、GCP、Azure),使其达到生产级别的 HashiCorp 标准:分离 provider.tf、versions.tf 和 backend.tf,将硬编码字符串参数化为带类型的 variables.tf 和 locals.tf,用动态引用替换静态云 ID,通过 moved 块实施 ForceNew 和零漂移防护,并执行规范的 HCL 格式化、验证和计划差异摘要。

terraform-standardize 是什么?

这是一个 AI 代理技能,用于将原始的、棕地(brownfield)的、通过 Terraformer 或 `terraform import` 导入的、或遗留的 Terraform 代码重构为模块化、生产就绪的 HashiCorp 标准架构。该技能引导代理遵循一个五阶段工作流程:审计并识别代码异味(如硬编码的云 ID、CIDR 块、实例类型);建立标准的文件架构(分离 provider.tf、versions.tf、backend.tf、variables.tf、locals.tf 和 outputs.tf);将文字量参数化为带类型和描述的变量和本地值;用动态 HCL 引用替换静态资源 ID,并通过 moved 块和针对扁平化的 Image/AMI 的零漂移保护来保护现有基础设施;最后运行 `terraform fmt`、`terraform validate` 和 `terraform plan` 以验证零漂移。该技能支持 AWS、GCP 和 Azure,并提供针对 Terraformer 导入的特定清理清单,包括折叠重复资源、剥离计算出的只读属性以及处理内联脚本。它还强调区分安全强化与零漂移重构,保持现有标签/标签键,并在 Terraform 计划检测到差异时提供详细的结构化摘要。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/daily-dreamer/terraform-standardize/tree/HEAD/skills/terraform-standardize

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

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:审计与发现

在进行编辑之前,扫描目标目录以识别:

  1. CLI 可用性:运行 terraform version(或 OpenTofu 的 tofu version)以确定阶段 5 是否可以运行 CLI 格式化、验证和计划检查。
  2. 缺失的标准文件:检查 versions.tf、provider.tf、variables.tf、locals.tf 和 outputs.tf 是否存在。
  3. 硬编码的 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/...)、静态的 project ID
    • CIDR 块:"10.0.0.0/16"、"0.0.0.0/0"
    • 实例类型:"t3.micro"、"m5.large"、"e2-standard-4"
    • 静态区域和账号:"us-east-1"、"us-central1"、"123456789012"
    • 原始凭据或未加密的密钥
  4. 格式化基线:如果安装了 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 OAuth scopes)。将强化视为明确的选项,或清楚地列出每个有意的差异。
  • 迁移时保留旧有的标签/标签键:如果现有资源使用 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:格式化、漂移审计和验证

  1. 计划前漂移审计清单(始终执行,尤其是在本地未安装 terraform CLI 时):

    • 将每个资源的云 name、image/ami、location/zone、tags/labels 以及安全/网络设置与导入的源进行比较。
    • 验证没有 ForceNew 字段被意外更改,并且每个 tfer--* 资源都有匹配的 moved {} 块。
    • 将任何有意的差异(例如安全强化)与零漂移重构分开报告。
  2. 运行 Terraform CLI 检查(当已安装时):

# 1. 格式化代码为标准规范 HCL 缩进
terraform fmt -recursive

# 2. 初始化 provider 插件(在未使用状态进行离线验证时使用 -backend=false)并验证语法
terraform init -backend=false
terraform validate

# 3. 如果在具有状态的活动环境中工作,确保计划不产生意外差异
terraform plan
  1. 总结 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)。
    • 有意的与无意的漂移:清晰标记哪些差异是有意的(选择加入的安全强化或标签标准化)与无意的棕地漂移,以便用户批准或恢复它们。

详细指南与参考

  • 有关域分组模式,请参阅标准文件布局。
  • 有关命名、ForceNew 防护、标签/标签键和反模式清单,请参阅重构规则。

相关技能