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:
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:
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.
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.
Application Insights Link to heading
Telemetry is being captured by Application Insights.