Logic apps API connections with managed identities in Bicep

If you've ever tried to provision a Logic App (Consumption) end‑to‑end with an API connection for Azure Tables wired to a managed identity, you probably noticed the documentation is – let's say – thin. You quickly end up reverse engineering exported ARM templates or clicking in the portal to see what gets generated. This post documents a repeatable Bicep approach (user‑assigned managed identity + Azure Tables connection) and why (for now) AI is still of limited help for these integration cases.

Why this post (and why AI did not just write it for me)

I like to lean on AI for boring boilerplate. For integration scenarios though, AI still regularly hallucinates subtle but critical details: wrong resource types, outdated API versions, or non‑existent properties inside a Logic App $connections parameter. The training data for niche integration Infrastructure‑as‑Code (IaC) patterns is simply smaller than, say, generic web API samples or CRUD React apps. The result? Confidently wrong suggestions. I had ChatGPT and other models produce:

  • authType: ManagedIdentity while the platform expects ManagedServiceIdentity.
  • A managedIdentity object inside the Microsoft.Web/connections resource (doesn't exist for this scenario; the logic app's own identity is used).
  • Incorrect api.id formats (/providers/Microsoft.Web/locations/managedApis/... missing the subscription context) or hardcoded location mismatches.
  • Mixing Standard (single tenant) and Consumption (multi tenant) concepts (e.g. suggesting built‑in connectors that only apply to Standard).

So we (still) document. Here's a concise working pattern you can adapt.

Scenario

We provision:

  • A user‑assigned managed identity (typically you'd reuse an existing one; here we create it for completeness).
  • A Storage account plus an Azure Table (data plane resource) to hold run log entities.
  • RBAC role assignment granting the identity Storage Table Data Contributor on the storage account.
  • An Azure Tables API connection (azuretables) configured for Managed Identity, using the parameter value set style the portal emits today.
  • A Logic App (Consumption) referencing that connection via $connections with an explicit connectionProperties.authentication.identity reference to the user‑assigned identity.

All in one Bicep deployment, idempotent, no portal clicks.

High‑level steps

  1. Define parameters (company, environment, location, names).
  2. Create / reference user‑assigned managed identity.
  3. Create storage account + table.
  4. Assign Storage Table Data Contributor role to the identity.
  5. Create Azure Tables connection (Microsoft.Web/connections) using the managed identity parameter value set.
  6. Deploy workflow referencing connection + adding authentication block under $connections.
  7. (Optional) Add outputs or more tables.

Bicep walkthrough

Below is a condensed version of the relevant parts. (Your full file also injects the Logic App definition from a JSON file – a good practice to keep workflow JSON separate.) Focus on: user‑assigned identity creation, role assignment, connection, $connections parameter with authentication block.

The artifacts can be found on github

targetScope = 'resourceGroup'

param companyName string = 'Didago'
@allowed([ 'dev','tst','acc','prd' ])
param environmentName string
param location string = 'westeurope'
param logicAppName string = 'Demo-blog-la-${environmentName}-v001'

// User Assigned Managed Identity (would usually pre-exist)
param userManagedIdentityRg string = '${toLower(companyName)}-blog-rg-${toLower(environmentName)}'
param userManagedIdentityName string = '${toLower(companyName)}-blog-mi-${toLower(environmentName)}'

// Storage dependencies
param logStorageAccountName string = '${toLower(companyName)}log${substring(uniqueString(resourceGroup().id), 0, 10)}${toLower(environmentName)}'
var azureTablesConnectionName = 'azuretables'

// Identity
resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = {
	name: userManagedIdentityName
	location: location
}

// Storage + table
resource logStorageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = {
	name: logStorageAccountName
	location: location
	sku: { name: 'Standard_LRS' }
	kind: 'StorageV2'
	properties: { supportsHttpsTrafficOnly: true }
}
resource runLogsTable 'Microsoft.Storage/storageAccounts/tableServices/tables@2025-01-01' = {
	name: '${logStorageAccount.name}/default/RunLogs'
}

// RBAC for tables (Storage Table Data Contributor)
resource tableContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
	scope: logStorageAccount
	name: guid(userAssignedIdentity.id, logStorageAccount.id, 'table-contributor')
	properties: {
		roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
		principalId: userAssignedIdentity.properties.principalId
		principalType: 'ServicePrincipal'
	}
}

