HOW IT WORKS

Set up TerraProvider in your tenant and pipeline.

What the engineer who sets up TerraProvider needs to know: how it signs in, which permissions it asks for, what the first plan looks like and how an existing tenant comes under code.

  • terraprovider/microsoft365wp
  • Terraform and OpenTofu
  • 10 builds
  • version 0.20.2
SETUP

Your first plan in four steps

About as long as setting up any other Azure provider, because the sign-in works the same way.

  1. Register an app in Entra ID.

    Single tenant, no redirect URI needed. Note the application (client) ID and the directory (tenant) ID.
  2. Grant Graph permissions.

    Add the application permissions for the objects you want to manage (see the table below) and grant admin consent.
  3. Declare the provider.

    Set ARM_TENANT_ID and ARM_CLIENT_ID, plus ARM_CLIENT_SECRET for a first test. In a pipeline, use OpenID Connect instead (below).
    main.tf
    terraform {  required_providers {    microsoft365wp = {      source = "terraprovider/microsoft365wp"    }  }}provider "microsoft365wp" {}
  4. Plan.

    Write one resource, a compliance policy for example, and run terraform plan or tofu plan. Nothing changes in the tenant until you apply.
    compliance.tf
    resource "microsoft365wp_device_compliance_policy" "windows_defender" {  display_name = "Windows - Defender for Endpoint"  windows10 = {    device_threat_protection_enabled = true  }  scheduled_actions_for_rule = [{    scheduled_action_configurations = [{      action_type        = "block"      grace_period_hours = 6    }]  }]  assignments = [{    target = { all_licensed_users = {} }  }]}

    Example data.

PERMISSIONS

The Graph permissions it asks for

Application permissions, granted with admin consent. Grant only those for the objects you manage; a resource not listed here names its permissions on its docs page.

The Graph permissions it asks for
PermissionWhat it allows
DeviceManagementConfiguration.ReadWrite.AllIntune device configuration and policies
DeviceManagementApps.ReadWrite.AllIntune apps
DeviceManagementServiceConfig.ReadWrite.AllIntune service configuration, such as enrollment
Policy.ReadWrite.ConditionalAccessConditional Access policies
Policy.Read.AllReading your organization's policies
Application.ReadWrite.AllApplications
Directory.ReadWrite.AllDirectory data
Group.ReadWrite.AllGroups
User.ReadWrite.AllUsers' full profiles
CloudPC.ReadWrite.AllWindows 365 configuration

Source: the provider's guide "Configuring a Service Principal for managing EntraID".

SIGN-IN

The same sign-in settings as the azuread provider

The provider reads the same ARM_* settings as the azuread provider. Set ARM_TENANT_ID and ARM_CLIENT_ID, then pick one method.

The same sign-in settings as the azuread provider
MethodWhere it fitsSwitch it on
OpenID Connect, GitHub ActionsWorkflows without a stored secretARM_USE_OIDC=true
OpenID Connect, Azure DevOpsPipelines with a service connectionARM_USE_OIDC=true
ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID
OpenID Connect, any issuerOther CI systems that hand out an ID tokenARM_USE_OIDC=true
ARM_OIDC_TOKEN / ARM_OIDC_TOKEN_FILE_PATH
Client certificateRunners that hold a certificateARM_CLIENT_CERTIFICATE / ARM_CLIENT_CERTIFICATE_PATH
Client secretA first test, a labARM_CLIENT_SECRET / ARM_CLIENT_SECRET_FILE_PATH
Managed identityRunners on AzureARM_USE_MSI=true
AKS workload identityRunners on Kubernetes in AzureARM_USE_AKS_WORKLOAD_IDENTITY=true
Azure CLIYour laptopon by default; ARM_USE_CLI=false turns it off

Each setting also exists as a provider argument (use_oidc, client_certificate_path and so on). The provider's OpenID Connect guide shows how to let Entra ID trust a GitHub repository. In one line: OpenID Connect (GitHub Actions, Azure DevOps, any OIDC token), client certificate, client secret, managed identity, AKS workload identity, Azure CLI.

Four clouds

Set environment or ARM_ENVIRONMENT. The provider then talks to that cloud's Microsoft Graph endpoint: global, US Government L4, US Government L5 (DoD), China.

Environment

global (also public)
The commercial cloud, the default
usgovernmentl4 (also usgovernment)
US Government L4
usgovernmentl5 (also dod)
US Government L5 (DoD)
china
Microsoft's cloud in China
Your own endpoint
metadata_host or ARM_METADATA_HOSTNAME
DRIFT

What the plan watches

Terraform compares your configuration with what the tenant reports. TerraProvider makes that comparison cover the whole object: every attribute you don't set gets a default, taken from what the Microsoft portals send when you create the same object there. So an exclusion added to a policy in the portal, a toggle switched in the Intune admin center or a setting changed by a script shows up in the next plan, whether or not your file mentions it.
  • Collections you leave out default to empty, so an added member, exclusion or app is visible.
  • A new Conditional Access policy without a state is created as enabledForReportingButNotEnforced.
  • Each attribute's default is on the resource's docs page, marked as the provider default.

