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
Your first plan in four steps
About as long as setting up any other Azure provider, because the sign-in works the same way.
Register an app in Entra ID.
Single tenant, no redirect URI needed. Note the application (client) ID and the directory (tenant) ID.Grant Graph permissions.
Add the application permissions for the objects you want to manage (see the table below) and grant admin consent.Declare the provider.
SetARM_TENANT_IDandARM_CLIENT_ID, plusARM_CLIENT_SECRETfor a first test. In a pipeline, use OpenID Connect instead (below).terraform { required_providers { microsoft365wp = { source = "terraprovider/microsoft365wp" } }}provider "microsoft365wp" {}
Plan.
Write one resource, a compliance policy for example, and runterraform planortofu plan. Nothing changes in the tenant until you apply.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.
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.
| Permission | What it allows |
|---|---|
| DeviceManagementConfiguration.ReadWrite.All | Intune device configuration and policies |
| DeviceManagementApps.ReadWrite.All | Intune apps |
| DeviceManagementServiceConfig.ReadWrite.All | Intune service configuration, such as enrollment |
| Policy.ReadWrite.ConditionalAccess | Conditional Access policies |
| Policy.Read.All | Reading your organization's policies |
| Application.ReadWrite.All | Applications |
| Directory.ReadWrite.All | Directory data |
| Group.ReadWrite.All | Groups |
| User.ReadWrite.All | Users' full profiles |
| CloudPC.ReadWrite.All | Windows 365 configuration |
Source: the provider's guide "Configuring a Service Principal for managing EntraID".
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.
| Method | Where it fits | Switch it on |
|---|---|---|
| OpenID Connect, GitHub Actions | Workflows without a stored secret | ARM_USE_OIDC=true |
| OpenID Connect, Azure DevOps | Pipelines with a service connection | ARM_USE_OIDC=trueARM_ADO_PIPELINE_SERVICE_CONNECTION_ID |
| OpenID Connect, any issuer | Other CI systems that hand out an ID token | ARM_USE_OIDC=trueARM_OIDC_TOKEN / ARM_OIDC_TOKEN_FILE_PATH |
| Client certificate | Runners that hold a certificate | ARM_CLIENT_CERTIFICATE / ARM_CLIENT_CERTIFICATE_PATH |
| Client secret | A first test, a lab | ARM_CLIENT_SECRET / ARM_CLIENT_SECRET_FILE_PATH |
| Managed identity | Runners on Azure | ARM_USE_MSI=true |
| AKS workload identity | Runners on Kubernetes in Azure | ARM_USE_AKS_WORKLOAD_IDENTITY=true |
| Azure CLI | Your laptop | on 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_hostorARM_METADATA_HOSTNAME
What the plan watches
- 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.
resource "microsoft365wp_conditional_access_policy" "require_mfa_admins" { display_name = "Require MFA for admins" state = "enabled" conditions = { users = { include_roles = ["62e90394-69f5-4237-9190-012177145e10"] } applications = { include_applications = ["All"] } } grant_controls = { operator = "OR" built_in_controls = ["mfa"] }}
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.
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.Import them.
The tool calls Terraform or OpenTofu to import every exported object into the state.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.Import them.
Same tool, second phase.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 GitHubOne 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 = {} } }]}
microsoft365wp_mobile_app.contoso_agent: Refreshing state... [id=5e0c8a17-2d94-4b6f-a3e1-9c7d0f2b8a46]No changes. Your infrastructure matches the configuration.
# microsoft365wp_mobile_app.contoso_agent will be updated in-place ~ resource "microsoft365wp_mobile_app" "contoso_agent" { ~ win32_lob = { ~ source_file = "./packages/contoso-agent-4.2.1.intunewin" -> "./packages/contoso-agent-4.3.0.intunewin" ~ source_sha256 = "9f2c41d0…" -> "b71e05aa…" # (6 unchanged attributes hidden) } # (4 unchanged attributes hidden) }Plan: 0 to add, 1 to change, 0 to destroy.
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.
Several tenants from one repository
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.
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.