1.6.14.1. mapotf:Terraform 元编程工具

在大型基础设施即代码(IaC)项目中,经常会遇到如何统一管理和改动 Terraform 配置的问题。例如,为所有模块添加 modtm 调用,修改发往 modtm 的标签,统一规定 AzureRM / AzAPI Provider 的最低版本等等。Terraform 本身的一些局限性也导致在日常使用时可能会遇到一些令人窘迫的场景,例如,一些用户希望 Terraform 模块中的资源启用 ignore_changes 来忽略某些属性的漂移,但 Terraform 当前并不支持在 ignore_changes 等生命周期参数中使用变量。这意味着开源模块开发者无法通过参数化手段灵活地满足不同用户对忽略属性的定制需求。此外,不同模块开发者可能各自实现了一些通用的设计模式(比如为数据库创建私有终端节点、为存储桶配置日志等),这些模式在各模块中大同小异。如果能有统一的模式库,模块作者就无需到处查找示例或教程,只需应用共享的模式即可。

为了解决以上痛点,微软 Azure 团队推出了名为 mapotf 的开源工具。mapotf 是 “MetA PrOgramming for TerraForm” 的缩写。正如名称所示,它旨在为 Terraform 提供一种元编程机制,允许对 Terraform 配置进行额外的编程式修改。简单来说,mapotf 是一个与 Terraform 配合使用的元编程工具。借助 mapotf,开发者和平台工程团队可以在不直接修改源代码的前提下动态地生成、修改或移除 Terraform 配置,从而满足一些 Terraform 原生能力有限的场景需求。

1.6.14.1.1. mapotf 的作用与功能

