Patterns & Practices - Merge App Settings with Bicep for Azure Functions and App Services
If you are anything like me, you become glum when you run your bicep script and your Azure Function goes down because you lost the WEBSITE_RUN_FROM_PACKAGE
app setting. Oh, sad day, and possibly angry customers when your application stops working!
The problem is that we want our bicep scripts to be idempotent. We want to be able to run them anytime without adversely affecting our running environment.
If you are setting up your bicep script to deploy an Azure Function or App Service, you will come across sample code like the snippet below:
1 resource functionAppResource 'Microsoft.Web/sites@2023-01-01' = {
2 name: 'functionAppName'
3 location: location
4 kind: 'functionapp,linux'
5 identity: {
6 type: identityType
7 }
8 properties: {
9 reserved: true
10 serverFarmId: hostingPlan.id
11 siteConfig: {
12 linuxFxVersion: linuxFxVersion
13 appSettings: [
14 {
15 name: 'AzureWebJobsStorage'
16 value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
17 }
18 {
19 name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
20 value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
21 }
22 {
23 name: 'WEBSITE_CONTENTSHARE'
24 value: toLower(functionAppName)
25 }
26 {
27 name: 'FUNCTIONS_EXTENSION_VERSION'
28 value: '~4'
29 }
30 {
31 name: 'FUNCTIONS_WORKER_RUNTIME'
32 value: functionWorkerRuntime
33 }
34 ]
35 }
36 }
37 tags: groupTags
38 }
While it is true, the above sample will help you to create your Azure Function. The problem with the code above is that including appSettings
inside the siteConfig
will overwrite all the current app settings defined in your running Azure Function. You will lose the much loved (and required) WEBSITE_RUN_FROM_PACKAGE
value!
The Fix for Azure Functions and App Services🔗
The way to avoid this situation and to generally live a more cheerful life is to split apart the creation of the Azure Function from the definition of the app settings:
- Make changes to your Azure Function or App Service.
- Store your app settings in an object variable.
- Lookup the existing app settings for your Azure Function or App Service.
- Merge your initial app settings with the current app settings.
- Instantly become more attractive and loved among your peers.
Modify the initial creation of your Azure function by removing the app settings from the definition and store them in an object variable.
1 // 1. Make changes to your Azure Function or App Service.
2 resource functionAppResource 'Microsoft.Web/sites@2023-01-01' = {
3 name: 'functionAppName'
4 location: location
5 kind: 'functionapp,linux'
6 identity: {
7 type: identityType
8 }
9 properties: {
10 reserved: true
11 serverFarmId: hostingPlan.id
12 siteConfig: {
13 linuxFxVersion: linuxFxVersion
14 }
15 }
16 tags: groupTags
17 }
18
19 // 2. Store your app settings in an object variable.
20 var defaultAppSettings = {
21 AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
22 WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
23 WEBSITE_CONTENTSHARE: toLower(functionAppName)
24 FUNCTIONS_EXTENSION_VERSION: '~4'
25 FUNCTIONS_WORKER_RUNTIME: functionWorkerRuntime
26 }
Now, we need to get the existing App Settings for the function. On the first creation, the existing app settings will be empty, but after deploying the function code once, they will contain our much adored WEBSITE_RUN_FROM_PACKAGE
setting.
1 // 3. Lookup the existing app settings for your Azure Function or App Service.
2 var originalAppSettings = list('${functionAppResource.id}/config/appsettings', '2023-12-01').properties
(Note: Maybe it was a dumb mistake on my part, which is common, but when I tried to use the resource ... existing ={}
syntax, the app settings did not load. The above list
function was the only way that worked for me.)
So, what now? Well, we want to create our app settings merged together with the existing values. Something like the following:
1 // 4. Merge your initial app settings with the current app settings. ‼️ Does not work ‼️
2 resource functionConfigResource 'Microsoft.Web/sites/config@2023-12-01' = {
3 name: 'appsettings'
4 parent: functionAppResource
5 properties: union(originalAppSettings, defaultAppSettings)
6 }
If we add the above snippet to the same bicep file, we will get a circular error because life is never easy. It is a rule of thumb in application development that there is always one more error and hurdle that you must jump to get things to work correctly.
So the approach that works is to put the app settings code into its own bicep script to be consumed as a module. We will call this new file site-app-settings.bicep
Since the code will work for both an Azure Function and an App Service.
1 @description('Modify the app settings of this site resource')
2 param siteName string
3
4 @description('The app settings object to use for the update')
5 param appSettings object
6
7 resource siteResource 'Microsoft.Web/sites@2023-12-01' existing = {
8 name: siteName
9 }
10
11 resource functionConfigResource 'Microsoft.Web/sites/config@2023-12-01' = {
12 name: 'appsettings'
13 parent: siteResource
14 properties: appSettings
15 }
Pulling it all together🔗
With the site-app-settings.bicep
module in place we can finally bring it all together without creating any circular dependencies! Let's see what that looks like, shall we?
1 // 1. Make changes to your Azure Function or App Service.
2 resource functionAppResource 'Microsoft.Web/sites@2023-01-01' = {
3 name: 'functionAppName'
4 location: location
5 kind: 'functionapp,linux'
6 identity: {
7 type: identityType
8 }
9 properties: {
10 reserved: true
11 serverFarmId: hostingPlan.id
12 siteConfig: {
13 linuxFxVersion: linuxFxVersion
14 }
15 }
16 tags: groupTags
17 }
18
19 // 2. Store your app settings in an object variable.
20 var defaultAppSettings = {
21 AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
22 WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
23 WEBSITE_CONTENTSHARE: toLower(functionAppName)
24 FUNCTIONS_EXTENSION_VERSION: '~4'
25 FUNCTIONS_WORKER_RUNTIME: functionWorkerRuntime
26 }
27
28 // 3. Lookup the existing app settings for your Azure Function or App Service.
29 var originalAppSettings = list('${functionAppResource.id}/config/appsettings', '2023-12-01').properties
30
31 // 4. Merge your initial app settings with the current app settings.
32 module functionAppSettingsModule 'site-app-settings.bicep' = {
33 name: '${functionAppResource.name}-appsettings_deployment'
34 params: {
35 siteName: functionAppResource.name
36 appSettings: union(originalAppSettings, defaultAppSettings)
37 }
38 }
39
40 // 5. Instantly become more attractive and loved among your peers.
Conclusion🔗
With the above pattern and practice we can now run our bicep file after our Azure Function has been deployed and not destroy the WEBSITE_RUN_FROM_PACKAGE
app setting key that gets created once we deploy our Azure Function through a CI/CD pipeline like a GitHub Action. I also want to note, that any app settings that you may have created outside of a bicep file will also be retained. Happy Day!
I hope you enjoyed this article and if you have any questions feel free to join my discord server and ask away!
I also stream on twitch, sometimes I'm coding to the void, other times I am gaming to the void!