Self-Portrait by Rembrandt

Artwork: Self-Portrait by Rembrandt. The Metropolitan Museum of Art · Public domain

Azure

Managed Identity on Logic App Consumption with Terraform

On a recent project we needed an Azure Logic App Consumption workflow that securely connects to Azure Storage using Managed Identity — no hardcoded credentials — while staying entirely in Terraform. The catch: Terraform doesn’t natively support configuring API connections with Managed Identity for Logic App Consumption workflows. This is how we worked around it.

The problem

Managed Identity is the obvious choice for Logic App → Storage authentication: Azure handles the token exchange, and there are no secrets to rotate or leak. But there’s a documented gap in the azurerm provider — it can’t directly configure an API connection with Managed Identity for Consumption workflows.

That left us needing a path that keeps everything in IaC without falling back to portal clicks.

The approach

Two escape hatches make this work:

  1. azapi_resource to create the API connection with Managed Identity auth, since the native resource can’t.
  2. azurerm_resource_group_template_deployment (an ARM template) to deploy the Logic App itself with the connection wired in.

Around those, the usual supporting cast: resource group, storage account, queue, and a role assignment.

Weighed against the alternatives, this is the only fully-automated, credential-free path:

Approach Managed Identity? Fully automated? Notes
Portal / click-ops Yes No Drifts from IaC; not reproducible
azurerm_api_connection No Yes No MI support for Consumption connections
Access keys in Terraform Connection only Yes Secrets to store and rotate
azapi_resource + ARM Yes Yes This post — no secrets, all in Terraform

Implementation

Hand-drawn three-step flow showing the Logic App system-assigned identity, the API connection configured for managed identity, and the Storage RBAC assignment for the same principal.

1. API connection with Managed Identity

The key is parameterValueSet.name = "managedIdentityAuth". Setting it enforces Managed Identity on the connection:

resource "azapi_resource" "create_api_connection" {
  type                      = "Microsoft.Web/connections@2016-06-01"
  name                      = "storage-queue-connection"
  location                  = azurerm_resource_group.rg.location
  parent_id                 = azurerm_resource_group.rg.id
  schema_validation_enabled = false

  body = jsonencode({
    properties = {
      displayName = "Queue"
      parameterValueSet = {
        name  = "managedIdentityAuth" # enforces Managed Identity on the connection
        value = {}
      }
      api = {
        name        = "azurequeues"
        displayName = "Azure Queues"
        id          = "/subscriptions/${data.azurerm_subscription.current.subscription_id}/providers/Microsoft.Web/locations/${azurerm_resource_group.rg.location}/managedApis/azurequeues"
      }
    }
  })
}

2. Deploy the Logic App via ARM template

Terraform can’t express the full Logic App body with a Managed Identity connection, so we hand that to an ARM template deployment. Two details make the identity flow work end to end:

data "template_file" "workflow" {
  template = file(var.arm_file_path)
}

resource "azurerm_resource_group_template_deployment" "logic_app_workflow_deployment" {
  deployment_mode     = "Incremental"
  name                = "workflow_deployment"
  resource_group_name = azurerm_resource_group.rg.name
  parameters_content = jsonencode({
    logic_app_name           = var.logic_app_name
    location                 = var.location
    azurequeue_connection_id = "/subscriptions/${var.subscription_id}/resourceGroups/${azurerm_resource_group.rg.name}/providers/Microsoft.Web/connections/storage-queue-connection"
    managed_api_id           = "/subscriptions/${var.subscription_id}/providers/Microsoft.Web/locations/${azurerm_resource_group.rg.location}/managedApis/azurequeues"
  })
  template_content = data.template_file.workflow.template
}

3. Grant the identity access

The Logic App’s system-assigned identity needs a role on the storage account. For queue writes, that’s Storage Queue Data Contributor:

resource "azurerm_role_assignment" "logic_app_contributor" {
  scope                = azurerm_storage_account.storage.id
  role_definition_name = "Storage Queue Data Contributor"
  principal_id         = data.azurerm_logic_app_workflow.logic_app.identity[0].principal_id
}

4. Extract the callback URL (optional)

Because we deployed via ARM rather than a native Terraform trigger, the trigger’s callback URL isn’t a Terraform output. Pull it with an azapi_resource_action:

data "azapi_resource_action" "callback_url_data" {
  type        = "Microsoft.Logic/workflows/triggers@2019-05-01"
  action      = "listCallbackUrl"
  resource_id = "/subscriptions/${data.azurerm_subscription.current.subscription_id}/resourceGroups/${azurerm_resource_group.rg.name}/providers/Microsoft.Logic/workflows/${local.logic_app_name}/triggers/${local.trigger_name}"
}

Gotchas worth knowing

A few things cost us time so they don’t have to cost you:

Takeaways

The result is a scalable, credential-free setup that stays fully automated — no secrets, no portal drift.

Note: the article’s illustration was generated with Bing Image Creator.

#terraform#logic-apps#managed-identity#azure#iac