Deploying Azure Purview with Terraform: Overcoming AzureRM Limitations

Microsoft shifted the Azure Purview to be Tenant aligned, but the AzureRM Provider hasn’t made that move yet, so if you are deploying your Azure infrastructure using Terraform you need to do some alternative changes to get it to work, especially when you’re trying to deploy Purview securely using private networking.

After a few painful attempts I reverse engineered the ARM Templates to determine the correct AzAPI resources you need to create a working example of the new Azure Purview.

The problem with AzureRM and Purview

At the time of writing, the azurerm_purview_account resource is based on an older API version. In practice, that means:

  • Deploys as multiple accounts.
  • Missing or incomplete network configuration options.
  • Private access behaving inconsistently.
  • Limited control when locking down ingress and egress.

If you’re building using the new view of Purview these will not work, so instead we can use the latest API version 2024-04-01. Below is a working example with a Resource Group for the account and the Purview Account. You can find the documentation of the API using AzAPI on Microsoft.Purview/accounts – Microsoft Learn. You will notice it has been configured to all be private networked, which I will talk about in the later section.

resource "azurerm_resource_group" "purview" {
  name     = "rg-example-purview"
  location = "uksouth"

  tags = var.tags
}

resource "azapi_resource" "purview" {
  type      = "Microsoft.Purview/accounts@2024-04-01-preview"
  name      = "purview-main"
  parent_id = azurerm_resource_group.purview.id

  identity {
    type         = "SystemAssigned"
    identity_ids = []
  }
  location = azurerm_resource_group.purview.location
  tags     = var.tags

  body = {
    properties = {
      cloudConnectors = {
      }
      ingestionStorage = {
        publicNetworkAccess = "Disabled"
      }
      managedEventHubState                = "Disabled"
      managedResourceGroupName            = "${azurerm_resource_group.purview.name}-mgmt"
      managedResourcesPublicNetworkAccess = "NotSpecified"
      mergeInfo = {
      }
      publicNetworkAccess = "Disabled"
      tenantEndpointState = "Enabled"
    }
    sku = {
      capacity = 1
      name     = "Standard"
    }
  }
}

The networking issue nobody tells you about

When you deploy a Purview account, Azure quietly creates a managed storage account behind the scenes in a unmanaged subscription which is used for ingestion.

If you’re running a locked‑down environment (no public network access), Purview expects private connectivity to that storage account as well.

So you do what you’d normally do:

  • Create private endpoints
  • Disable public access
  • Keep everything inside your virtual network

Sounds straightforward, but you don’t own this storage account and have no permissions over it. Therefore you can’t just deploy private endpoints, atleast not using the AzureRM provider.

If you try to create a Private Endpoint for the managed storage account using azurerm_private_endpoint, Terraform will fail every time. The AzureRM azurerm_private_endpoint always tries to auto‑approve the Private Endpoint connection.

That’s fine when:

  • You own the target resource
  • You control both ends of the connection

The fix: manual Private Link connections

The solution is not to fight Terraform, but to stop it auto‑approving in the first place. Instead, the Private Endpoint must be created in a pending state, so it can be approved later from within Purview itself. That’s done using:

manualPrivateLinkServiceConnections

And once again, this is where AzAPI is essential as AzureRM doesn’t expose this property. Using the Microsoft.Network/privateEndpoints in the AzAPI we can create the endpoints for both the blob and queue in your own virtual network against the storage account outputted by the Purview AzAPI call. You need to use the output as the Storage Account name is dynamically created, then post creation we can use the AzAPI to approve them.

Approving them is not the same as approving normal Private Endpoints as we don’t have the access, so instead we use the Purview Account API to trigger the action.

resource "azapi_resource" "purview_managed_private_endpoints" {
  for_each = { for pep_type in ["blob", "queue"] : pep_type => pep_type }

  type      = "Microsoft.Network/privateEndpoints@2025-03-01"
  name      = "pep-${each.value}-${azapi_resource.purview.name}"
  location  = azurerm_resource_group.purview.location
  parent_id = azurerm_resource_group.purview.id

  tags = local.tags

  body = {
    properties = {
      subnet = {
        id = module.subnets["pe"].subnet_ids
      }

      manualPrivateLinkServiceConnections = [
        {
          name = "pep-${azapi_resource.purview.name}-${each.value}"
          properties = {
            privateLinkServiceId = azapi_resource.purview.output.properties.ingestionStorage.id
            groupIds             = [each.value]
          }
        }
      ]
    }
  }
  depends_on = [azapi_resource.purview]
}

resource "azapi_resource_action" "approve_ingestion_private_endpoint" {
  for_each = azapi_resource.purview_managed_private_endpoints

  type        = "Microsoft.Purview/accounts@2024-04-01-preview"
  resource_id = azapi_resource.purview.id

  # REST action name from the API spec
  action = "ingestionPrivateEndpointConnectionStatus"

  method = "POST"

  body = {
    privateEndpointId = each.value.id
    status            = "Approved"
  }

  response_export_values = ["*"]
}

However, don’t forget to still deploy your private endpoints for the account to be accessible itself. For this one you can still use the AzureRM if you would like as you own all these resources.

resource "azurerm_private_endpoint" "purview" {
  for_each = { for pep_type in ["account", "platform", "portal"] : pep_type => pep_type }

  name                = "pep-${each.value}-${azapi_resource.purview.name}"
  resource_group_name = azurerm_resource_group.purview.name
  location            = azurerm_resource_group.purview.location
  subnet_id           = module.subnets["pe"].subnet_ids

  private_service_connection {
    name                              = "psc-${each.value}-${azapi_resource.purview.name}"
    is_manual_connection              = false
    private_connection_resource_id    = azapi_resource.purview.id
    private_connection_resource_alias = null
    subresource_names                 = [each.value]
    request_message                   = null
  }

  tags = local.tags

  lifecycle {
    ignore_changes = [tags["Deployment-date"], private_dns_zone_group]
  }

  depends_on = [azapi_resource.purview]
}

Published by Chris Pateman - PR Coder

A Digital Technical Lead, constantly learning and sharing the knowledge journey.

Leave a message please

This site uses Akismet to reduce spam. Learn how your comment data is processed.