TERRAFORM PROVIDER FOR INTUNE AND ENTRA ID
Review your Microsoft 365 tenant as code.
TerraProvider is a Terraform and OpenTofu provider for Microsoft 365 that manages Intune, Entra ID and tenant-wide settings as code. A Conditional Access or compliance change is a plan someone reads before it's applied, and configuration drift from the portal shows up in the next one.
Community Edition free for organizations with up to 99 users. See pricing
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.
Works with what you already run
- Terraform
- OpenTofu
- Microsoft Graph
- GitHub Actions
- Azure DevOps
- Azure CLI
Intune and Entra ID are configured in browser tabs
That works until someone asks what changed, who agreed to it and why production looks different from test.
Nobody reviews a click.
A Conditional Access change is live the moment someone presses Save. The first review is often the helpdesk call.
With TerraProvider the change is a pull request. The plan shows which attribute moves from what to what before anything touches the tenant.
The portal remembers who, not why.
The audit log names the account that saved a policy. The ticket, the reason and the colleague who agreed are somewhere else, if anywhere.
Git keeps the reason next to the change: commit message, pull request, approval. Rolling back is a revert.
Test and production drift apart.
The test tenant got the new compliance rule in March, production in May, with one setting different and nobody sure which.
One module, applied to both tenants. Where they differ, a variable says so.
The second tenant is built by hand.
A new subsidiary or a rebuilt tenant means clicking through every policy with the old tenant open in the next tab.
Export groups, filters and policies into code once (start from the tenant you have), then apply the same code to the new tenant.
The change nobody wrote down.
Someone fixed something in the portal late at night and forgot about it. The documentation now describes a tenant that doesn't exist.
The next plan lists it, including settings your files never mention. You decide whether to adopt it or put it back.
~ exclude_users = [ - "7c1e4a90-3f2b-4d8e-9a61-0b5c2e8f1d47" ]
Intune as code: the plan also watches the settings you leave out
- A change to an attribute in your file: shown, and reverted by the next apply.
- A change to an attribute you left out: shown, and reset to the portal's default.
- A change you want to keep: write it into the file, and the difference is gone.
A setting someone switched on and off again in the portal can show up once as a change back to its default. That's the price of watching everything, and 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"] }}
Conditional Access as code: new policies start in report-only
- The default is
enabledForReportingButNotEnforcedunless your file says otherwise. - Every policy change is a diff in a pull request, with the approval next to it.
- Microsoft's agent conditions for Conditional Access came with the Graph metadata update in 0.20.0.
resource "microsoft365wp_conditional_access_policy" "block_legacy_auth" { display_name = "Block legacy authentication" conditions = { users = { include_users = ["All"] } applications = { include_applications = ["All"] } client_app_types = ["exchangeActiveSync", "other"] } grant_controls = { operator = "OR" built_in_controls = ["block"] }}
# microsoft365wp_conditional_access_policy.block_legacy_auth will be created + resource "microsoft365wp_conditional_access_policy" "block_legacy_auth" { + display_name = "Block legacy authentication" + id = (known after apply) + state = "enabledForReportingButNotEnforced" + conditions = { ... } ... }Plan: 1 to add, 0 to change, 0 to destroy.
Example data, plan shortened.
What it manages
60 resources and 127 data sources in version 0.20.2, across 25 areas of Intune and Entra ID. Users, groups and app registrations are read, not created.
Resources are generated from Microsoft's Graph metadata
A new kind of object still needs a release of its own: someone has to decide which attributes are required and which defaults are right. The metadata update covers new attributes of the objects already there.
Import the tenant you already have
Nobody starts with an empty tenant. TerraProvider adopts what's there, and the quickstart does most of the typing.
Export what's there.
The quickstart's PowerShell export writes your groups, filters, notification templates, compliance policies, configuration policies into Terraform files, with a mapping for every object.Import it into the state.
The same tool imports them in order, groups and filters before the policies that point to them. Any other resource can be imported too.Export single objects.
For anything else, one command writes a Terraform file for a single object by its id, using Terraform's own configuration generation.Run the first plan.
It shows what still differs between code and tenant. The quickstart calls the result a starting point, which is fair: not every attribute can be exported, and a short clean-up is normal.
TerraProvider encrypts and uploads the app package itself
.intunewin and APPX/MSIX files, so the file name and the setup file come from the package, not from a copy in your HCL.- 21 app types, from Win32 and MSI to macOS DMG and PKG, App Store, managed Google Play and web links.
- Assignments to all devices, all licensed users, groups and exclusion groups, each with a filter, in the same resource.
- Line-of-business apps for iOS and Android are read, not uploaded.
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.
It runs in the pipeline that already deploys Azure
The provider signs in with the same ARM_* settings as the azuread provider, so the pipeline that deploys your Azure resources can usually deploy your tenant settings too.
Apply one tenant baseline to many tenants
- A new tenant gets the baseline with one module block and one plan.
- Exceptions live in variables, not in someone's memory.
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.
Which edition fits your organization
No credit card or sales call to get started. Both editions run the same provider; Enterprise adds support and covers organizations from 100 users.
Specifications
Provider
- Address
terraprovider/microsoft365wp- Works with
- Terraform and OpenTofu
- Plugin protocol
- 6
- Latest version
- 0.20.2, October 23, 2026
- Builds
- 10 (macOS, Linux, Windows, FreeBSD)
- Source
- Public on GitHub, Business Source License 1.1
Microsoft Graph
- API
- Microsoft Graph, beta endpoint
- Clouds
- global, US Government L4, US Government L5 (DoD), China
- Throttling
- Up to 10 retries, 3 to 60 seconds apart
Coverage
- Resources
- 60
- Data sources
- 127
- Areas
- 25
- Tenant-wide settings
- 13
- Functions
- 2
- Import
- every resource
Sign-in
- Methods
- OpenID Connect (GitHub Actions, Azure DevOps, any OIDC token), client certificate, client secret, managed identity, AKS workload identity, Azure CLI
- Local default
- Azure CLI
Behavior
- Attributes you leave out
- Watched, set to the portal's default
- New Conditional Access policies
- report-only
- App packages
- .intunewin, MSI, APPX/MSIX, DMG, PKG, uploaded again only when the file changed
Licensing and support
- Editions
- Community Edition (up to 99 users, free), Enterprise (from 100 users)
- Support
- Monday to Friday, 08:00 to 18:00 CET/CEST
- Response
- Typically under 8 hours for incidents, within support hours
- Language
- English
- Certification
- TerraProvider is ISO 27001 certified (certificate)
Questions before the first plan
01Does it work with OpenTofu?
Yes. The provider is listed in the OpenTofu Registry under the same address, terraprovider/microsoft365wp, and the quickstart's own workflows run OpenTofu. Your configuration doesn't change when you switch.
02How is TerraProvider different from Microsoft365DSC?
They work in different toolchains. Microsoft365DSC is built on PowerShell Desired State Configuration; TerraProvider is a provider for Terraform and OpenTofu, so your tenant settings are written in HCL and use the same state, plans and pipelines as your Azure resources. If your team already runs Terraform or OpenTofu, it fits into what you have.
03Does it create users and groups?
No. Users, groups, applications and service principals are lookups: you can read them and reference their ids. Create groups with HashiCorp's azuread provider, as the quickstart does, and assign policies to them with TerraProvider.
04Can we use it next to the azuread provider or another Microsoft 365 provider?
Yes. The quickstart does it: groups come from HashiCorp's azuread provider, and TerraProvider assigns policies to them in the same configuration. Let one provider own each object: TerraProvider watches every attribute of the objects it manages, so a change another tool makes to one of them shows up in the next plan.
05We configured our tenant by hand. Do we start over?
No. Every resource can be imported into the Terraform state, and the quickstart exports groups, filters, notification templates, compliance policies, configuration policies into code and imports them for you. Expect a short clean-up after the first plan; the export is a starting point, not a perfect copy.
06What happens to settings we don't write down?
They get the default the Microsoft portals use, and the plan watches them like everything else. A change made in the portal shows up in the next plan; the next apply resets it, unless you put the new value into your file.