mapotf 的核心思想是将 Terraform 配置视为可以被匹配和重写的对象,并通过声明式规则来批量应用更改。mapotf 提供了两种主要类型的块:数据源(data转换(transform。用户可以在 mapotf 配置文件(使用HCL语言编写)中定义特定的 data 块去匹配 Terraform 配置中的某些目标元素;然后通过 transform 块定义如何改写这些被匹配到的部分。在转换阶段,mapotf 支持多种操作,比如就地更新已有属性值、插入新的资源或区块,以及删除指定的配置片段。这一切都由 mapotf 引擎根据配置规则自动完成,相当于为 Terraform 配置提供了一个“查找并替换”的智能机制。

通过这样的元编程能力,mapotf 能够实现许多强大的用途:

  • 支持 Terraform 原生不支持的定制:针对前述的 ignore_changes 场景,mapotf 可以在不更改模块源码的情况下,为资源块动态加入所需的 ignore_changes 属性,从而规避 Terraform 的限制。正如 mapotf 作者在 Terraform 的相关 Issue 中提到的:“通过元编程,我们可以定制修改根模块和依赖模块中的任何配置”。这使得模块作者可以提供一个可选的外部补丁,让使用者按需应用。例如,有用户采用了自定义的 Azure Policy 或 AWS Config 来自动纠正资源,这会导致资源的属性被平台修改而产生偏差。借助 mapotf,不同用户可以各自定义需要忽略的属性列表,而模块无需预先为所有可能性做硬编码处理。

  • 推广通用模式和最佳实践mapotf 可充当一个模式库工具。针对常见的架构需求(如为某类资源添加监控诊断、配置私有链接等),平台团队可以预先编写 mapotf 配置,将这些经过验证的模式封装起来。模块开发者在设计模块时,无需从零开始实现这些模式,只需在需要时引用相应的 mapotf 配置即可自动将模式应用到模块中。这既减少了重复劳动,又保证了不同模块之间实践的一致性。一个例子,不同的企业和团队可能都会尝试编写包含 Azure Storage Account 存储服务的模块,无论该模块是如何实现的,他们可能都要为 Storage Account 添加 Private Endpoint。这时我们可以编写一段 mapotf 配置,匹配代码中的 azurerm_storage_account 资源块,然后为其生成对应的 Private Endpoint 相关的资源。

  • 集中式治理:对于组织内大规模使用的 Terraform 模块,mapotf 提供了一种集中施加变更的手段。平台治理团队(DevOps/SRE)可以维护一套 mapotf 规则,批量对所有模块进行统一的调整。例如,要求所有模块资源都添加某个标签,或插入一个额外的资源用于审计/遥测。通过 mapotf,这种变更可以一次性在多个模块上自动执行,而不需要人工逐个修改每个模块,大大提高治理效率。

  • 模块配置的批量升级:当底层云提供商的 Terraform Provider 发布大版本更新时,往往会引入不兼容的更改,迫使模块代码需要跟随调整。mapotf 在这方面也大有用武之地。通过编写一组映射旧版配置到新版要求的规则,mapotf 可以自动重构 Terraform 配置以符合新版本 Provider 的升级指南。例如,对于 AzureRM Provider 从 3.x 升级到 4.x 的情形,就可以编写相应的 mapotf 升级代码,批量完成资源属性的重命名、字段删除等改动。这方面的一个实践工具是 TerraformConfigWelder,它利用了 mapotf 的配置文件机制来执行 Provider 升级转换。换言之,mapotf 为模块维护者提供了“批量脚手架”功能,在需要进行大规模机械式替换时(例如 Provider 不再支持某字段,需要移除),可以借助 mapotf 快速完成并降低出错风险。

更形象地总结 mapotf:假如说 Terraform Module 是一组可复用的 Terraform 配置,那么 mapotf 就是一组可复用的针对 Terraform 代码的变更模式。

1.6.14.1.2. 使用方法

安装 mapotf 非常简便,可通过 Go 命令一键安装最新版本:

go install github.com/Azure/mapotf@latest

安装完成后,mapotf 以命令行工具形式工作。它可以作为 Terraform 的包装器运行,不取代 Terraform 本身的功能。mapotf 的典型工作流程如下:

  1. 准备 mapotf 配置:接下来,用户需要提供 mapotf 的“元编程”的代码,可以是本地文件夹,也可以引用远程 Git 仓库中的配置目录。通过参数 --mptf-dir 可以指定配置来源。例如,Azure 官方提供了一些示例配置库,可以直接通过 Git URL 引用。这些配置文件使用标准的 HCL 语法编写,里面包含若干 data 块和对应的 transform 块,定义了需要匹配的目标以及转换的指令。

  2. 执行转换(transform):mapotf主要提供两种执行模式:

    • 即时应用(apply):使用mapotf apply命令可以直接执行转换并随即调用 terraform apply 来部署更改。在此模式下,mapotf 会先下载并加载指定的配置规则,备份当前的 Terraform 文件,然后对当前 Terraform 文件进行修改(例如插入或更新代码片段),之后自动触发 Terraform 的 Plan 和 Apply 操作,并且把 Stdout 和 Stderr 重定向到 Terraform 进程上。在 Terraform 进程结束后,再将 Terraform 文件还原到变换前的内容,并删除备份文件。
    • 仅转换(transform):使用 mapotf transform 命令则仅执行代码转换,而不调用后续的 Terraform 部署。mapotf 会将修改过的 Terraform 文件保留下来,并为每个变更的文件生成一个备份副本(扩展名为.tf.mptfbackup)供用户参考或回滚。此模式下,用户可以自行审查代码差异,然后通过手动执行 terraform plan/apply 来应用,或者如果发现问题,可以使用 mapotf reset 快速还原文件。一旦确认修改是需要的且正确的,还可以用 mapotf clean-backup 清除所有备份文件,保持代码库整洁。
  3. 查看和验证修改:在转换执行后,无论是 apply 还是 transform 模式,mapotf 都会在 Terraform 代码中直接体现出修改结果。比如在上文提到的 AKS 集群示例中,mapotf 为资源 azurerm_kubernetes_cluster 的生命周期配置添加了 microsoft_defender[0].log_analytics_workspace_id 属性到 ignore_changes 列表中。用户可以打开相应的 .tf 文件,看到这些新的配置已经插入其中,且原始内容已被备份到 .tf.mptfbackup 文件中。只有当用户确认执行 Terraform 部署(或者可以保存变更)后,这些改动才真正生效。如果中途选择不应用(例如 mapotf apply 过程中回答了"no"),mapotf 会自动将所有文件还原并删除备份,从而保证代码库不会留下意外的中间状态。

通过上述方式,mapotf 将原本需要手工编辑 Terraform 代码的操作自动化了。开发者既可以将其作为开发期间的辅助工具,根据需要临时调整配置;也可以将其集成到CI/CD流程中批量执行标准化改造。值得注意的是,由于 mapotf 直接修改 Terraform 配置文件,其产生的变更应纳入版本控制并经过审阅,以确保对基础设施的修改是可追踪且符合预期的。值得注意的是,mapotf 对 Terraform 的变更不仅限于当前文件夹内的 .tf 文件,也是通过设置 -r 参数,递归地对所有涉及到的 Terraform 模块代码进行变换,这就使得用户可以动态地对第三方开源模块代码进行定制化变换。

1.6.14.1.3. 与 Pre-commit 的集成及在 AVM 中的应用

mapotf 不仅可以由开发者手动执行,还可以很好地集成到代码管理流程中。例如,在Azure Verified Modules (AVM)的模块开发框架中,mapotf 被用作预提交(pre-commit)步骤的一部分,以在开发者提交代码前自动应用规定的配置转换。Azure 官方提供的 Terraform 模块脚手架(tfmod-scaffold)包含了一个预提交钩子脚本,该脚本调用 mapotf 来执行集中定义的转换规则。具体流程如下:

  • 当开发者准备提交AVM模块代码时,预提交脚本会运行 mapotf transform,并指向AVM维护的一组远程规则集(存储在 avm_mapotf/pre_commit 目录中)。这些规则涵盖了 AVM 对所有模块要求的一些统一改动,比如 Telemetry 遥测代码的植入、模块版本号/ Provider 版本的一致性调整等。通过将规则集中托管,AVM 团队可以在中央仓库更新这些转换逻辑,所有模块在下次执行预提交时都会自动获取并应用最新规则,确保治理策略的即时生效。

  • mapotf 在应用转换后,会调用 avmfix 进行进一步的处理,然后使用 mapotf clean-backup 清理备份文件。清理备份是因为在预提交场景下,转换后的代码会被直接提交到版本库中,不需要保留多余的备份文件。整个过程对开发者而言是透明的 —— 除非规则导致代码变更未被提交,否则预提交检查会提示错误要求重新提交修改后的文件。这一机制保证了每次提交的代码都已经过 mapotf 的规范化处理

在 Azure Verified Modules 的治理中,mapotf 发挥了核心作用

  • Telemetry 遥测植入:每个 AVM Terraform 模块都需要包含一个特殊的部署(如 main.telemetry.tf)用于标识该模块的部署情况。这一 GUID 标识的遥测信息可让微软统计模块的使用频率,但又不会收集具体资源内容,符合隐私要求。为了确保所有模块都正确包含 Telemetry 部署且保持一致,AVM 团队可以借助 mapotf 统一插入或更新这一段代码。当 Telemetry 机制需要调整时(例如更换 GUID 生成方式或新增字段),只需更新 mapotf 规则即可批量应用到所有模块,实现 “一处更改,处处生效” 的效果,而不必手工编辑每个模块。

  • Provider 统一升级:当 AzureRM 等 Provider 发布大版本更新时,AVM 会要求所有官方模块跟进升级,以利用新特性和保持支持。借助 mapotf,AVM 团队可以发布对应的转换规则来统一升级模块的配置。这些规则可以涵盖:修改 required_providers 中的版本约束、替换已弃用的资源类型或属性,以及调整模块内部逻辑以兼容新版本。例如,从 AzureRM 3.x 迁移到 4.x 需要替换若干资源名称和参数,mapotf 的升级规则会按照官方指南对模块代码做批量替换和删除。模块维护者只需运行预提交或专门的升级脚本,即可自动完成大部分改造工作,然后再手动校验少量无法自动处理的部分。这种集中升级的方式,大幅降低了分散维护的沟通成本和出错概率。

总的来说,mapotf 在 Azure Verified Modules 体系中提供了一种集中控制、分散执行的机制:治理者在中央制定规则,各模块在各自代码库中通过 mapotf 执行规则,实现了标准的下发和落实。这极大增强了平台团队对模块生态的掌控力,同时又保留了模块开发的灵活性。

1.6.14.1.4. 实际应用场景示例

结合一些示例,我们可以进一步理解 mapotf 的实际用途:

  • 忽略属性变化(ignore_changes)的定制:在 mapotf_demo/ignore_changes 示例中,展示了 mapotf 如何为 Terraform 资源动态添加 ignore_changes 设置以忽略特定属性的变动。
data "resource" "resource_group" {
    resource_type = "azurerm_resource_group"
}

transform "update_in_place" resource_group_ignore_changes {
  for_each = try(data.resource.resource_group.result.azurerm_resource_group, {})
  target_block_address = each.value.mptf.block_address
  asstring {
    lifecycle {
      ignore_changes = "[\ntags, ${trimprefix(try(each.value.lifecycle.0.ignore_changes, "[\n]"), "[")}"
    }
  }
}

该代码匹配到类型为 azurerm_resource_groupresource 块,将 tags 动态地添加到 ignore_changes 列表中。

这对应于前述场景:某些基础设施由外部策略自动修改了一些设置,如果不忽略,Terraform 每次 plan 都会试图纠正它。通过 mapotf,用户可以在不修改模块源码的情况下,按需为资源追加忽略这些属性的配置。例如 Azure AKS 模块的示例中,mapotf 成功将 microsoft_defender[0].log_analytics_workspace_id 加入了 AKS 集群资源的 ignore_changes 列表。这样一来,模块使用者能够方便地避免无意义的 Terraform 资源更新,从而兼容企业自定义策略带来的配置漂移。

data "resource" azapi_resource {
  resource_type = "azapi_resource"
}

locals {
  azapi_resource_blocks = data.resource.azapi_resource.result.azapi_resource
  azapi_resource_map    = { for _, block in local.azapi_resource_blocks : block.mptf.block_address => block }
  payload = jsonencode({
    avm = "true"
  })
  compact_payload = replace(replace(replace(replace(local.payload, " ", ""), "\n", ""), "\r", ""), "\t", "")
  create_headers = {
    for address, block in local.azapi_resource_map :
    address => try(replace(replace(replace(replace(block.create_headers, " ", ""), "\n", ""), "\r", ""), "\t", ""), "")
  }
  delete_headers = {
    for address, block in local.azapi_resource_map :
    address => try(replace(replace(replace(replace(block.delete_headers, " ", ""), "\n", ""), "\r", ""), "\t", ""), "")
  }
  read_headers = {
    for address, block in local.azapi_resource_map :
    address => try(replace(replace(replace(replace(block.read_headers, " ", ""), "\n", ""), "\r", ""), "\t", ""), "")
  }
  update_headers = {
    for address, block in local.azapi_resource_map :
    address => try(replace(replace(replace(replace(block.update_headers, " ", ""), "\n", ""), "\r", ""), "\t", ""), "")
  }
}

transform "update_in_place" headers {
  for_each = local.azapi_resource_map

  target_block_address = each.key
  asstring {
    create_headers = try(strcontains(local.create_headers[each.key], local.compact_payload), false) ? each.value.create_headers : try(each.value.create_headers == "" || each.value.create_headers == null, true) ? local.payload : "merge(${each.value.create_headers}, ${local.payload})"
    delete_headers = try(strcontains(local.delete_headers[each.key], local.compact_payload), false) ? each.value.delete_headers : try(each.value.delete_headers == "" || each.value.delete_headers == null, true) ? local.payload : "merge(${each.value.delete_headers}, ${local.payload})"
    read_headers   = try(strcontains(local.read_headers[each.key], local.compact_payload), false) ? each.value.read_headers : try(each.value.read_headers == "" || each.value.read_headers == null, true) ? local.payload : "merge(${each.value.read_headers}, ${local.payload})"
    update_headers = try(strcontains(local.update_headers[each.key], local.compact_payload), false) ? each.value.update_headers : try(each.value.update_headers == "" || each.value.update_headers == null, true) ? local.payload : "merge(${each.value.update_headers}, ${local.payload})"
  }
}

以上代码匹配到所有 AzAPI Provider 的资源块,动态地为其配置 create_headers / delete_headers / read_headers / update_headers 属性,通过跟踪这些属性,Azure 官方可以获知哪些发往 Azure API 的请求是来自于 AVM 模块的,分别是哪些模块,哪些版本。

假设组织要求所有官方模块增加某段标准配置(比如前述的 Telemetry 部署或统一的 tag 策略),mapotf 可以通过一次命令在各模块代码中插入相应片段,实现“一键批量更改”。这体现了 mapotf模块治理方面的价值:无论是十几个还是上百个模块,都能通过集中定义的规则进行统一改造,避免人工修改的耗时和潜在遗漏。

  • Provider重大升级改造mapotf_demo/terraform_provider_major_upgrade 示例则演示了 mapotf 辅助 Terraform Provider 版本升级的过程。当需要将模块从旧版本 Provider 迁移到新版本时,mapotf 配合类似 TerraformConfigWelder 的工具,依据升级指南对模块代码进行系统化的替换与调整。例如,当 AzureRM Provider 版本跃迁时,某些资源块的属性名字可能发生变化,亦或是旧有的参数需被删除。通过事先编写好的 mapotf 转换规则,可以自动执行这些替换/删除操作(运行 mapotf transform --mptf-dir git::https://github.com/lonegunmanb/TerraformConfigWelder.git//azurerm/v3_v4 --tf-dir .),然后开发者再做少量人工检查即可。实际案例证明,使用 mapotf 进行升级改造,可将繁琐重复的工作自动化,大幅降低升级所需时间。

在上面给出的 mapotf 的例子中,我们可发现,类似 greptmapotf 被刻意设计成高度兼容 Terraform 的语法。熟练的 Terraform 用户可以轻松地在短时间内掌握 mapotf 的语法,开始元编程。

综上,mapotf 作为 Terraform 的元编程工具,面向 Terraform 使用者、模块开发者和平台治理团队都提供了独特的价值。对普通使用者而言,它提供了一种扩展 Terraform 能力的途径,满足特殊需求(如忽略配置漂移)的临时调整。对于模块开发者,mapotf 则是一个增强模块灵活性的利器,可以将某些复杂或可选的逻辑放在模块之外,由用户按需应用,减轻模块本身的负担。最后,对于负责平台治理的 DevOps / SRE 团队,mapotf 更是一个集中管控的好帮手,让他们能够在不中断各模块自主演进的前提下,实现对全局的一致性要求(如遥测、升级、安全策略)的快速下发。

results matching ""

    No results matching ""