
Artwork: Self-Portrait by Rembrandt. The Metropolitan Museum of Art · Public domain
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:
azapi_resourceto create the API connection with Managed Identity auth, since the native resource can’t.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
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:
- The workflow declares
"identity": { "type": "SystemAssigned" }. - The
$connectionsparameter setsconnectionProperties.authentication.type = "ManagedServiceIdentity".
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:
schema_validation_enabled = falseon theazapi_resourceis not optional — the connection body doesn’t match the provider’s strict schema, and validation will reject a perfectly validmanagedIdentityAuthconnection otherwise.- Order matters. The role assignment needs the workflow’s
principal_id, which only exists after the ARM deployment creates the system-assigned identity. Let Terraform’s dependency graph sequence it, or you’ll get a null principal. - RBAC is eventually consistent. A fresh role assignment can take a minute or two to propagate;
a Logic App run fired immediately after
applymay 403 before the grant lands. - Match the connection name. The
$connectionskey in the ARM template (azurequeues-1) must match what the action references, or the trigger silently fails to bind.
Takeaways
- When
azurermlacks a surface,azapi_resource+ ARM template deployment keeps you in Terraform instead of resorting to manual steps. parameterValueSet = "managedIdentityAuth"is the single line that flips a connection to Managed Identity.- Wire the identity in three places: the workflow
identityblock, the connection’sauthentication.type, and the RBAC role assignment.
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.