Msgraph Terraform - Infrastructure-as-Code for M365/EntraID -  Part 1 Authentication

Msgraph Terraform - Infrastructure-as-Code for M365/EntraID - Part 1 Authentication

Introduction

Microsoft released the long awaited MSGraph Terraform provider in 08/25 to public preview 🎉. At the time of writing the msgraph-terraform provider is available in version “0.2.0” and it supports Day 0 operations for initial deployment and provisioning. Finally you can manage each and every resource or service which is available in Microsoft Graph API.

💡 Good to know: The MSGraph Terraform provider implementation is designed as a thin layer over the MSGraph REST API, and enables direct resource management via API endpoints. Schema versioning is directly embedded in the resource body of the Terraform resource block (we’ll see examples in an upcoming post) and made available through the resource_url value, you can handle different schema versions like “v1” or “beta” independently in your resource block, without depending on provider updates.

In this series we will answer the question on why this announcement is that great news, especially for platform teams and MSPs who manage multiple tenants and are already heavily invested into terraform.

We will also dig into the provider’s capabilities and see how to use the available and different resource types. The first part of the blog series will therefore focus on basics, but mostly authentication and CI/CD integration.

Why use Terraform for Microsoft365/Entra ID management ?

💡 Note: Expand to read more about why using Terraform for Microsoft 365 and Entra ID management makes strategic sense for your organization.

Click to expand

As mentioned in the introduction, if your organization has already invested in Terraform, not just for infrastructure management, but also in building team expertise, extending that investment to manage M365 and Entra ID resources makes strategic sense. The Microsoft Graph Terraform Provider brings several benefits like:

  • Unified Infrastructure-as-Code approach: Manage your cloud infrastructure, identity platform, and M365 services using the same tooling, workflows, and GitOps practices your team already knows.

  • Version control and governance: Every change to your Entra ID configuration, group memberships, Conditional Access policies, or Intune policies is tracked in Git with complete auditing. You could use branch protection policies, require approvals from SECOPS or IAM teams before merging changes, and enforce code reviews for sensitive resources. When talking about CI/CD, the benefits are also very interesting, your existing pipeline infrastructure (GitHub Actions, Azure DevOps) and automation frameworks already running in your organization. There’s no need to build separate deployment processes or learn new tools, simply extend your current CI/CD pipelines.

  • Consistency across environments: Deploy identical configurations across dev, test, and production environments or spanning configurations across multiple customer environments.

  • Reduced context switching and clickops: No need to jump between Azure Portal, PowerShell scripts, and Terraform, your team could manage everything from one codebase.

  • Declarative state management: Define your desired end-state and let Terraform handle the orchestration, rather than writing imperative scripts that need to handle every edge case. There are also a few stumbling blocks when talking declarative state management we will talk about in this blog series, especially when it comes to managing graph resources with Terraform 😮 so keep your eyes open for upcoming parts. 😏

  • Reusability through modules: Build standardized modules for common patterns (like onboarding new teams,applications or rolling out new Intune policies) and reuse them across your organization. I’m also pretty sure that there will be verified modules similar to the great Azure Verified Modules project also coming either from Microsoft or the community itself.

  • Disaster recovery and redeployability: Your entire Entra ID and M365 configuration becomes reproducible code 🎉 , making recovery scenarios and migrations way easier.

Howto authenticate in different scenarios

Setting up authentication (SPNs, Managed Identities, etc.)

When it comes to authentication and you already worked with the AzureRM provider, the options are pretty much the same. Microsoft is recommending using Service Principals or Managed Identities when running Terraform inside CI/CD. In this post I will cover how to setup authentication for Azure DevOps; this will be a little bit different for GitHub.

Also keep in mind you have to setup the correct Graph API permissions for your Application Registration depending on which service you want to manage with the msgraph-terraform provider. So for example:

Resource TypeRequired Graph API PermissionsPermission TypeScope
UsersUser.ReadWrite.AllApplicationRead and write all users’ full profiles
GroupsGroup.ReadWrite.All
GroupMember.ReadWrite.All
ApplicationManage groups and group memberships
ApplicationsApplication.ReadWrite.AllApplicationManage app registrations and service principals
Conditional AccessPolicy.ReadWrite.ConditionalAccess
Policy.Read.All
ApplicationManage Conditional Access policies
Intune PoliciesDeviceManagementConfiguration.ReadWrite.All
DeviceManagementManagedDevices.ReadWrite.All
ApplicationManage device configurations and policies
Microsoft TeamsTeam.ReadWrite.All
Channel.ReadWrite.All
Group.ReadWrite.All
ApplicationCreate and manage Teams and channels
SharePoint SitesSites.ReadWrite.All
Sites.FullControl.All
ApplicationManage SharePoint sites and permissions
Service PrincipalsApplication.ReadWrite.All
Directory.ReadWrite.All
ApplicationManage enterprise applications