The flip side: a setting someone switched on and off again in a portal can come back as false instead of unset, and appears once as a change. One apply settles it.

In the portal: No changes since the last apply.

microsoft365wp_conditional_access_policy.require_mfa_admins: Refreshing state... [id=2b7f9c3e-8d41-4f6a-b0e2-5c9a1d7e3f80]No changes. Your infrastructure matches the configuration.
Example data.
EXISTING TENANT

Export and import an existing tenant in two phases

The quickstart repository has a reference configuration with modules, GitHub Actions workflows and a PowerShell export. The export works in two phases, because policies point to groups and filters.

  1. Export groups, filters and notification templates.

    They're written into the quickstart's modules, with a mapping file for each type. Mail-enabled groups are left out unless you ask for them.
  2. Import them.

    The tool calls Terraform or OpenTofu to import every exported object into the state.
  3. Export compliance and configuration policies.

    Settings catalog and endpoint security policies count as configuration policies. Each export also keeps a raw JSON dump of what Graph returned.
  4. Import them.

    Same tool, second phase.
  5. Plan.

    The plan shows what still differs. Not every attribute can be exported, so expect a short clean-up before the first clean plan.

One object at a time

For anything the export doesn't cover, a small script writes a Terraform file for one object of any resource type, given its id. It uses Terraform's own configuration generation, so the file holds what the provider itself reads from the tenant.

The quickstart on GitHub
APPS

One resource holds the app, its package and its assignments

microsoft365wp_mobile_app covers 21 app types. For .intunewin, MSI, APPX/MSIX, DMG, PKG packages the provider encrypts the file and uploads it to Intune, and it does that again only when source_sha256 changes. Assignments sit in the same resource: all devices, all licensed users, a group or an exclusion group, each with an include or exclude filter.
locals {  agent_package = "${path.module}/packages/contoso-agent-4.2.1.intunewin"  agent_meta    = provider::microsoft365wp::parse_intunewin_metadata(local.agent_package)}resource "microsoft365wp_mobile_app" "contoso_agent" {  display_name = "Contoso Agent"  publisher    = "Contoso"  win32_lob = {    file_name              = local.agent_meta.file_name    setup_file_path        = local.agent_meta.setup_file    install_command_line   = "setup.exe /quiet"    uninstall_command_line = "setup.exe /uninstall /quiet"    detection_rules = [{      filesystem = {        detection_type      = "exists"        path                = "C:\\Program Files\\Contoso\\Agent"        file_or_folder_name = "agent.exe"      }    }]    source_file   = local.agent_package    source_sha256 = filesha256(local.agent_package)  }  assignments = [{    target   = { group = { group_id = "3d9b2f61-7a4e-4c18-9e05-6f2a8b1c4d93" } }    settings = { win32_lob = {} }  }]}

Example data.

  • Windows

    Win32 (.intunewin), MSI, APPX/MSIX, Microsoft Store (new, via WinGet), Microsoft Store (legacy), Microsoft Edge, Microsoft 365 Apps, web links.

  • macOS

    DMG, PKG, Microsoft Edge, Microsoft Defender, Microsoft 365 Apps, web clips.

  • iOS and iPadOS

    App Store apps, managed iOS store apps, web clips.

  • Android

    Google Play store apps, managed Google Play apps, managed Android store apps.

  • Any platform

    Web links.

TENANTS

Several tenants from one repository

A provider block signs in to one tenant. For several tenants, declare one block per tenant with an alias and its tenant_id, write the baseline as a module and call it once per tenant. Each block signs in on its own, so each tenant can have its own service principal with its own permissions.
  • Differences between tenants are module variables, visible in the review.
  • A new tenant is one provider block, one module call and one plan.
main.tf
provider "microsoft365wp" {  alias     = "contoso"  tenant_id = "8a1f3c2e-5b7d-4e90-a6c4-2d9e7f1b0c35"}provider "microsoft365wp" {  alias     = "fabrikam"  tenant_id = "e4c27b91-0d3a-4f86-b5e2-7a1c9d0f6b48"}module "baseline_contoso" {  source    = "./baseline"  providers = { microsoft365wp = microsoft365wp.contoso }}module "baseline_fabrikam" {  source     = "./baseline"  providers  = { microsoft365wp = microsoft365wp.fabrikam }  allow_byod = true}

Example data.

How it handles Graph throttling

Microsoft Graph limits how many requests a client may send, and a plan over a large tenant sends many. The provider waits and retries on its own.

Retries

Retries per request
up to 10
Wait between retries
3 to 60 seconds
A Retry-After of zero
Stretched to the backoff, because Graph often answers that way and an immediate retry rarely helps

Start with the quickstart.

Clone it, point it at a test tenant and run a plan. The Community Edition is free for organizations with up to 99 users.