This guide is a self-contained introduction to managing your Jamf environment with Terraform using the Jamf Platform provider. You don't need to have read anything else first: we cover installation, a project skeleton and the resources you'll want to define. If you're brand new to Infrastructure as Code, our Jamf Pro provider guide is worth a read alongside this one, but it isn't a prerequisite.
The Jamf Platform API puts one authentication model and one regional gateway in front of the Jamf estate. This provider is built against it, so a single provider block and a single set of credentials cover Blueprints, Compliance Benchmarks, device groups, AI Governance policies, Jamf Security Cloud, organization single sign-on and the Jamf Pro surface.
A nod to where this started
This provider builds on a path charted by Deployment Theory and their terraform-provider-jamfpro. First released in early 2024, it grew into the most comprehensive community Terraform provider for Jamf and the one the community rallies behind. It proved the depth of demand for managing Jamf as code. This provider wouldn't exist in its current form without that groundwork. terraform-provider-jamfpro remains an independent, community-maintained project; we're grateful to its maintainers for the example they set.
What does it cover?
Resources are grouped by the Jamf product they target, each under its own namespace, behind one provider configuration.
Jamf Platform services (no resource prefix) — continuously deployed microservices:
- Compliance Benchmarks (
jamfplatform_cbengine_*) — benchmarks built on macOS Security Compliance Project (mSCP) baselines including CIS Level 1 and 2, NIST 800-53 (Low, Moderate, High) and DISA STIG. The template reference lists every baseline available. Enable or disable individual rules, customize Organization-Defined Values (ODV), and target benchmarks at device groups and specific major OS versions. - Blueprints (
jamfplatform_blueprints_blueprint) — device configuration covering software update enforcement and settings, passcode policies, Safari settings, disk management, background tasks, legacy payloads, custom DDM declarations and AI Governance delivery. Each blueprint targets one or more device groups and is deployed or undeployed as code. - Device Groups (
jamfplatform_device_group) — smart and static groups for computers and mobile devices, used to target benchmarks and blueprints. - Unified Inventory and Device Actions — data sources for querying devices, plus Terraform 1.14+ actions for erase, restart, shutdown and unmanage.
Jamf Pro (jamfplatform_pro_*) — the breadth of Jamf Pro: policies, scripts, configuration profiles for macOS and mobile, Smart and Static groups, computer and mobile prestages, extension attributes, packages, categories, buildings, departments, LDAP servers, printers, restricted software, patch management, PKI configurations and the many singleton settings screens, with their data sources, list resources and management actions.
Jamf Security Cloud (jamfplatform_security_cloud_*) — ZTNA access policy and gateways, Custom DNS zones and mappings, device groups, activation profiles and the UEM Connect integration.
Jamf AI Governance (jamfplatform_ai_governance_*) — the managed settings for an AI tool such as Claude Code or OpenAI Codex. You version and publish the policy here; a blueprint delivers it.
Jamf Account (jamfplatform_account_*) — organization-level single sign-on: claimed domains and the identity providers that sign users in for them.
The provider covers all five spaces above: Jamf Platform services, Jamf Pro, Jamf Security Cloud, Jamf AI Governance and Jamf Account. You get resources and data sources in each, plus list resources, actions and provider functions where the API supports them. Every resource supports full CRUD and terraform import. The jamfplatform_pro_* resources are built against the Jamf Pro API as of version 11.31.0; tenants below that get an advisory warning, and resources that depend on newer endpoints declare their own minimum and refuse to configure below it. Everything else targets continuously deployed services with no tenant version to consider. The provider documentation carries the current list.
Before you begin
You'll need Terraform (or OpenTofu) and a way to talk to your environment.
Install Terraform. On macOS the quickest route is Homebrew:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform -version # confirm it installed
The provider requires Terraform >= 1.13.0 (or OpenTofu >= 1.6.0). Two features, list resources and actions, need Terraform 1.14+.
A text editor. Any editor works; we like VS Code with the official HashiCorp Terraform extension for syntax highlighting and validation.
API credentials. Register an API integration in Jamf Account and note its client ID and secret. The Getting Started guide on the Jamf Developer portal walks through it. Two things to get right at registration:
- Scope. Register the integration against a platform environment, a group of tenants across product types. One environment-scoped integration covers the whole group, and it's the only scope on which the Blueprints, Compliance Benchmarks and AI Governance permissions can be selected. (Organization single sign-on is the exception:
jamfplatform_account_*needs an organization management integration, which belongs in its own aliased provider block or its own workspace.) - Permissions. Permissions are organized by capability and action, such as
device-groups:readorcompliance-benchmarks:create, and Jamf Account's picker presents each one as a named permission with a checkbox per action. Every resource, data source, list resource and action page in the provider documentation carries a Required Jamf permissions table written the way the picker reads: the section, the permission name and the boxes to tick. Use those tables and grant only what you need. Each action covers itself and nothing else, so an integration that reads a record before modifying it needs the read action as well as the update action.
Configuring the provider
Create a directory for your project and add a terraform block declaring the provider, plus a provider block:
terraform {
required_providers {
jamfplatform = {
source = "jamf/jamfplatform"
version = ">= 0.29.0"
}
}
}
provider "jamfplatform" {
base_url = var.jamfplatform_base_url
client_id = var.jamfplatform_client_id
client_secret = var.jamfplatform_client_secret
environment_id = var.jamfplatform_environment_id
}
We use a >= version constraint rather than a pessimistic ~> pin. The provider tracks the Jamf Platform services, which are deployed and updated remotely, so you want to pick up new provider releases as they ship rather than lock yourself to a single minor version. Pin more tightly only if you have a specific reason to.
base_url is the regional gateway root: https://us.api.jamfcloud.com, https://eu.api.jamfcloud.com or https://apac.api.jamfcloud.com. Give the host alone. The gateway serves the token endpoint and every API namespace at the root, so adding a path makes authentication fail. environment_id is the ID of the platform environment your integration targets.
You can also keep the provider block empty and supply everything through the environment:
export JAMFPLATFORM_BASE_URL="https://eu.api.jamfcloud.com"
export JAMFPLATFORM_CLIENT_ID="your-client-id"
export JAMFPLATFORM_CLIENT_SECRET="your-client-secret"
export JAMFPLATFORM_ENVIRONMENT_ID="your-environment-id"
Keep credentials out of Git. Put variable values in a terraform.tfvars file and add it to your .gitignore. Terraform records what it manages in a terraform.tfstate file. Keep one state file per environment.
Three optional attributes are worth setting from the start:
impact_alerts = truereports, duringterraform plan, how many computers or mobile devices each deployable or scopeable change reaches. It is the same signal Jamf Pro shows when you save an object, and it never blocks a plan. See the Impact alerts guide.min_request_interval_mspaces outbound requests. At100no plan exceeds ten requests per second however many operations Terraform runs in parallel. Raise it to ease the load on a busy server, at the cost of slower plans.custom_headersandauthorization_header_namecover the case where traffic reaches Jamf through a reverse proxy that authenticates callers itself. An ordinary forward proxy needs neither, justHTTPS_PROXYandNO_PROXY. See the reverse proxy guide.
Don't test this against a production environment. A misplaced
terraform destroyremoves real configuration from real devices, so work in a sandbox until you trust your project.
Defining your resources
Device groups come first, since benchmarks and blueprints target them. Jamf Pro objects follow.
Device groups
Before creating benchmarks or blueprints you'll want a device group to target them at. A smart computer group for devices running macOS 26 or later:
resource "jamfplatform_device_group" "macos_26_plus" {
name = "macOS 26+"
group_type = "smart"
device_type = "computer"
criteria = [
{
criteria = "Operating System Version"
operator = "greater than or equal"
value = "26.0"
}
]
}
Reference the group by ID from your benchmark and blueprint resources and Terraform works out the dependency graph, creating everything in the right order.
The group also exposes a computed jamf_pro_id holding its numeric Jamf Pro identifier, which is what the Jamf Pro scope blocks want:
scope = {
targets = {
computer_group_ids = [jamfplatform_device_group.macos_26_plus.jamf_pro_id]
}
}
One group definition serves both sides: the Platform UUID where a Platform service wants it, the Jamf Pro ID where a policy or profile does, and no identifier copied between consoles by hand.
Compliance benchmarks
Compliance benchmarks use the mSCP baselines built into the Compliance Benchmark Engine. First, use a data source to fetch the rules for the baseline you want. Then create a benchmark that references those rules and targets your device groups:
data "jamfplatform_cbengine_rules" "cis_lvl1" {
baseline_id = "cis_lvl1"
}
resource "jamfplatform_cbengine_benchmark" "cis_level_1" {
title = "CIS macOS Level 1"
description = "CIS Level 1 benchmark - Managed by Terraform"
source_baseline_id = "cis_lvl1"
rules = [
for r in data.jamfplatform_cbengine_rules.cis_lvl1.rules : {
id = r.id
enabled = r.enabled
}
]
target_device_groups = [jamfplatform_device_group.macos_26_plus.id]
enforcement_mode = "MONITOR"
}
This creates a CIS Level 1 benchmark in monitor mode, scoped to the smart group we defined earlier. target_device_groups takes a set of device group Platform UUIDs, so a single benchmark can point at several groups at once. You don't configure the benchmark's mSCP sources: a benchmark always spans the full source set of its baseline, so sources is computed and read-only.
Every rule from the baseline is included above. You can also name rules individually, disable the ones you don't want and set custom ODV values to match your organization's requirements:
rules = [
{
id = "system_settings_time_server_configure"
enabled = true
odv_value = "ntp.example.com"
},
{
id = "system_settings_critical_update_install_enforce"
enabled = true
}
]
Set enforcement_mode to "MONITOR_AND_ENFORCE" and the benchmark remediates settings that fall out of compliance.
Targeting specific OS versions
A benchmark applies to every operating system version its baseline supports unless you say otherwise, so you end up generating configuration for all of them. The optional selected_os_versions attribute narrows it to the major versions you care about, such as macOS 26 (Tahoe) alone. Valid values come from the available_os_versions attribute on the jamfplatform_cbengine_rules data source, so you can see what a baseline offers before choosing:
resource "jamfplatform_cbengine_benchmark" "cis_tahoe_only" {
title = "CIS macOS Level 1 - Tahoe only"
description = "CIS Level 1 scoped to macOS 26 - Managed by Terraform"
source_baseline_id = "cis_lvl1"
rules = [
for r in data.jamfplatform_cbengine_rules.cis_lvl1.rules : {
id = r.id
enabled = r.enabled
}
]
# Scope to a single major OS version. Omit this attribute to target
# every version the baseline supports.
selected_os_versions = [
{ os_type = "MAC_OS", os_version = 26 }, # 26 = Tahoe, 15 = Sequoia, 14 = Sonoma
]
target_device_groups = [jamfplatform_device_group.macos_26_plus.id]
enforcement_mode = "MONITOR"
}
Each entry is an { os_type, os_version } pair, where os_version is the major version integer.
Creating a benchmark is asynchronous: the platform accepts the request, deploys the artifacts to the MDM, and the provider polls the sync state until it reaches SYNCED or fails. Benchmarks also have no update endpoint, so every attribute replaces the resource when it changes: a configuration edit destroys and recreates the benchmark rather than updating it in place. Keep that in mind when changing benchmarks that are already deployed and reporting on devices.
Version control then holds the exact set of rules and ODV customizations you applied, changes go through pull requests, and a second environment gets the same benchmark without anyone rebuilding it by hand.
Blueprints
Blueprints compose configuration into a single unit deployed to one or more device groups. Every component the editor offers is listed in the Blueprints components reference. In Terraform a blueprint is authored as an ordered list of component blocks via the component_blocks attribute. Each block appears as a step in the Jamf Blueprints editor with its own name, its own optional activation condition and one or more components, and blocks are applied in the order you list them. Each component type maps to a Declarative Device Management (DDM) declaration configuration domain. A single-block blueprint that configures software update settings:
resource "jamfplatform_blueprints_blueprint" "software_update_settings" {
name = "Software Update Settings"
description = "Managed by Terraform"
deployed = true
device_groups = [jamfplatform_device_group.macos_26_plus.id]
component_blocks = [
{
name = "Software Update Settings"
software_update_settings = {
allow_standard_user_os_updates = true
automatic_download = "AlwaysOn"
automatic_install_os_updates = "AlwaysOn"
automatic_install_security_updates = "AlwaysOn"
deferral_major_period_days = 30
deferral_minor_period_days = 14
deferral_system_period_days = 3
notifications_enabled = true
rapid_security_response_enabled = true
rapid_security_response_rollback_enabled = false
recommended_cadence = "Newest"
}
},
]
}
Because component_blocks is an ordered list, a single blueprint can carry several steps. This one enforces a passcode policy, then a software update:
resource "jamfplatform_blueprints_blueprint" "baseline" {
name = "Device Baseline"
description = "Managed by Terraform"
deployed = true
device_groups = [jamfplatform_device_group.macos_26_plus.id]
component_blocks = [
{
name = "Passcode Policy"
passcode_policy = {
require_passcode = true
require_alphanumeric_passcode = true
minimum_length = 12
minimum_complex_characters = 1
maximum_failed_attempts = 10
maximum_inactivity_in_minutes = 5
maximum_passcode_age_in_days = 90
passcode_reuse_limit = 5
}
},
{
name = "Latest OS Software Updates"
software_update = {
ignore_major_versions = true
deployment_time = "02:00"
enforce_after_days = 7
}
},
]
}
Each blueprint targets a set of device groups via device_groups (a set of Platform UUIDs). Setting deployed to true deploys the blueprint, and redeploys it if it falls out of date; false undeploys it. Within a block you can add an activation_conditions expression to further restrict which scoped devices that step applies to, use the legacy_payloads component for traditional configuration profile payloads, custom_declarations for arbitrary DDM declarations the provider doesn't yet have a dedicated component for, or the ai_governance component to deliver a published AI policy version. See the AI Governance policies guide.
Jamf Pro resources, same provider
Jamf Pro objects live under the jamfplatform_pro_* namespace and behave like any other resource, with no second provider and nothing extra to configure:
resource "jamfplatform_pro_category" "productivity" {
name = "Productivity"
priority = 9
}
resource "jamfplatform_pro_script" "install_rosetta" {
name = "Install Rosetta"
category_id = jamfplatform_pro_category.productivity.id
priority = "BEFORE"
script_contents = file("${path.module}/support_files/scripts/install_rosetta.sh")
}
Configuration profiles are the one place where authoring in Terraform usually means pasting XML. Two provider functions remove that step. mobileconfig assembles a full profile from payload objects written in HCL, using Apple's own payload keys:
resource "jamfplatform_pro_macos_configuration_profile" "workstation" {
general = {
name = "Workstation Baseline"
payloads = provider::jamfplatform::mobileconfig({
display_name = "Workstation Baseline"
identifier = "com.example.workstation"
organization = "Example Org"
payloads = [
{
PayloadType = "com.apple.dock"
tilesize = 48
orientation = "left"
autohide = true
},
{
PayloadType = "com.apple.finder"
ShowExternalHardDrivesOnDesktop = true
ShowRemovableMediaOnDesktop = true
},
]
})
}
scope = {
targets = {
computer_group_ids = [jamfplatform_device_group.macos_26_plus.jamf_pro_id]
}
}
}
mcx_forced_payload is the shorthand for the common single-domain case, encoding an application's preferences as managed (forced) defaults:
payloads = provider::jamfplatform::mcx_forced_payload("com.example.app", {
RotateWithinHours = 24
AdminBase = "https://admin.example.com"
Browsers = ["edge", "chrome", "firefox"]
})
Both functions keep values in their natural types: whole numbers become integers, fractional numbers reals, booleans and strings map directly, lists become arrays and nested objects become dictionaries.
Go further: the rest of the platform
Blueprints, benchmarks and Jamf Pro are where most projects start. Three more families sit behind the same provider block, each with its own guide in the provider documentation.
Jamf AI Governance manages the settings Jamf delivers to Claude Code, Claude Desktop and OpenAI Codex. You write the policy as JSON against a schema the platform serves, and applying it publishes a version:
data "jamfplatform_ai_governance_tool" "claude_code" {
id = "com.anthropic.claudecode"
}
resource "jamfplatform_ai_governance_policy" "engineering" {
name = "Claude Code - Engineering"
description = "Managed Claude Code settings for the engineering fleet."
tool_id = data.jamfplatform_ai_governance_tool.claude_code.id
schema_version = data.jamfplatform_ai_governance_tool.claude_code.current_schema_version
settings_json = jsonencode({
model = "sonnet"
availableModels = ["sonnet", "haiku"]
enforceAvailableModels = true
permissions = {
allow = ["Bash(git *)", "Read"]
deny = ["Read(./.env)", "Read(./secrets/**)"]
}
})
}
A blueprint delivers it: add an ai_governance component block naming the policy and the version to pin. Reading current_schema_version from the tool data source keeps the policy on whatever schema the platform serves today; pin schema_version to a literal instead when you want the settings to stay on one schema until you move them yourself. The AI Governance Configuration Guide covers the capability itself, including the AI Visibility dashboard, which is read-only and has no Terraform constructs.
Jamf Security Cloud reaches the radar.jamf.com portal: ZTNA access policy and gateways, Custom DNS zones, hostname mappings and search domains, device groups, activation profiles and the UEM Connect integration with Jamf Pro. Device groups are how access is assigned, and they hold nothing but a name:
resource "jamfplatform_security_cloud_device_group" "executives" {
name = "Executives"
}
Membership comes from whatever references the group, so a ZTNA app's assignments and a UEM Connect mapping decide who is in it. Security Cloud is a separate entitlement, and these resources return a clear error when a tenant does not hold it. Read the provider guide for the order to build in before your first apply.
Jamf Account single sign-on manages the domains your organization has claimed and the identity providers that sign users in for them. It needs an organization management integration, configured by leaving environment_id unset so the gateway resolves your organization from the access token, which puts this family in its own aliased provider block or its own workspace. Claiming a domain also takes two applies: the first returns the TXT record to publish in your DNS, and the jamfplatform_account_sso_domain_verify action asks Jamf Account to check it once the record is live.
Applying your configuration
The core workflow is the same regardless of which resources you define:
terraform init # Initialize and download the provider
terraform plan # Review what will be created, changed or destroyed
terraform apply # Apply the changes (you'll be asked to confirm)
terraform destroy tears down what's in state. Each subsequent run consults your state file, works out what's changed and executes only the necessary actions.
With impact_alerts enabled, a plan that changes scope tells you what it reaches before you apply it:
Warning: Impact alert — this configuration profile affects 47 of 312 computers (15%)
with jamfplatform_pro_macos_configuration_profile.workstation,
on main.tf line 12, in resource "jamfplatform_pro_macos_configuration_profile" "workstation":
12: scope = {
Counted from 2 groups: All Managed Clients (44), Lab Macs (5).
Bringing an existing environment under management
If you already have resources configured, you don't need to start from scratch. Every resource supports terraform import, and on Terraform 1.14+ the provider supports list resources, which query existing infrastructure from Terraform and generate configuration for it.
Create a query file (e.g. discover.tfquery.hcl):
list "jamfplatform_blueprints_blueprint" "software_update" {
provider = jamfplatform
include_resource = true
config {
search = "software update"
}
}
list "jamfplatform_device_group" "smart_computer_groups" {
provider = jamfplatform
include_resource = true
config {
filter = [
{
selector = "deviceType"
argument = "COMPUTER"
},
{
join_with = "and"
selector = "groupType"
argument = "SMART"
},
]
}
}
Then run:
terraform query -generate-config-out=generated.tf
Terraform queries your environment, prints each matching object with its ID and name, and writes resource blocks and import blocks into generated.tf. List resources exist for the Platform services and across the jamfplatform_pro_* surface, so this scales well beyond blueprints and benchmarks.
Doing this by hand for an entire environment is a lot of work, so we built a tool that automates the loop: discovery, generation, cross-resource references, secret scanning and per-type file splitting. See Adopting Terraform for Jamf with jamformer.
Using alongside other Jamf providers
This provider can manage most of a Jamf environment on its own. You can still combine it with others where it makes sense:
- Jamf Protect Provider — Jamf Protect resources (endpoint security plans, threat prevention, telemetry and more) through the Jamf Protect GraphQL API.
- Jamf School Provider — Jamf School resources (apps, classes, device groups, users, user groups and iBeacons) through the Jamf School REST API. It authenticates separately, with a network ID and API key from your Jamf School tenant, and is in early development.
Getting involved
The provider is open-source under the MPL license and published on both the HashiCorp and OpenTofu registries. It's built on HashiCorp's Terraform Plugin Framework (Protocol v6) and uses our open-source jamfplatform-go-sdk, which you can import into your own Go projects for scripting and automation.
Bug reports, feature requests and pull requests are welcome. The provider grows alongside the Jamf Platform API as capabilities land. For questions and discussion, join #terraform-provider-jamfplatform on the MacAdmins Slack.
References
- GitHub Repository
- Terraform Registry
- Provider guides: AI Governance, Jamf Security Cloud, Jamf Account SSO, impact alerts, reverse proxies
- Jamf Platform API Reference
- Jamf Pro Blueprints Configuration Guide
- Compliance Benchmarks Configuration Guide
- AI Governance Configuration Guide
- Jamf Security Cloud Portal Setup Guide
- Single sign-on with Jamf Account
- Jamf Pro permissions map
- Adopting Terraform for Jamf with jamformer
- Managing Jamf Protect with Terraform: The Jamf Protect Provider
- Jamf School Provider
- Terraform Documentation