💡 Must: Start with the minimum required permissions and expand as needed. Always follow the principle of least privilege.

⚠️ Important: After adding API permissions in the Azure Portal, don’t forget to click “Grant admin consent” for your organization. Without admin consent, the service principal won’t be able to perform operations even if the permissions are assigned.

Authentication Options

The msgraph Terraform provider supports three authentication methods. Choose based on your environment:

MethodBest ForSecuritySetup Complexity
Azure CLILocal developmentInherits user permissionsLow
Service PrincipalCI/CD pipelinesDedicated identityMedium
Managed IdentityAzure-hosted runnersPasswordlessLow (Azure only)

1. Azure CLI Authentication (Local Development)

Azure CLI authentication is the quickest way to get started. Terraform uses your active az login session automatically.

Supports three modes:

  • User Account: Interactive login for developers (az login)
  • Service Principal: Scripted login with SPN credentials
  • Managed Identity: Automatic authentication on Azure VMs

Configuration:

provider "msgraph" {
  # No explicit configuration needed - uses active az login session
}

Usage:

# Login and verify
az login
az account show

# Run Terraform
terraform init
terraform plan

💡 Best for: Local development and testing. For production CI/CD, use explicit Service Principal or Managed Identity authentication.


2. Service Principal Authentication (CI/CD Pipelines)

For automated deployments, use a dedicated Service Principal with explicit credentials. This provides better control, auditing, and security isolation.

Authentication Methods
MethodWhen to Use
Client SecretQuick setup, rotate regularly
Client CertificateHigher security, little bit complex setup
OpenID Connect (OIDC)Workload identity federation
Configuration via Environment Variables

Client Secret:

export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_SECRET="YourClientSecret"
export ARM_TENANT_ID="10000000-2000-3000-4000-500000000000"

Client Certificate:

export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_CERTIFICATE_PATH="/path/to/certificate.pfx"
export ARM_CLIENT_CERTIFICATE_PASSWORD="CertPassword"
export ARM_TENANT_ID="10000000-2000-3000-4000-500000000000"

Provider configuration with variables:

provider "msgraph" {
  tenant_id     = var.tenant_id
  client_id     = var.client_id
  client_secret = var.client_secret  # better use environment variables
}
Secure Secret Management

Never hardcode credentials. Use one of these solutions to inject secrets at runtime:

Azure Key Vault - (Click to expand)

Enable a Managed Identity on your DevOps agent to access Key Vault without additional credentials:

# azure-pipelines.yml
variables:
- group: terraform-keyvault-secrets  # Linked to Key Vault

steps:
- task: AzureCLI@2
  displayName: 'Run Terraform'
  inputs:
    azureSubscription: 'my-service-connection'
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: |
      export ARM_CLIENT_ID="$(kv-client-id)"
      export ARM_CLIENT_SECRET="$(kv-client-secret)"
      export ARM_TENANT_ID="$(kv-tenant-id)"
      terraform init
      terraform apply      
HashiCorp Vault - (Click to expand)
# Authenticate to Vault
export VAULT_ADDR="https://vault.company.com"
export VAULT_TOKEN=$(vault write -field=token auth/approle/login \
  role_id="${VAULT_ROLE_ID}" \
  secret_id="${VAULT_SECRET_ID}")

# Retrieve secrets
export ARM_CLIENT_ID=$(vault kv get -field=client_id secret/terraform/azure)
export ARM_CLIENT_SECRET=$(vault kv get -field=client_secret secret/terraform/azure)
export ARM_TENANT_ID=$(vault kv get -field=tenant_id secret/terraform/azure)

terraform apply

Or use the Vault provider directly:

data "vault_generic_secret" "terraform_creds" {
  path = "secret/terraform/azure"
}

