In my recent exploration of the new Microsoft Graph Terraform provider, I set out with a simple goal: to create a Conditional Access policy based on a known network location — specifically, my home IP address.

What seemed like a straightforward use case quickly ran into problems. While the provider promises flexibility by exposing Microsoft Graph directly, its low-level design and rough edges made the experience far less smooth than expected.

A Minimal Setup to Target My Home IP

The journey began by configuring the new msgraph Terraform provider. It follows a similar design philosophy as the AzAPI provider, offering a thin abstraction layer over the raw API schema. With AzAPI, this means working directly against the Azure Resource Manager (ARM) schema; with msgraph, you’re operating directly against the Microsoft Graph schema. That means very little abstraction or guidance—just raw API mechanics.

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

provider "msgraph" {
}

To determine my public IP address, I used the http data source:

data "http" "home_ip" {
  url = "https://ipv4.icanhazip.com"
}

locals {
  home_ip = trimspace(data.http.home_ip.response_body)
}

Next, I used the msgraph_resource—very reminiscent of AzAPI’s generic azapi_resource—to define an IP-based named location:

resource "msgraph_resource" "home_location" {
  url = "identity/conditionalAccess/namedLocations"
  body = {
    "@odata.type" = "#microsoft.graph.ipNamedLocation"
    displayName   = "Home CIDRs"
    "isTrusted"   = false,
    ipRanges = [
      {
        "@odata.type" = "#microsoft.graph.iPv4CidrRange",
        cidrAddress   = "${local.home_ip}/32"
      }
    ]
  }

  # Capture the created object id
  response_export_values = {
    id = "id"
  }
}

The schema closely mirrors the raw API, and I referred directly to Microsoft’s REST documentation to shape the JSON body. The lack of abstraction meant I had to supply all the OData type annotations manually — an awkward and brittle experience. It’s functional, but far from elegant.

https://learn.microsoft.com/en-us/graph/api/conditionalaccessroot-post-namedlocations?view=graph-rest-1.0

The example is pure JSON but its pretty easy for me to figure out that I am trying to do from there as there is almost no abstraction from the underlying schema.

POST https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations
Content-type: application/json

{
    "@odata.type": "#microsoft.graph.ipNamedLocation",
    "displayName": "Untrusted IP named location",
    "isTrusted": false,
    "ipRanges": [
        {
            "@odata.type": "#microsoft.graph.iPv4CidrRange",
            "cidrAddress": "12.34.221.11/22"
        },
        {
            "@odata.type": "#microsoft.graph.iPv6CidrRange",
            "cidrAddress": "2001:0:9d38:90d6:0:0:0:0/63"
        }
    ]
}

The OData Type annotations are pretty janky. If there was a Terraform provider with rational abstraction I’m sure I wouldn’t have to deal with this mess.

The Deployment Fails — But Not Where You’d Expect

After authenticating with the correct Entra ID tenant using az login, I ran:

terraform apply

At first, things looked fine. The resource creation began as expected but instead of completing successfully, Terraform failed during the read-back phase — oddly, not during the initial POST, but on a follow-up GET request.

msgraph_resource.home_location: Creating…
╷
│ Error: Failed to read data source
│
│ with msgraph_resource.home_location,
│ on main.tf line 20, in resource “msgraph_resource” “home_location”:
│ 20: resource “msgraph_resource” “home_location” {
│
│ GET https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations/0667e619-5c87-4f99-b31c-882799de4217
│ — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
│ RESPONSE 404: 404 Not Found
│ ERROR CODE: ResourceNotFound
│ — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
│ {
│ “error”: {
│ “code”: “ResourceNotFound”,
│ “message”: “NamedLocation with id 0667e619–5c87–4f99-b31c-882799de4217 does not exist in the directory.”,
│ “innerError”: {
│ “date”: “2025–08–22T17:05:13”,
│ “request-id”: “d2319f66-e180–4348-a360–4d36abac92fd”,
│ “client-request-id”: “d2319f66-e180–4348-a360–4d36abac92fd”
│ }
│ }
│ }

However, it looks like the resource does get created.

Alt

The message was clear: Microsoft Graph said the resource didn’t exist — despite just having created it. However, Terraform had already returned the resource ID, indicating that the initial creation was in fact successful.

It looks like this issue has already been logged so hopefully since there is only one resource we can get to this quick. It seems like many resources that have a time delay during provisioning due to propogation or whatever will have this issue.

https://github.com/microsoft/terraform-provider-msgraph/issues/32

Verifying the Resource — A Race Condition?

Curious, I tested retrieval of the supposedly non-existent resource by setting up a fresh Terraform workspace and referencing it directly via its ID:

data "msgraph_resource" "home_location" {
  url = "identity/conditionalAccess/namedLocations/0667e619-5c87-4f99-b31c-882799de4217"
  response_export_values = {
    all          = "@"
    display_name = "displayName"
  }
}

This worked perfectly — the resource was found and returned as expected. That leads to only one conclusion: there’s a delay between the resource being created and when it becomes available for retrieval through the Microsoft Graph API. The Terraform provider appears not to account for this delay, resulting in a race condition. Terraform attempts a GET immediately after the POST, before the Microsoft Graph backend has fully propagated the object.

Final Thoughts

This experience underscores the limitations of the Microsoft Graph Terraform provider in its current state. Its low-level, schema-first design offers power, but requires users to deal directly with all the quirks of the Microsoft Graph API — quirks like OData annotations and eventual consistency.

For now, anyone experimenting with this provider should be prepared for fragile workflows, unclear abstractions, and behavior that may feel counterintuitive — especially compared to more mature Terraform providers like the azurerm or aws.

I think the new provider will prove useful. It opens so many doors to a huge API surface — MS Graph!!! However, until built-in retry logic for eventual consistency, use of the msgraph_resource will remain an exercise in manual craftsmanship rather than reliable infrastructure as code.

Alt