Patterns & Practices - Merge App Settings with Bicep for Azure Functions and App Services

posted: May. 26, 2024 01:41 PM

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!

Glum Developer after destroying a production environment!

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:

1resource 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:

  1. Make changes to your Azure Function or App Service.
  2. Store your app settings in an object variable.
  3. Lookup the existing app settings for your Azure Function or App Service.
  4. Merge your initial app settings with the current app settings.
  5. 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.
2resource 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.
20var 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.
2var 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 ‼️
2resource 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')
2param siteName string
3
4@description('The app settings object to use for the update')
5param appSettings object
6
7resource siteResource 'Microsoft.Web/sites@2023-12-01' existing = {
8  name: siteName
9}
10
11resource 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.
2resource 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.
20var 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.
29var originalAppSettings = list('${functionAppResource.id}/config/appsettings', '2023-12-01').properties
30
31// 4. Merge your initial app settings with the current app settings.
32module 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!