provider "msgraph" {
  client_id     = data.vault_generic_secret.terraform_creds.data["client_id"]
  client_secret = data.vault_generic_secret.terraform_creds.data["client_secret"]
  tenant_id     = data.vault_generic_secret.terraform_creds.data["tenant_id"]
}
1Password CLI - (Click to expand)
# Retrieve secrets and run Terraform
export ARM_CLIENT_ID=$(op read "op://DevOps/Terraform-Azure/client_id")
export ARM_CLIENT_SECRET=$(op read "op://DevOps/Terraform-Azure/client_secret")
export ARM_TENANT_ID=$(op read "op://DevOps/Terraform-Azure/tenant_id")

terraform apply

Or use secret references in CI/CD:

steps:
- script: |
    op run -- terraform apply    
  env:
    ARM_CLIENT_ID: "op://DevOps/Terraform-Azure/client_id"
    ARM_CLIENT_SECRET: "op://DevOps/Terraform-Azure/client_secret"
    ARM_TENANT_ID: "op://DevOps/Terraform-Azure/tenant_id"

3. Managed Identity Authentication (Azure-Hosted Runners or Azure Managed Devops Pools)

Passwordless authentication for Terraform running on Azure compute resources (VMs, Container Instances, DevOps Managed Pools). Activating User-Assigned Managed Identities on a Managed Azure Devops allows you to grant the Managed Identity GraphAPI Permissions as seen here in this article and from then on go secretless and request the access token via IMDS.

How does it work ?: Azure automatically provides an identity token via the Instance Metadata Service (IMDS)—a local endpoint available at http://169.254.169.254 on Azure resources. The provider makes an HTTP call to IMDS to retrieve an OAuth2 token, eliminating the need to manage any secrets.

Token request:

GET 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/' HTTP/1.1
Metadata: true

Managed-Identities

The response contains a temporary access token that Terraform uses to authenticate against Microsoft Graph API.

Prerequisites:

  1. Enable Managed Identity (System-Assigned or User-Assigned) on your Azure resource
  2. Grant the Managed Identity appropriate Graph API permissions in Entra ID (same as Service Principal)
  3. Ensure the compute resource can reach the IMDS endpoint (default for Azure resources)

Configuration:

provider "msgraph" {
  use_msi = true  # Uses Managed Identity
}

Azure DevOps example:

# Requires: Managed DevOps Pool with Managed Identity enabled
steps:
- script: |
    # No credential setup needed - uses Managed Identity automatically
    terraform init
    terraform apply    
  displayName: 'Run Terraform with Managed Identity'

🔒 Best Practices: Managed Identity is the most secure option for Azure-hosted workloads. Tokens are short-lived and automatically rotated by Azure—no secrets to rotate, leak, or manage.

⚠️ Limitations: Only works on Azure compute resources. For non-Azure environments (GitHub Actions on GitHub-hosted runners, on-premises), use Service Principal authentication instead.


Provider resource types

Resources

Now as we have the authentication out of our way let’s hop into the provider resource types available.

Resource TypeWhen to UseBlast Radius
msgraph_resourceThis resource can manage any Microsoft Graph API resourcee.g adds defined users to a group
msgraph_resource_actionThis resource can perform any Microsoft Graph API action. Use this for operations like password resets, sending emails, or other one-time actionschanges a setting
msgraph_resource_collectionManage the full contents of a child reference collection (such as group members or owners) for an existing Microsoft Graph resource. Missing items are added; extra remote items are removedtakes full control of a resource e.g defines all members of a group
msgraph_update_resourceThis resource is used to add or modify properties on an existing resource. When msgraph_update_resource is deleted, no operation will be performed, and these properties will stay unchanged. If you want to restore the modified properties to some values, you must apply the restored properties before deleting.updates an existing resource

Now let’s look at the first resource type available “msgraph_resource”. I will show you a few examples on how to use the resource type and what options we have available there.

💡 Tip: When working with MSGraph-Terraform the Microsoft Graph API Reference is your friend 😉

Example

When to use which resource type may, at the beginning, be the most interesting question to answer. In the following example, we will try to use the new Terraform provider to handle some very basic user onboarding. The idea is to create a user object and send them a welcome email. It’s definitely not the best use case for the provider, as Terraform is designed for reproducible, idempotent configuration and lifecycle management of resources.

User onboarding, however, is typically a one-time operational task rather than a managed resource. That said, this example effectively demonstrates the provider’s capabilities and helps illustrate where the boundaries between infrastructure-as-code and operational automation should be drawn.

