Deploying Logic Apps Standard Without App Settings Secrets

Scenario Link to heading

Logic Apps Standard use Application Settings. By default these settings have secrets, particularly the key in the connection string for the storage account used to keep the code and the state and run history of all the workflows. In addition, if using Application Insights integration, there is the instrumentation key which according to some including the official documentation is not really a secret, still, something I don’t like there in plain text in the settings.

This is how it looks like after creating one using the portal, mid Oct 2024, in the East US region:

Default App Settings Deployed from the Portal

Solution Link to heading

With the recent announcement of support for managed identities in Logic Apps, combined with the existing support for Function Apps and Application Insights, I came up with a Bicep template which deploys the full underlying infrastructure without any secrets in App Settings:

App Settings Without Secrets

Here the storage account connection string used in the WEBSITE_CONTENTAZUREFILECONNECTIONSTRING setting is now a KeyVault reference, which is populated and refreshed by the platform using RBAC and a user managed identity assigned to the Logic App.

The earlier AzureWebjobsStorage setting, which also had the same connection string is no longer there, replaced by settings which rely on managed identities and RBAC to gain access to the storage account.

Finally, the instrumentation key for App Insights is now a reference to a KeyVault secret.

Challenges Link to heading

RBAC Propagation Link to heading

The first challenge is with RBAC propagation. Logic Apps need access to the storage account. This is verified during deployment, failing the deployment if, for instance, the permission in KeyVault for the Logic App to access the secret which contains the connection string is not there yet. I tried using dependsOn, outputs, modules, and while it worked every once in a while it was not consistent, every 2/3 times I was getting the error:

1Unable to resolve Azure Files Settings from Key Vault. Details: Unable to resolve setting: WEBSITE_CONTENTAZUREFILECONNECTIONSTRING with error: AccessToKeyVaultDenied.

Even directly in the Azure Portal, assigning the role to myself I had to refresh the page and wait for up to a minute to be able to query the secrets in KeyVault.

There is an application setting that allows the deployment to succeed by disabling the validation, but with this the file share is not created automatically, I tried creating it manually but failed to make it work.

Adding a two minutes wait via deployment scripts gave me a consistent experience. I have deployed this Bicep at least 4 times in new resource groups in different regions with consistent success.

Azure Roles Link to heading

This Bicep works fine as long as the identity used to run the deployment has the proper roles. I’m testing against an Azure subscription for which I’m the owner, however in a realistic scenario I won’t have that privilege. In a realistic corporate scenario the script will be run by Azure DevOps or GitHub actions. For those deployment pipelines to succeed, their identities need to be able to assign roles and run scripts in addition to the permissions to deploy the resources.

System assigned vs user assigned Link to heading

I prefer system assigned identities over user assigned. Simpler to setup, simpler to use and permissions deleted automatically when the parent resource is deleted. The problem here with system assigned is that the identity won’t be available until after the logic is deployed, but I need the identity in advance, to assign the RBAC in KeyVault. User assigned identities makes this feasible since the managed identity can be created independently of the resources which are going to use it.

The Bicep Templates Link to heading

secrets.bicep Link to heading

Since I struggled so much with RBAC propagation I ended up leaving this portion of the script on its own module, and a more maintainable and reusable solution will have more modules than this one.

The secrets module takes care of creating the secrets in KeyVault, in particular the storage account connection string and the Application Insights instrumentation Key. After that it gives RBAC permissions to the managed identity to read those secrets, then waits for 2 minutes.

The RBAC permissions are set at the individual secret level. While this enforces the minimal privilege principle, alternatively the permission can be set at the KeyVault level, allowing access to all the secrets, which will be much more convenient specially since in addition to these secrets there might be others used by the workflows.

 1param projectName string
 2param environmentName string
 3param locationName string
 4param storageAccountName string
 5param keyVaultName string
 6
 7// Azure Built-in roles Ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles
 8var keyVaultSecretsUserRoleDefinitionId = '4633458b-17de-408a-b874-0445c86b69e6'
 9
10resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
11  name: keyVaultName
12}
13
14resource storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' existing = {
15  name: storageAccountName
16}
17
18resource appInsights 'microsoft.insights/components@2020-02-02-preview' existing = {
19  name: 'appi-${projectName}-${environmentName}-${locationName}'
20}
21
22resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' existing = {
23  name: 'mi-${projectName}-${environmentName}-${locationName}'
24}
25
26@description('Adds the storage account connection string as a secret in KeyVault')
27resource storageAccountConnStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
28  name: '${storageAccount.name}-connectionString'
29  parent: keyVault
30  tags: {
31    ResourceType: 'StorageAccount'
32    ResourceName: storageAccount.name
33  }
34  properties: {
35    contentType: 'string'
36    value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${listKeys(storageAccount.id,'2019-06-01').keys[0].value};EndpointSuffix=core.windows.net'
37  }
38}
39
40@description('Gives permissions to the Managed Identity to retrieve the connection string from KeyVault')
41resource logicAppKeyVaultConnectionStringPermission 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
42  name: guid(keyVaultSecretsUserRoleDefinitionId, keyVault.id, managedIdentity.id, storageAccount.id)
43  scope: storageAccountConnStringSecret
44  properties: {
45    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretsUserRoleDefinitionId)
46    principalId: managedIdentity.properties.principalId
47  }
48}
49
50@description('Adds the App Insights Instrumentation Key as a secret in KeyVault')
51resource appInsightsSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
52  name: '${appInsights.name}-instrumentationKey'
53  parent: keyVault
54  tags: {
55    ResourceType: 'AppInsights'
56    ResourceName: appInsights.name
57  }
58  properties: {
59    contentType: 'string'
60    value: appInsights.properties.InstrumentationKey
61  }
62}
63
64@description('Gives permissions to the Managed Identity to retrieve the AppInsights Instrumentation Key from KeyVault')
65resource logicAppKeyVaultInstrumentationKeyPermission 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
66  name: guid(keyVaultSecretsUserRoleDefinitionId, keyVault.id, managedIdentity.id, appInsights.id)
67  scope: appInsightsSecret
68  properties: {
69    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretsUserRoleDefinitionId)
70    principalId: managedIdentity.properties.principalId
71  }
72}
73
74@description('Wait for 2 minutes to allow RBAC propagation to complete')
75resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
76  name: 'inlineCLI'
77  location: locationName
78  kind: 'AzurePowerShell'
79  properties: {
80    azPowerShellVersion: '12.2'
81    scriptContent: 'Start-Sleep -Seconds 120'
82    retentionInterval: 'PT1H'
83  }
84}

infrastructure.bicep Link to heading

This is the main script, which deploys all resources, configures application settings and setups RBAC roles for the storage account

  1param subscriptionId string = subscription().id
  2param projectName string = 'atlantis'
  3param environmentName string = 'dev'
  4param locationName string = resourceGroup().location
  5param storageAccountName string = 'st${projectName}${environmentName}${locationName}'
  6param hostingPlanName string = 'app-${projectName}-${environmentName}-${locationName}'
  7param logicAppName string = 'logic-${projectName}-${environmentName}-${locationName}'
  8param keyVaultName string = 'kv-${projectName}-${environmentName}-${locationName}'
  9var isProduction = environmentName == 'prod'
 10
 11// Azure Built-in roles Ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles
 12var storageAccountContributorRoleDefinitionId = '17d1049b-9a84-46fb-8f53-869881c3d3ab'
 13var storageBlobDataOwnerRoleDefinitionId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
 14var storageTableDataContributorRoleDefinitionId = '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'
 15var storageQueueDataContributorRoleDefinitionId = '974c5e8b-45b9-4653-ba55-5f855dd0fb88'
 16var monitoringMetricsPublisherRoleDefinitionId = '3913510d-42f4-4e42-8a64-420c390055eb'
 17
 18resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' = {
 19  name: 'mi-${projectName}-${environmentName}-${locationName}'
 20  location: locationName
 21}
 22
 23resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
 24  name: keyVaultName
 25  location: locationName
 26  properties: {
 27    sku: {
 28      name: 'standard'
 29      family: 'A'
 30    }
 31    tenantId: subscription().tenantId
 32    enableSoftDelete: isProduction
 33    enableRbacAuthorization: true
 34  }
 35}
 36
 37resource storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
 38  name: storageAccountName
 39  location: locationName
 40  tags: {}
 41  sku: {
 42    name: 'Standard_LRS'
 43  }
 44  properties: {
 45    supportsHttpsTrafficOnly: true
 46    minimumTlsVersion: 'TLS1_2'
 47    defaultToOAuthAuthentication: true
 48  }
 49  kind: 'StorageV2'
 50}
 51
 52module secrets 'secrets.bicep' = {
 53  name: 'secrets-${environmentName}'
 54  params: {
 55    projectName: projectName
 56    environmentName: environmentName
 57    locationName: locationName
 58    storageAccountName: storageAccountName
 59    keyVaultName: keyVaultName
 60  }
 61  dependsOn: [
 62    managedIdentity
 63    keyVault
 64    storageAccount
 65    appInsights
 66  ]
 67}
 68
 69resource hostingPlan 'Microsoft.Web/serverfarms@2022-09-01' = {
 70  name: hostingPlanName
 71  location: locationName
 72  kind: 'windows'
 73  sku: {
 74    name: 'WS1'
 75    tier: 'WorkflowStandard'
 76  }
 77}
 78
 79resource logicApp 'Microsoft.Web/sites@2022-09-01' = {
 80  name: logicAppName
 81  kind: 'functionapp,workflowapp'
 82  location: locationName
 83  tags: {
 84    'hidden-link: /app-insights-resource-id': '/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${logicAppName}'
 85  }
 86  properties: {
 87    publicNetworkAccess: 'Enabled'
 88    httpsOnly: true
 89    serverFarmId: hostingPlan.id
 90    keyVaultReferenceIdentity: managedIdentity.id
 91  }
 92  identity: {
 93    type: 'UserAssigned'
 94    userAssignedIdentities: {
 95      '${managedIdentity.id}': {}
 96    }
 97  }
 98}
 99