// Azure Tables connection (managed identity flavor)
resource azureTablesConnection 'Microsoft.Web/connections@2016-06-01' = {
	name: azureTablesConnectionName
	location: location
	properties: {
		displayName: azureTablesConnectionName
		api: {
			id: subscriptionResourceId('Microsoft.Web/locations/managedApis', location, 'azuretables')
		}
		// Newer pattern uses parameterValueSet instead of parameterValues + authType
		parameterValueSet: {
			name: 'managedIdentityAuth'
			values: {}
		}
	}
}

// Logic App (user-assigned identity & $connections auth block)
resource logicApp 'Microsoft.Logic/workflows@2019-05-01' = {
	name: logicAppName
	location: location
	identity: {
		type: 'UserAssigned'
		userAssignedIdentities: { '${userAssignedIdentity.id}': {} }
	}
  properties: {
    definition: json(loadTextContent('../../src/logic-app-1/workflow.json')).definition
    parameters: union(
      json(loadTextContent('../../src/logic-app-1/workflow.json')).parameters,
      {
        LogStorageAccountName: {
          value: logStorageAccountName
        }
        '$connections': {
          value: {
            azuretables: {
              connectionId: azureTablesConnection.id
              connectionName: azureTablesConnection.name
              id: subscriptionResourceId('Microsoft.Web/locations/managedApis', location, 'azuretables')
              connectionProperties: {
                authentication: {
                  type: 'ManagedServiceIdentity'
                  identity: resourceId(userManagedIdentityRg, 'Microsoft.ManagedIdentity/userAssignedIdentities', userManagedIdentityName)
                }
              }
            }
          }
        }
      }
    )
  }
}

Key points:

  • Two parallel patterns exist: older parameterValues.authType = ManagedServiceIdentity and newer parameterValueSet + an auth block under $connections → This connector currently needs the auth in $connections for user‑assigned scenarios.
  • The identity reference inside connectionProperties.authentication.identity must use the resource scope RG of the identity (can differ from the Logic App RG in cross‑RG reuse scenarios).

Switching to a system-assigned identity? Replace the identity block on the workflow with { type: 'SystemAssigned' }, drop connectionProperties.authentication.identity (still accepted but optional), and change the role assignment principal ID accordingly. The connection resource remains unchanged.

You cannot switch an existing connection from key based to MI in place; delete + recreate if you must change auth method.

Where AI tripped up for me

Concrete misfires I saw:

  1. Replaced the modern parameterValueSet with an authType property only (worked for some connectors, failed here with user‑assigned identity).
  2. Suggested adding a managedIdentity object inside the Microsoft.Web/connections resource (non‑existent; identity context comes from workflow at runtime or $connections auth block).
  3. Produced a Standard plan pattern (connectionRuntimeUrl) instead of the Consumption pattern using host.connection.name.
  4. Gave the wrong role GUID (Queue / Blob contributor) which compiles but lacks Table Data permissions → 403 at runtime.

They look plausible, but each is just “off” enough to cost you time.

Final thoughts

Integration IaC still benefits from precise, curated examples. AI accelerates drafting but its weaker training signal for connector internals (like the subtle $connections auth structure) means you still need exported templates, schema docs – or posts like this. Until models ingest more quality integration artifacts, documenting patterns like user‑assigned MI + Azure Tables keeps teams moving. Spot something off or want a Blob / Queue / Key Vault variant? Let me know. Happy provisioning!