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'll cover installation, a project skeleton and the resources you'll most likely 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 APIs power a generation of Jamf microservices — blueprints, Compliance Benchmarks, device groups and more — that sit alongside Jamf Pro and Jamf Protect. We've built a dedicated Terraform provider for them, and as of recent releases that same provider now also federates the full Jamf Pro surface, so a single provider and a single set of credentials can describe almost your entire Jamf environment as code.
Public beta. The Jamf Platform API is currently in public beta. Provider stability, functionality and schemas are subject to change without notice.
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 and set the bar for what a Jamf provider could be — 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?
The provider groups resources by the Jamf product they target, all behind one provider configuration:
Native Jamf Platform Services — continuously deployed microservices with no tenant version requirement:
- Compliance Benchmark Engine — create and manage benchmarks against macOS Security Compliance Project (mSCP) baselines including CIS Level 1 and 2, NIST 800-53 (Low, Moderate, High) and DISA STIG. Enable or disable individual rules, customize Organization-Defined Values (ODV) and target benchmarks at specific device groups and operating system versions.
- Blueprints — define device configuration covering software update enforcement and settings, passcode policies, Safari settings, disk management, legacy payloads, custom DDM declarations and more. Each blueprint targets one or more device groups and can be deployed or undeployed as code.
- Device Groups — smart and static groups for computers and mobile devices, used to target benchmarks and blueprints.
- Unified Inventory and Device Actions — read-only data sources for querying devices, plus Terraform 1.14+ actions for erase, restart, shutdown and unmanage.
The Jamf Pro surface — under the jamfplatform_pro_* namespace, the provider now covers the breadth of Jamf Pro: policies, scripts, configuration profiles (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 — along with their data sources, list resources and management actions.
All told, the provider ships well over 90 resources, 120+ data sources, 60+ list resources and 20+ actions across both namespaces, and every resource supports full CRUD and terraform import. The jamfplatform_pro_* resources are built against the Jamf Pro API as of version 11.29.0; tenants below that emit an advisory warning, and resources that depend on newer endpoints declare their own minimum and fail clearly on unsupported tenants. For the authoritative, always-current list, see the provider documentation.
Before you begin
You'll need Terraform (or OpenTofu) and a way to talk to your tenant.
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). A couple of features — list resources and device 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. Head to Jamf Account and create an API client with the permissions you need for the resources you want to manage. The Getting Started guide on the Jamf Developer portal walks through this. The same OAuth client and tenant ID work across both the native Platform and jamfplatform_pro_* resources.
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-Concepts/jamfplatform"
version = ">= 0.25.0"
}
}
}
provider "jamfplatform" {
base_url = var.jamfplatform_base_url
client_id = var.jamfplatform_client_id
client_secret = var.jamfplatform_client_secret
tenant_id = var.jamfplatform_tenant_id
}
We use a >= version constraint rather than a pessimistic ~> pin on purpose. The provider tracks the Jamf Platform microservices, which are continuously 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.
Use the API gateway for your region as the base_url: https://us.apigw.jamf.com, https://eu.apigw.jamf.com or https://apac.apigw.jamf.com. You can also keep the provider block clean with environment variables:
export JAMFPLATFORM_BASE_URL="https://us.apigw.jamf.com"
export JAMFPLATFORM_CLIENT_ID="your-client-id"
export JAMFPLATFORM_CLIENT_SECRET="your-client-secret"
export JAMFPLATFORM_TENANT_ID="your-tenant-id"
As always, 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 per environment so they never cross.
Don't test this against a production instance. Terraform is a powerful tool; use a sandbox or beta tenant while you find your feet.
Defining your resources
Let's walk through some high-impact examples.
Device groups
Before creating benchmarks or blueprints you'll want a device group to target them at. Here's 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"
}
]
}
This group can then be referenced by ID in your benchmark and blueprint resources, so Terraform understands the dependency graph and creates everything in the right order.
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 you can point a single benchmark at several groups at once. Note that you no longer configure the benchmark's mSCP sources: a benchmark always spans the full source set of its baseline, so sources is now computed and read-only — the provider fills it in for you.
Every rule from the baseline is included, and you can selectively disable individual rules or 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
}
]
Setting enforcement_mode to "MONITOR_AND_ENFORCE" will remediate non-compliant settings automatically.
Targeting specific OS versions
By default a benchmark applies to every operating system version its baseline supports, which means you'd otherwise be generating configuration for all of them. The optional selected_os_versions attribute lets you scope a benchmark to just the major OS versions you care about — for example, only macOS 26 (Tahoe). The valid values come from the available_os_versions attribute on the jamfplatform_cbengine_rules data source, so you can inspect 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 entirely 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. Supply a subset to narrow the benchmark, or omit selected_os_versions altogether to keep the default of all available versions.
It's worth noting that the Compliance Benchmark Engine API does not currently offer a PUT or PATCH method for benchmarks. This means any change to a benchmark resource in your configuration results in a destroy-and-recreate (force replacement) rather than an in-place update. Keep this in mind when changing benchmarks that are already deployed and actively reporting on devices.
The real power here is that you can version-control the exact set of rules and customizations you've applied, review changes through pull requests and replicate the same benchmark across multiple tenants with zero manual effort.
Blueprints
Blueprints compose configuration into a single unit that gets deployed to one or more device groups. As of provider v0.25.0, 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. Here's 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. Here's a two-step blueprint that enforces a passcode policy and then a software update — each block is a distinct step, and a block can hold more than one component:
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 also add an optional activation_conditions expression to further restrict which scoped devices that step applies to, use the software_update component to enforce a specific OS version by a date, the legacy_payloads component for traditional configuration profile payloads, or custom_declarations for arbitrary DDM declarations the provider doesn't yet have a dedicated component for.
Migrating from the flat component syntax. Before v0.25.0, components were set as top-level attributes directly on the resource (e.g.
software_update_settings = { ... }with no surroundingcomponent_blocks). That flat form still works but is now deprecated and collapses into a single unnamed step, and it can't be combined withcomponent_blocksin the same resource. It's scheduled for removal on 2026-10-22 — move existing blueprints intocomponent_blocksby wrapping each component in a block as shown above.
Jamf Pro resources, same provider
Because the provider federates the Jamf Pro surface, you can manage Jamf Pro objects from the same project without adding another provider. They live under the jamfplatform_pro_* namespace and behave like any other resource:
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")
}
The same credentials and provider block cover both namespaces — there's nothing extra to configure.
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.
Bringing an existing tenant 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 — a way to query existing infrastructure directly 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"
}
filter {
join_with = "and"
selector = "groupType"
argument = "SMART"
}
}
}
Then run:
terraform query -generate-config-out=generated.tf
Terraform queries your tenant, returns the matching resources with their IDs, and generates both resource blocks and import blocks into generated.tf. List resources exist for the native Platform services and across the jamfplatform_pro_* surface, so the same approach 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 whole 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
With the Jamf Pro surface federated in, 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 — for Jamf Protect resources (endpoint security plans, threat prevention, telemetry and more) via the Jamf Protect GraphQL API.
- Deployment Theory Jamf Pro Provider — the community-maintained provider that started it all, which we hold in high regard. It can also authenticate through the Jamf Platform gateway (
auth_provider = "platform"), sharing the same Jamf Account credentials as this provider.
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.
We welcome contributions — bug reports, feature requests and pull requests are all appreciated. The provider will continue to grow in lockstep with the Jamf Platform APIs as more capabilities are added. For questions and discussion, join #terraform-provider-jamfplatform on the MacAdmins Slack.