Jamf Concepts

指南

使用 jamformer 采用 Terraform 管理 Jamf

~5 min read

在我们之前的文章中,我们介绍了使用 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 importterraform querylist 块如何将现有资源纳入管理。

但是,如果您已经通过控制台管理 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 querylist 块(与我们在 Protect 文章中涵盖的方法相同)通过提供程序直接发现资源。

接下来,它生成导入块。对于每个发现的资源,jamformer 编写一个 Terraform import 块,标签派生自资源名称。空格变为下划线,特殊字符被移除,如果两个资源最终得到相同的标签,会添加数字后缀以保持唯一性。它还生成为正确的提供程序配置的 provider.tfvariables.tfterraform.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` 跳过此步骤并单独处理包。

密钥扫描在生成后自动运行。如果您想自己处理密钥