Anyway i think it`s a great example because we can use the first two resource types (msgraph_resource + msgraph_resource_action).

SPN API Permissions

As I do run the tests on my local machine, we will use a SPN created which has the following api permissions, remember least privilege, so we will start small:

TypeAPI permission
ApplicationUser.ReadWrite.All
ApplicationMail.Send

Now as we do have the right permission to create a user and send them an email, let’s authenticate via az login:


az login --service-principal \
         --username 00000000-0000-0000-0000-000000000000 \
         --password "MyCl1eNtSeCr3t" \
         --tenant 10000000-2000-3000-4000-500000000000 \
         --allow-no-subscriptions

Provider Examples

User object creation

Let’s create our Terraform config for creating the user:


terraform {
  required_providers {
    msgraph = {
      source  = "Microsoft/msgraph"
      version = "0.2.0"
    }
  }
}

provider "msgraph" {
  use_msi = false
}

resource "msgraph_resource" "user" {
  url = "users"

  body = {
    accountEnabled    = true
    displayName       = "Test User Display Name"
    mailNickname      = "testusermailnickname"
    userPrincipalName = "testuser@mycompany.com"
    usageLocation     = "DE"
    passwordProfile = {
      forceChangePasswordNextSignIn = false
      password                      = "P@ssw0rd1234"
    }
  }
  response_export_values = {
    all = "@" 
    /* 
       without exporting the graph api response here, 
       we wont be able to use the output of the resource in our send email 
       msgraph_resource_action block
    */
  }
}

License assignment

As we need to make sure the user has a mailbox as well, we need to assign them a license. Here are some common license SKUs you can use:

LicenseSKU ID
Microsoft 365 E305e9a617-0261-4cee-bb44-138d3ef5d965
Microsoft 365 E506ebc4ee-1bb5-47dd-8120-11324bc54e06
Office 365 E36fd2c87f-b296-42f0-b197-1e91e994b900
Exchange Online Plan 14b9405b0-7788-4568-add1-99614e613b69

To assign the user a license, make sure you’ve set the usageLocation in the user config.


variable "license_sku_id" {
  description = "SKU ID for license assignment"
  type        = string
  default     = "05e9a617-0261-4cee-bb44-138d3ef5d965"
}

# Assign Microsoft 365 E3 license to the user
resource "msgraph_resource_action" "assign_license" {
  resource_url = "users/${msgraph_resource.user.output.all.id}"
  action       = "assignLicense"
  method       = "POST"
  body = {
    addLicenses = [
      {
        skuId = var.license_sku_id
        disabledPlans = []
      }
    ]
    removeLicenses = []
  }
  
  depends_on = [msgraph_resource.user]
}

Send Mail

As the user now has a mailbox let’s send them an email, with a nice welcome message from HR:


resource "msgraph_resource_action" "send_welcome_email" {
  resource_url = "users/hr@mycompany.com"
  action       = "sendMail"
  method       = "POST"

  body = {
    message = {
      subject = "Welcome to the organization!"
      body = {
        contentType = "HTML"
        content     = "<h1>Welcome!</h1><p>We're excited to have you join our team.</p>"
      }
      toRecipients = [
        {
          emailAddress = {
            address = resource.msgraph_resource.user.output.all.userPrincipalName
            name    = resource.msgraph_resource.user.output.all.displayName
          }
        }
      ]
    }
    saveToSentItems = true
  }
}

a “terraform init” and “terraform apply” later - the newly created user has received the email 😉

Email

Conclusion

The msgraph Terraform provider is finally available, and it solves a real problem. If your team already uses Terraform for infrastructure, you can now manage M365 and Entra ID the same way. No more clicking through portals—just define your users, groups, and policies in code, commit them to Git, and let your pipeline handle the rest.

When it comes to usecases, my opinion is that this provider will really shine when it comes to:

  • Managing Global Tenant settings (App Management Policies)
  • CRUD operations on Conditional Access Policies (also think about drift detection !!)
  • Handling Intune Policies
  • Access Packages, etc.

As the provider is, at the time of writing, available in version 0.2.0 there will be still some bugs, so keep that in mind.

What`s next ?

This is just the beginning. I have already lots of ideas on how to move forward here. In Part 2 of this series, we’ll look into more practical, real-world implementations—showing you how to manage Entra ID global tenant settings, Conditional Access policies, and Intune configurations with actual code examples.

So if you don’t want to miss them - stay tuned!