在我们之前的文章中,我们介绍了使用 Terraform 和 Jamf Pro 进行基础设施即代码的基础知识,然后演示了 Jamf Protect 和 Jamf Platform 的专用 Terraform 提供程序。如果您还没有读过,我们建议您从那里开始 — 它们涵盖了 Terraform 基础知识、项目结构、状态管理以及如何从头开始用代码定义 Jamf 资源。
这一次,我们要解决一个我们经常听到的问题:"这听起来都很不错,但我的 Jamf 环境中已经有数百个策略、配置文件、脚本和组。我真的必须手动重新编写所有这些吗?"
简短的答案是否定的。我们构建了一个名为 jamformer 的工具来帮助解决这个问题 — 但在深入之前,重要的是要坦诚说明它是什么以及不是什么。
jamformer 是什么?
jamformer 是一个赋能和加速工具,用于团队采用 Jamf 和 Terraform。它连接到您现有的 Jamf 实例,发现可以找到的所有内容,并生成一个 Terraform 项目作为起点 — 一个现实的脚手架,您可以从中学习和完善。可以把它看作是连接您现在所在位置(通过控制台管理的完全配置的 Jamf 环境)和您想要达到的位置(同一环境通过代码管理)的桥梁 — 但这座桥并不是传送带。您仍然需要走过它。
它故意不是:
- 生产代码生成器。它生成的 HCL 是初稿。在用它来管理真实基础设施之前,预期要审查每个文件、重构为您自己的模块结构、应用命名约定、加强密钥处理,并处理提供程序漂移的怪癖。
- Terraform 或 Jamf 提供程序学习的替代品。它压平学习曲线;它不能消除学习曲线。
- 一键式迁移按钮。大型或不寻常的 Jamf 环境总是需要人工判断。
它确实擅长的是:
- 查看您的 Jamf 实例中实际存在的内容,用 Terraform 提供程序自己的资源模型表示 — 属性、交叉引用、生命周期怪癖等等。
- 引导概念验证、演示、研讨会、内部培训和迁移规划会议。
- 为工程师提供现实的脚手架进行重构为基础设施即代码,而不是盯着空白编辑器。
它也是只读的。jamformer 不会修改、创建或删除 Jamf 实例中的任何内容。它唯一写入的是本地机器上的一组 Terraform 文件。
请记住这个框架用于本文的其余部分。当我们演示工作流并显示示例输出时,您看到的是人类工程师要完善的原始材料 — 而不是最终产品。
为什么不直接使用 terraform import?
您完全可以,如果您读过 Protect 的文章,您已经看到 terraform import 和 terraform query 与 list 块如何将现有资源纳入管理。
但是,如果您已经通过控制台管理 Jamf 环境一段时间,您会知道里面有很多东西。手动导入所有内容意味着要编目每种资源类型、编写数百个带有正确 Jamf ID 的导入块、找出哪个策略引用哪个脚本和哪个组以及哪个类别、将嵌入式脚本和配置文件有效负载提取到独立文件,以及处理关于默认值的提供程序怪癖。
这是很多工作。我们想让这变得更容易。
它支持什么?
jamformer 适用于 Jamf 产品系列中的四个 Terraform 提供程序:
| 提供程序 | 发现方式 |
|---|---|
| Jamf Pro(默认) | Jamf Pro API |
| Jamf Protect | terraform query |
| Jamf Platform | terraform query |
| Jamf Security Cloud (JSC) | Terraform 数据源 |
仅对于 Jamf Pro,这涵盖策略、脚本、配置文件、Smart Groups、Static Groups、计算机预阶段、移动设备配置文件、扩展属性、包、类别、建筑物、部门等等 — 加上 Client Check-In、Cloud Distribution Point 和 Self Service 配置等设置。权威的、始终最新的列表是 jamformer -list-resources(可选择用 -provider 过滤)。
运行它时会发生什么?
当您将 jamformer 指向您的 Jamf 实例时,它会经历几个阶段。我们将逐一介绍每个阶段的作用,以便您知道会发生什么。
首先,它发现您的资源。对于 Jamf Pro,它认证到您的实例(OAuth2 或基本认证)并查询 API 以获得每种支持的资源类型。它按依赖顺序遍历您的环境 — 首先是站点和类别,然后是脚本和包,然后是策略和配置文件 — 这样它可以映射资源之间的关系。对于 Jamf Protect 和 Jamf Platform,它使用 terraform query 与 list 块(与我们在 Protect 文章中涵盖的方法相同)通过提供程序直接发现资源。
接下来,它生成导入块。对于每个发现的资源,jamformer 编写一个 Terraform import 块,标签派生自资源名称。空格变为下划线,特殊字符被移除,如果两个资源最终得到相同的标签,会添加数字后缀以保持唯一性。它还生成为正确的提供程序配置的 provider.tf、variables.tf 和 terraform.tfvars。
这些导入块的样子可能是这样的:
import {
to = jamfpro_policy.software_update_enforce
id = "142"
}
import {
to = jamfpro_script.install_rosetta
id = "87"
}
import {
to = jamfpro_category.productivity
id = "5"
}
然后它**请求 Terraform 生成 HCL**。jamformer 运行 `terraform plan -generate-config-out`(或对于 Protect 和 Platform 运行 `terraform query -generate-config-out`),它让提供程序为每个导入的资源生成 HCL。由于提供程序本身在这里做了繁重工作,生成的配置总是与提供程序期望的相匹配。
如果特定资源导入失败 — 有时会发生,有时提供程序无法干净地读取特定资源 — jamformer 会自动移除失败的导入块并重试。您会看到关于哪些资源被跳过的消息,以便您稍后可以处理它们。
最后,它**清理输出**。来自 Terraform 的原始输出是一个大文件,到处都是字面值。jamformer 将其转换为您实际想要使用的东西:
- jamformer 将字面 Jamf ID 重写为 Terraform 引用。引用类别 ID `5` 的策略变为 `jamfpro_category.productivity.id`,所以 Terraform 理解资源之间的关系 — 就像您手工编写的一样。
- 嵌入式脚本转到 `support_files/scripts/` 下的单个文件,配置文件有效负载转到 `support_files/macos_configuration_profiles/`,扩展属性脚本转到 `support_files/extension_attributes/`,移动设备应用配置转到 `support_files/app_configurations/`。jamformer 用 `file()` 引用更新 HCL,所以您的脚本和配置文件是您可以差异、审查和独立编辑的正确文件。设备注册和批量购买令牌使用 `file(var.xxx)` 路径变量 — 您将从 Apple Business Manager 下载的令牌文件放在您在变量中指定的路径处。
- jamformer 将图标资源解析为 CDN URL(图标不在本地下载)并添加 `lifecycle { ignore_changes }` 块以防止首次应用时出现不必要的销毁/创建周期。
- 作为 `null` 返回的可选属性被剥离,已知会导致问题的值(如未分类资源的 `category_id = -1`)被移除,单个大文件被拆分为按资源类型划分的文件 — `policies.tf`、`scripts.tf`、`categories.tf` 等等。
- 验证通过运行 `terraform validate` 来检测条件无效的属性 — 例如,仅对特定 CDN 类型有效的属性 — 并自动移除它们,以便您不必手动追踪模式错误。
后处理后,jamformer **扫描输出中的密钥**。Jamf 环境经常包含嵌入在配置文件、脚本和资源属性中的凭据 — 诸如 LDAP 绑定密码、SMTP 凭据、Wi-Fi 共享密钥和 API 令牌等内容。jamformer 使用带有 Jamf 特定规则的 [gitleaks](https://github.com/gitleaks/gitleaks) 在您意外将它们提交到 Git 之前找到这些。
当 jamformer 找到密钥时,它显示按资源和属性分组的报告。在交互模式下,您可以选择自动补救所有发现、逐个浏览或完全跳过补救。补救将密钥移动到敏感的 Terraform 变量 — 对于 `.tf` 文件,密钥值被替换为 `var.` 引用;对于脚本和配置文件等支持文件,使用 `templatefile()` 将文件转换为 `.tpl` 模板。如果您想自己处理密钥,可以用 `-skip-secret-scan` 跳过此步骤。
## 开始使用
如果您按照介绍文章进行操作,您应该已经安装了 Terraform 和文本编辑器。这里的步骤很直接。
### 1. 安装 jamformer
Homebrew:
```bash
brew install Jamf-Concepts/tap/jamformer
预构建二进制文件和 `.pkg` 安装程序可从[发布页面](https://github.com/Jamf-Concepts/jamformer/releases)获得。
您不需要单独安装 Terraform — jamformer 会自动将其下载到临时目录,因此不会与您机器上的任何现有 Terraform 安装冲突。
### 2. 对您的实例运行它
最简单的入门方式是只需不带参数运行 jamformer:
```bash
jamformer
它会以交互方式引导您完成所有操作 — 您想使用哪个提供程序、您的实例 URL 和凭据。您将密码和客户端密钥输入为隐藏输入,这样它们就不会显示在您的终端历史记录中。
速记 URL 在这里也适用 — 您可以只输入 `yourinstance`,jamformer 会将其扩展为 `yourinstance.jamfcloud.com`。对于 Protect,`your-tenant` 扩展为 `your-tenant.protect.jamfcloud.com`。
一旦您熟悉了该工具或想要脚本化它,您可以通过环境变量设置凭据:
```bash
# Jamf Pro with OAuth2
export JAMF_CLIENT_ID='your-client-id'
export JAMF_CLIENT_SECRET='your-client-secret'
jamformer -url https://yourinstance.jamfcloud.com
# Jamf Protect
export JAMF_CLIENT_ID='your-client-id'
export JAMF_CLIENT_SECRET='your-client-secret'
jamformer -provider jamfprotect -url https://your-tenant.protect.jamfcloud.com
jamformer 仅从环境变量或交互提示读取凭据 — 从不使用 CLI 标志 — 以避免在 shell 历史记录和进程列表中泄露密钥。您可以将 URL 作为标志传递(`-url`)或通过 `JAMF_URL`。
jamformer 不会将凭据写入 `terraform.tfvars` 并生成 `.gitignore`,该文件排除 tfvars 文件、状态文件和 `.terraform/` 目录。
### 3. 一次性导入所有内容是可选的
如果您的环境很大并且您想从小处开始,您可以定位特定的资源类型:
```bash
# 仅从策略、脚本和类别开始
./jamformer -include-resources "policies scripts categories"
# 除了包和图标的所有内容
./jamformer -exclude-resources "packages icons"
# 查看所有可用的过滤名称
./jamformer -list-resources
当不包括依赖类型时(比如您导入策略但不导入类别),对这些类型的引用保持为字面 ID 而不是 Terraform 表达式。当您变得舒适时,您总是可以稍后使用更多类型重新运行。
## 那么,输出实际上是什么样子的?
jamformer 完成后,您将有一个看起来像这样的目录:
generated/
.gitignore
provider.tf
variables.tf
terraform.tfvars
policies.tf
policies_import.tf
scripts.tf
scripts_import.tf
categories.tf
categories_import.tf
...
support_files/
scripts/
install_rosetta.sh
set_wallpaper.sh
extension_attributes/
battery_health.sh
macos_configuration_profiles/
wifi_corporate.mobileconfig
filevault_enforce.mobileconfig
device_enrollment_tokens/ (空 - 在此放置您的 .p7m 文件)
volume_purchasing_tokens/ (空 - 在此放置您的 .vpptoken 文件)
app_configurations/
managed_browser.xml
每种资源类型都有其自己的 `.tf` 文件和相应的 `_import.tf` 文件。`support_files/` 目录包含通过 `file()` 表达式引用的提取的脚本、配置文件和应用配置。jamformer 创建令牌目录(`device_enrollment_tokens/` 和 `volume_purchasing_tokens/`)作为放置 Apple Business Manager 令牌文件的推荐位置 — 这些使用 `file(var.xxx)` 路径变量,因为 API 不返回令牌数据。
## 紧凑模式
默认情况下,jamformer 为每个对象生成一个资源块 — 一个扁平、明确的布局,易于阅读和审查。但是如果您的环境有很多类似的资源(类别、建筑物、部门),输出可能会感到重复。
`-compact` 标志将合格的资源类型合并为 `for_each` + `locals` 模式,生成更接近您手工编写的输出:
```bash
jamformer -compact
没有紧凑模式,您会得到单个块:
```hcl
resource "jamfpro_category" "productivity" {
name = "Productivity"
}
resource "jamfpro_category" "security" {
name = "Security"
}
启用紧凑模式后,这些变为:
```hcl
locals {
categories = {
productivity = { name = "Productivity" }
security = { name = "Security" }
}
}
resource "jamfpro_category" "all" {
for_each = local.categories
name = each.value.name
}
jamformer 自动重写交叉资源引用以使用新的寻址 — `jamfpro_category.all["productivity"].id` 而不是 `jamfpro_category.productivity.id` — 所以一切都保持连接。它以相同方式更新导入块。
jamformer 在运行时动态确定资格。当文件包含两个或更多资源块,这些块都共享相同的属性集,并且没有嵌套块(除了 `lifecycle`)时,资源类型符合条件。在所有实例中都相同的属性成为资源块上的字面值;只有变化的值才进入本地映射。这在所有四个提供程序上工作。
如果您想要更精细的控制,可以定位特定类型:
```bash
# 仅紧凑类别和建筑物
jamformer -compact -compact-include "categories buildings"
# 紧凑所有合格的,除了策略
jamformer -compact -compact-exclude "policies"
## 多环境支持
> **⚠️ 实验性和高级**。此功能针对已经熟悉 Terraform 模块、长期分支工作流和 Jamf 提供程序资源模型的人员。输出是*更多*脚手架级别,比单环境模式 — 在任何内容可用之前,预期编辑生成的模块、每个环境根和变量提取。将其视为研究/赋能功能,而不是受支持的生产工作流。如果您是 Terraform 的新手或仍在让您的第一个单实例项目工作,从那里开始,稍后再回到这里。
如果您管理多个 Jamf Pro 环境 — 暂存和生产,或开发和产品 — 您可能已经考虑过如何保持它们同步。jamformer 可以生成围绕共享模块组织的 Terraform 项目,带有按环境根目录,设计用于 Git 分支工作流,其中每个长期分支代表一个环境。
```bash
export JAMF_URL_STAGING=https://staging.jamfcloud.com
export JAMF_CLIENT_ID_STAGING=xxx
export JAMF_CLIENT_SECRET_STAGING=xxx
export JAMF_URL_PROD=https://prod.jamfcloud.com
export JAMF_CLIENT_ID_PROD=xxx
export JAMF_CLIENT_SECRET_PROD=xxx
jamformer -multi-env "staging prod"
jamformer 独立对每个环境进行发现,按名称匹配资源,并生成如下所示的输出:
generated/
modules/jamf/ # 共享资源定义
policies.tf # 存在于所有环境中的资源
scripts.tf
categories.tf
policies_staging_only.tf # 仅在暂存中的资源
variables.tf # 模块输入变量
providers.tf
support_files/ # 在环境中相同的文件
scripts/
macos_configuration_profiles/
environments/
staging/
main.tf # 提供程序配置 + 模块调用
backend.tf # 您的状态后端的占位符
variables.tf
terraform.tfvars # 环境特定值
imports.tf # 带有 module.jamf. 前缀的导入块
support_files/ # 与其他环境不同的文件
prod/
main.tf
backend.tf
variables.tf
terraform.tfvars
imports.tf
support_files/
`modules/jamf/` 中的共享模块包含所有环境中通用的资源定义。在属性值在环境中不同的地方 — 具有不同 `category_id` 的策略或具有不同 `description` 的配置文件 — jamformer 将这些值提取到模块变量,并在 `terraform.tfvars` 中按环境设置。交叉资源引用保持为正确的 Terraform 表达式(例如 `jamfpro_category.productivity.id`),所以资源之间的关系保持完整。
这在您有意保持环境同步时效果最好 — 相同的策略、脚本、配置文件和组部署在实例中。它们匹配越多,输出就越干净。也就是说,jamformer 处理常见的真实世界差异:
- **相同资源,不同设置**(例如,暂存与产品中具有不同类别或频率的策略)— 不同的属性变为模块变量,每个环境的值在其自己的 `terraform.tfvars` 中。jamformer 按字母顺序排序变量并按资源类型对其进行分组以提高可读性。
- **在某些环境中存在但在其他环境中不存在的资源**(例如,仅在暂存中的测试策略)— jamformer 将这些分离为模块中的明确标记文件,如 `policies_staging_only.tf`。在另一个环境的分支上,您只需删除这些文件。
- **在实例中不同的支持文件**(例如,内容在实例中略有不同的脚本)— 在所有环境中相同的文件位于共享模块的 `support_files/` 目录中。当文件不同时,jamformer 将它们放在每个环境自己的 `support_files/` 目录中,并将它们作为变量传递给模块。
每个环境目录都有自己的 `backend.tf` 占位符、自己的状态和带有 `module.jamf.` 前缀的导入块。这意味着您可以独立地按环境运行 `terraform plan` 和 `terraform apply`,并且可以并行运行,而一个环境的状态干扰另一个环境的风险为零。
jamformer 使用列出的第一个环境作为共享资源定义的真实来源;使用 `-source-env` 覆盖此。它按名称匹配资源,所以暂存中名为"Software Update - Enforce"的策略将匹配产品中具有相同名称的策略,无论其 Jamf ID 如何。
预期的工作流是将输出提交到存储库,为每个环境创建一个长期分支,为每个分支的 `backend.tf` 配置相应的状态后端,以及设置 CI 以在代码登陆该分支时在相应的 `environments/<env>/` 目录中运行 `terraform apply`。更改从功能分支流入第一个环境,然后通过拉取请求在分支之间推进。
您的环境差异越大,输出将包含越多变量和环境特定文件。如果您的实例相差很大,您可能最好为每个实例单独运行 jamformer 并维护独立的 Terraform 项目。
## 使用生成的输出
我们在最前面说过,我们在这里重新说一遍,因为这是最重要的部分:**生成的配置不是生产就绪的 Terraform。** jamformer 是一个赋能和加速工具。在管理您的 Jamf 环境作为代码的旅程中,它为您提供了一个现实的起点 — 而不是立即到达那里的捷径。计划在任何驱动真实基础设施变化之前花费真实时间审查、细化和理解它生成的内容。
输出故意是扁平的 — 每个对象一个资源块,每种类型一个文件,没有模块。实际上,您可能想要将其重构为模块化项目结构,利用 HCL 功能,如 `for_each` 和 `locals` 来减少重复,将实例 URL 和通用设置等内容转换为环境的变量,并以对您的团队有意义的方式组织资源。jamformer 为您提供做这件事的原始材料;如何塑造它取决于您。这个塑造工作是大多数 Terraform 学习实际发生的地方 — 这就是 jamformer 以现有形式存在的全部原因。
考虑到这一点,这是我们推荐的工作流。
您在介绍中学到的相同命令适用于这里:
```bash
cd generated
terraform plan # 审查导入计划,检查提供程序错误
**预期您首次计划时会有差异。** 看到 Terraform 生成的内容与 Jamf 实例中实际情况之间的差异是正常的。某些属性可能显示与您在控制台中看到的不匹配的值,某些可选字段可能具有提供程序填充的默认值,与活配置不同,某些资源可能由于写入专用字段而有验证错误,提供程序无法读取回来。这些是提供程序级别的怪癖,不是 jamformer 中的错误,它们需要人工关注。
仔细阅读计划输出,修复任何错误并调整属性,直到您对 Terraform 报告的内容感到满意。这个审查过程是您将了解提供程序如何表示您的资源最多的地方 — 这是在将控制权移交给 Terraform 之前的重要步骤。
一旦计划看起来干净:
```bash
terraform apply # 将资源导入到状态中(您会被要求确认)
在这里运行 `terraform apply` 会将您现有的资源导入到 Terraform 状态中。它**不**改变 Jamf 中的任何内容 — 它只告诉 Terraform"这些资源已经存在,这是它们的样子"。
成功应用后,导入块已完成其工作。您可以移除它们:
```bash
rm *_import.tf
然后再次运行 `terraform plan`。理想情况下,您看不到任何更改。如果出现差异,这些是提供程序的默认值与 Jamf 中实际情况不完全匹配的情况 — 调整生成的 HCL 直到您获得干净的计划。
此时,您拥有一个代表您的 Jamf 环境的 Terraform 项目。将其提交到 Git,您已准备好开始真实工作:细化配置、组织它以适应您团队的工作流,并逐步转向通过拉取请求、代码审查和 `terraform apply` 进行更改,而不是控制台。
## 要记住的事情
默认情况下,jamformer 从 Cloud Distribution Point 下载包。对于拥有大量包的大型环境,这可能需要一段时间。使用 `-skip-package-downloads` 跳过此步骤并单独处理包。
密钥扫描在生成后自动运行。如果您想自己处理密钥