100resource logAnalyticsWS 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
101  name: 'log-${projectName}-${environmentName}-${locationName}'
102  location: locationName
103}
104
105resource appInsights 'microsoft.insights/components@2020-02-02-preview' = {
106  name: 'appi-${projectName}-${environmentName}-${locationName}'
107  location: locationName
108  kind: 'web'
109  properties: {
110    Request_Source: 'IbizaWebAppExtensionCreate'
111    Flow_Type: 'Redfield'
112    Application_Type: 'web'
113    DisableLocalAuth: true
114    WorkspaceResourceId: logAnalyticsWS.id
115  }
116}
117
118resource logicAppStorageAccountPermission 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
119  name: guid(storageAccountContributorRoleDefinitionId, keyVault.id, storageAccount.id, managedIdentity.id)
120  scope: storageAccount
121  properties: {
122    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', storageAccountContributorRoleDefinitionId)
123    principalId: managedIdentity.properties.principalId
124  }
125}
126
127resource logicAppStorageBlobPermission 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
128  name: guid(storageBlobDataOwnerRoleDefinitionId, keyVault.id, storageAccount.id, managedIdentity.id)
129  scope: storageAccount
130  properties: {
131    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataOwnerRoleDefinitionId)
132    principalId: managedIdentity.properties.principalId
133  }
134}
135
136resource logicAppStorageTablesPermission 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
137  name: guid(storageTableDataContributorRoleDefinitionId, keyVault.id, storageAccount.id, managedIdentity.id)
138  scope: storageAccount
139  properties: {
140    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', storageTableDataContributorRoleDefinitionId)
141    principalId: managedIdentity.properties.principalId
142  }
143}
144
145resource logicAppQueuePermission 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
146  name: guid(storageQueueDataContributorRoleDefinitionId, keyVault.id, storageAccount.id, managedIdentity.id)
147  scope: storageAccount
148  properties: {
149    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', storageQueueDataContributorRoleDefinitionId)
150    principalId: managedIdentity.properties.principalId
151  }
152}
153
154resource logicAppMonitoringPermission 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
155  name: guid(monitoringMetricsPublisherRoleDefinitionId, keyVault.id, appInsights.id, managedIdentity.id)
156  scope: appInsights
157  properties: {
158    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', monitoringMetricsPublisherRoleDefinitionId)
159    principalId: managedIdentity.properties.principalId
160  }
161}
162
163resource siteconfig 'Microsoft.Web/sites/config@2022-09-01' = {
164  parent: logicApp
165  name: 'web'
166  kind: 'string'
167  properties: {
168  appSettings: [
169    {
170      name: 'FUNCTIONS_EXTENSION_VERSION'
171      value: '~4'
172    }
173    {
174      name: 'FUNCTIONS_WORKER_RUNTIME'
175      value: 'node'
176    }
177    {
178      name: 'WEBSITE_NODE_DEFAULT_VERSION'
179      value: '~20'
180    }
181    {
182      name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
183      value: '@Microsoft.KeyVault(VaultName=${keyVaultName};SecretName=${appInsights.name}-instrumentationKey)'
184    }
185    {
186      name: 'APPLICATIONINSIGHTS_AUTHENTICATION_STRING'
187      value: 'Authorization=AAD;ClientId=${managedIdentity.properties.clientId}'
188    }
189    {
190      name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
191      value: '@Microsoft.KeyVault(VaultName=${keyVaultName};SecretName=${storageAccount.name}-connectionString)'
192    }
193    {
194      name: 'WEBSITE_CONTENTSHARE'
195      value: '${toLower(storageAccountName)}-contentshare'
196    }
197    {
198      name: 'AzureFunctionsJobHost__extensionBundle__id'
199      value: 'Microsoft.Azure.Functions.ExtensionBundle.Workflows'
200    }
201    {
202      name: 'AzureFunctionsJobHost__extensionBundle__version'
203      value: '[1.*, 2.0.0)'
204    }
205    {
206      name: 'APP_KIND'
207      value: 'workflowApp'
208    }
209    {
210      name:'AzureWebJobsStorage__blobServiceUri'
211      value: storageAccount.properties.primaryEndpoints.blob
212    }
213    {
214      name:'AzureWebJobsStorage__queueServiceUri'
215      value: storageAccount.properties.primaryEndpoints.queue
216    }
217    {
218      name:'AzureWebJobsStorage__tableServiceUri'
219      value: storageAccount.properties.primaryEndpoints.table
220    }
221    {
222      name:'AzureWebJobsStorage__credential'
223      value: 'managedIdentity'
224    }
225    {
226      name:'AzureWebJobsStorage__managedIdentityResourceId'
227      value: managedIdentity.id
228    }
229  ]
230  }
231  dependsOn: [
232    secrets
233  ]
234}

Outcome Link to heading

The script runs without errors or warnings, and the infrastructure is now available.

Infrastructure deployed

Workflow Test Link to heading

While the fact that the deployment worked and the logic app is able to show the runtime version without error notifications is a good sign, nothing like a simple test, so I created the simplest workflow then verified run history with per action state.

Simple workflow state and history

Application Insights Link to heading

Telemetry is being captured by Application Insights.

Telemetry in app insights