From 07a059307535995bdf22a385dfebf224d3ce34ea Mon Sep 17 00:00:00 2001 From: Peter Budai Date: Fri, 20 Sep 2024 09:35:15 +0200 Subject: [PATCH] feat: Retain existing settings during deployment - `avm/res/web/site` (#3311) ## Description This commit changes the app settings deployment in order to retain existing app settings that are not defined in the Bicep file. This change allows for updating **only** the app settings that are defined in the Bicep file, while leaving the rest unchanged. As this is a change in behavior, version number has been increased. Fixes #949 ## Pipeline Reference | Pipeline | | -------- | | [![avm.res.web.site](https://github.com/peterbud/bicep-registry-modules/actions/workflows/avm.res.web.site.yml/badge.svg)](https://github.com/peterbud/bicep-registry-modules/actions/workflows/avm.res.web.site.yml) | ## Type of Change - [ ] Update to CI Environment or utilities (Non-module affecting changes) - [ ] Azure Verified Module updates: - [ ] Bugfix containing backwards-compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `version.json`: - [ ] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [ ] The bug was found by the module author, and no one has opened an issue to report it yet. - [x] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `version.json`. - [ ] Breaking changes and I have bumped the MAJOR version in `version.json`. - [ ] Update to documentation ## Checklist - [x] I'm sure there are no other open Pull Requests for the same update/change - [x] I have run `Set-AVMModule` locally to generate the supporting module files. - [x] My corresponding pipelines / checks run clean and green without any errors or warnings --- avm/res/web/site/README.md | 108 +++++++++++++++--- .../web/site/config--appsettings/README.md | 11 +- .../web/site/config--appsettings/main.bicep | 16 ++- .../web/site/config--appsettings/main.json | 17 ++- avm/res/web/site/main.bicep | 3 +- avm/res/web/site/main.json | 44 ++++--- .../site/slot/config--appsettings/README.md | 9 ++ .../site/slot/config--appsettings/main.bicep | 10 +- .../site/slot/config--appsettings/main.json | 11 +- avm/res/web/site/slot/main.bicep | 1 + avm/res/web/site/slot/main.json | 16 ++- .../functionApp.settings/dependencies.bicep | 21 ++++ .../e2e/functionApp.settings/main.test.bicep | 67 +++++++++++ avm/res/web/site/version.json | 2 +- 14 files changed, 284 insertions(+), 52 deletions(-) create mode 100644 avm/res/web/site/tests/e2e/functionApp.settings/dependencies.bicep create mode 100644 avm/res/web/site/tests/e2e/functionApp.settings/main.test.bicep diff --git a/avm/res/web/site/README.md b/avm/res/web/site/README.md index bc9c390cb6..12c8a2371f 100644 --- a/avm/res/web/site/README.md +++ b/avm/res/web/site/README.md @@ -21,8 +21,9 @@ This module deploys a Web or Function App. | `Microsoft.Insights/diagnosticSettings` | [2021-05-01-preview](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Insights/2021-05-01-preview/diagnosticSettings) | | `Microsoft.Network/privateEndpoints` | [2023-11-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Network/2023-11-01/privateEndpoints) | | `Microsoft.Network/privateEndpoints/privateDnsZoneGroups` | [2023-11-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Network/2023-11-01/privateEndpoints/privateDnsZoneGroups) | -| `Microsoft.Web/sites` | [2022-09-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Web/2022-09-01/sites) | +| `Microsoft.Web/sites` | [2023-12-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Web/sites) | | `Microsoft.Web/sites/basicPublishingCredentialsPolicies` | [2022-09-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Web/sites) | +| `Microsoft.Web/sites/config` | [2023-12-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Web/sites) | | `Microsoft.Web/sites/config` | [2022-09-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Web/sites) | | `Microsoft.Web/sites/extensions` | [2023-12-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Web/sites/extensions) | | `Microsoft.Web/sites/hybridConnectionNamespaces/relays` | [2022-09-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Web/2022-09-01/sites/hybridConnectionNamespaces/relays) | @@ -41,15 +42,16 @@ The following section provides usage examples for the module, which were used to - [Function App, using only defaults](#example-1-function-app-using-only-defaults) - [Function App, using large parameter set](#example-2-function-app-using-large-parameter-set) -- [Web App, using only defaults](#example-3-web-app-using-only-defaults) -- [Web App](#example-4-web-app) -- [WAF-aligned](#example-5-waf-aligned) -- [Web App, using only defaults](#example-6-web-app-using-only-defaults) -- [Web App, using large parameter set](#example-7-web-app-using-large-parameter-set) -- [Web App, using only defaults](#example-8-web-app-using-only-defaults) -- [Web App, using large parameter set](#example-9-web-app-using-large-parameter-set) -- [Web App](#example-10-web-app) -- [Windows Web App for Containers, using only defaults](#example-11-windows-web-app-for-containers-using-only-defaults) +- [Function App, using only defaults](#example-3-function-app-using-only-defaults) +- [Web App, using only defaults](#example-4-web-app-using-only-defaults) +- [Web App](#example-5-web-app) +- [WAF-aligned](#example-6-waf-aligned) +- [Web App, using only defaults](#example-7-web-app-using-only-defaults) +- [Web App, using large parameter set](#example-8-web-app-using-large-parameter-set) +- [Web App, using only defaults](#example-9-web-app-using-only-defaults) +- [Web App, using large parameter set](#example-10-web-app-using-large-parameter-set) +- [Web App](#example-11-web-app) +- [Windows Web App for Containers, using only defaults](#example-12-windows-web-app-for-containers-using-only-defaults) ### Example 1: _Function App, using only defaults_ @@ -515,7 +517,75 @@ module site 'br/public:avm/res/web/site:' = {

-### Example 3: _Web App, using only defaults_ +### Example 3: _Function App, using only defaults_ + +This instance deploys the module as Function App with the minimum set of required parameters. + + +

+ +via Bicep module + +```bicep +module site 'br/public:avm/res/web/site:' = { + name: 'siteDeployment' + params: { + // Required parameters + kind: 'functionapp' + name: 'wsfaset001' + serverFarmResourceId: '' + // Non-required parameters + appSettingsKeyValuePairs: { + AzureFunctionsJobHost__logging__logLevel__default: 'Trace' + FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_WORKER_RUNTIME: 'dotnet' + } + location: '' + } +} +``` + +
+

+ +

+ +via JSON Parameter file + +```json +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Required parameters + "kind": { + "value": "functionapp" + }, + "name": { + "value": "wsfaset001" + }, + "serverFarmResourceId": { + "value": "" + }, + // Non-required parameters + "appSettingsKeyValuePairs": { + "value": { + "AzureFunctionsJobHost__logging__logLevel__default": "Trace", + "FUNCTIONS_EXTENSION_VERSION": "~4", + "FUNCTIONS_WORKER_RUNTIME": "dotnet" + } + }, + "location": { + "value": "" + } + } +} +``` + +
+

+ +### Example 4: _Web App, using only defaults_ This instance deploys the module as a Linux Web App with the minimum set of required parameters. @@ -591,7 +661,7 @@ module site 'br/public:avm/res/web/site:' = {

-### Example 4: _Web App_ +### Example 5: _Web App_ This instance deploys the module as Web App with the set of logs configuration. @@ -723,7 +793,7 @@ module site 'br/public:avm/res/web/site:' = {

-### Example 5: _WAF-aligned_ +### Example 6: _WAF-aligned_ This instance deploys the module in alignment with the best-practices of the Azure Well-Architected Framework. @@ -865,7 +935,7 @@ module site 'br/public:avm/res/web/site:' = {

-### Example 6: _Web App, using only defaults_ +### Example 7: _Web App, using only defaults_ This instance deploys the module as Web App with the minimum set of required parameters. @@ -921,7 +991,7 @@ module site 'br/public:avm/res/web/site:' = {

-### Example 7: _Web App, using large parameter set_ +### Example 8: _Web App, using large parameter set_ This instance deploys the module as Web App with most of its features enabled. @@ -1389,7 +1459,7 @@ module site 'br/public:avm/res/web/site:' = {

-### Example 8: _Web App, using only defaults_ +### Example 9: _Web App, using only defaults_ This instance deploys the module as a Linux Web App with the minimum set of required parameters. @@ -1445,7 +1515,7 @@ module site 'br/public:avm/res/web/site:' = {

-### Example 9: _Web App, using large parameter set_ +### Example 10: _Web App, using large parameter set_ This instance deploys the module asa Linux Web App with most of its features enabled. @@ -1907,7 +1977,7 @@ module site 'br/public:avm/res/web/site:' = {

-### Example 10: _Web App_ +### Example 11: _Web App_ This instance deploys the module as Web App with the set of api management configuration. @@ -2003,7 +2073,7 @@ module site 'br/public:avm/res/web/site:' = {

-### Example 11: _Windows Web App for Containers, using only defaults_ +### Example 12: _Windows Web App for Containers, using only defaults_ This instance deploys the module as a Windows based Container Web App with the minimum set of required parameters. diff --git a/avm/res/web/site/config--appsettings/README.md b/avm/res/web/site/config--appsettings/README.md index 17ff462236..db4a05b7d7 100644 --- a/avm/res/web/site/config--appsettings/README.md +++ b/avm/res/web/site/config--appsettings/README.md @@ -13,7 +13,7 @@ This module deploys a Site App Setting. | Resource Type | API Version | | :-- | :-- | -| `Microsoft.Web/sites/config` | [2022-09-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Web/sites) | +| `Microsoft.Web/sites/config` | [2023-12-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Web/sites) | ## Parameters @@ -35,6 +35,7 @@ This module deploys a Site App Setting. | :-- | :-- | :-- | | [`appInsightResourceId`](#parameter-appinsightresourceid) | string | Resource ID of the app insight to leverage for this resource. | | [`appSettingsKeyValuePairs`](#parameter-appsettingskeyvaluepairs) | object | The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING. | +| [`currentAppSettings`](#parameter-currentappsettings) | object | The current app settings. | | [`storageAccountResourceId`](#parameter-storageaccountresourceid) | string | Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions. | | [`storageAccountUseIdentityAuthentication`](#parameter-storageaccountuseidentityauthentication) | bool | If the provided storage account requires Identity based authentication ('allowSharedKeyAccess' is set to false). When set to true, the minimum role assignment required for the App Service Managed Identity to the storage account is 'Storage Blob Data Owner'. | @@ -83,6 +84,14 @@ The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDas - Required: No - Type: object +### Parameter: `currentAppSettings` + +The current app settings. + +- Required: No +- Type: object +- Default: `{}` + ### Parameter: `storageAccountResourceId` Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions. diff --git a/avm/res/web/site/config--appsettings/main.bicep b/avm/res/web/site/config--appsettings/main.bicep index 2f770a7774..202a2ac205 100644 --- a/avm/res/web/site/config--appsettings/main.bicep +++ b/avm/res/web/site/config--appsettings/main.bicep @@ -34,6 +34,9 @@ param appInsightResourceId string? @description('Optional. The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING.') param appSettingsKeyValuePairs object? +@description('Optional. The current app settings.') +param currentAppSettings object = {} + var azureWebJobsValues = !empty(storageAccountResourceId) && !(storageAccountUseIdentityAuthentication) ? { AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' @@ -51,9 +54,14 @@ var appInsightsValues = !empty(appInsightResourceId) } : {} -var expandedAppSettings = union(appSettingsKeyValuePairs ?? {}, azureWebJobsValues, appInsightsValues) +var expandedAppSettings = union( + currentAppSettings ?? {}, + appSettingsKeyValuePairs ?? {}, + azureWebJobsValues, + appInsightsValues +) -resource app 'Microsoft.Web/sites@2022-09-01' existing = { +resource app 'Microsoft.Web/sites@2023-12-01' existing = { name: appName } @@ -62,7 +70,7 @@ resource appInsight 'Microsoft.Insights/components@2020-02-02' existing = if (!e scope: resourceGroup(split(appInsightResourceId ?? '//', '/')[2], split(appInsightResourceId ?? '////', '/')[4]) } -resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = if (!empty(storageAccountResourceId)) { +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = if (!empty(storageAccountResourceId)) { name: last(split(storageAccountResourceId ?? 'dummyName', '/')) scope: resourceGroup( split(storageAccountResourceId ?? '//', '/')[2], @@ -70,7 +78,7 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing ) } -resource appSettings 'Microsoft.Web/sites/config@2022-09-01' = { +resource appSettings 'Microsoft.Web/sites/config@2023-12-01' = { name: 'appsettings' kind: kind parent: app diff --git a/avm/res/web/site/config--appsettings/main.json b/avm/res/web/site/config--appsettings/main.json index c59a554e2a..c50105f501 100644 --- a/avm/res/web/site/config--appsettings/main.json +++ b/avm/res/web/site/config--appsettings/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.29.47.4906", - "templateHash": "8777070640548664577" + "templateHash": "3998275265127709875" }, "name": "Site App Settings", "description": "This module deploys a Site App Setting.", @@ -66,13 +66,20 @@ "metadata": { "description": "Optional. The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING." } + }, + "currentAppSettings": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. The current app settings." + } } }, "resources": { "app": { "existing": true, "type": "Microsoft.Web/sites", - "apiVersion": "2022-09-01", + "apiVersion": "2023-12-01", "name": "[parameters('appName')]" }, "appInsight": { @@ -88,17 +95,17 @@ "condition": "[not(empty(parameters('storageAccountResourceId')))]", "existing": true, "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2023-01-01", + "apiVersion": "2023-05-01", "subscriptionId": "[split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2]]", "resourceGroup": "[split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]]", "name": "[last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))]" }, "appSettings": { "type": "Microsoft.Web/sites/config", - "apiVersion": "2022-09-01", + "apiVersion": "2023-12-01", "name": "[format('{0}/{1}', parameters('appName'), 'appsettings')]", "kind": "[parameters('kind')]", - "properties": "[union(coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", + "properties": "[union(coalesce(parameters('currentAppSettings'), createObject()), coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-05-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", "dependsOn": [ "app", "appInsight", diff --git a/avm/res/web/site/main.bicep b/avm/res/web/site/main.bicep index b7418015b6..cbd5824ec5 100644 --- a/avm/res/web/site/main.bicep +++ b/avm/res/web/site/main.bicep @@ -245,7 +245,7 @@ resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableT } } -resource app 'Microsoft.Web/sites@2022-09-01' = { +resource app 'Microsoft.Web/sites@2023-12-01' = { name: name location: location kind: kind @@ -294,6 +294,7 @@ module app_appsettings 'config--appsettings/main.bicep' = if (!empty(appSettings storageAccountUseIdentityAuthentication: storageAccountUseIdentityAuthentication appInsightResourceId: appInsightResourceId appSettingsKeyValuePairs: appSettingsKeyValuePairs + currentAppSettings: !empty(app.id) ? list('${app.id}/config/appsettings', '2023-12-01').properties : {} } } diff --git a/avm/res/web/site/main.json b/avm/res/web/site/main.json index e6eccb3688..d3577e551f 100644 --- a/avm/res/web/site/main.json +++ b/avm/res/web/site/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.29.47.4906", - "templateHash": "7320044434284742277" + "templateHash": "4626438010490721609" }, "name": "Web/Function Apps", "description": "This module deploys a Web or Function App.", @@ -860,7 +860,7 @@ }, "app": { "type": "Microsoft.Web/sites", - "apiVersion": "2022-09-01", + "apiVersion": "2023-12-01", "name": "[parameters('name')]", "location": "[parameters('location')]", "kind": "[parameters('kind')]", @@ -998,7 +998,8 @@ }, "appSettingsKeyValuePairs": { "value": "[parameters('appSettingsKeyValuePairs')]" - } + }, + "currentAppSettings": "[if(not(empty(resourceId('Microsoft.Web/sites', parameters('name')))), createObject('value', list(format('{0}/config/appsettings', resourceId('Microsoft.Web/sites', parameters('name'))), '2023-12-01').properties), createObject('value', createObject()))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -1008,7 +1009,7 @@ "_generator": { "name": "bicep", "version": "0.29.47.4906", - "templateHash": "8777070640548664577" + "templateHash": "3998275265127709875" }, "name": "Site App Settings", "description": "This module deploys a Site App Setting.", @@ -1068,13 +1069,20 @@ "metadata": { "description": "Optional. The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING." } + }, + "currentAppSettings": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. The current app settings." + } } }, "resources": { "app": { "existing": true, "type": "Microsoft.Web/sites", - "apiVersion": "2022-09-01", + "apiVersion": "2023-12-01", "name": "[parameters('appName')]" }, "appInsight": { @@ -1090,17 +1098,17 @@ "condition": "[not(empty(parameters('storageAccountResourceId')))]", "existing": true, "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2023-01-01", + "apiVersion": "2023-05-01", "subscriptionId": "[split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2]]", "resourceGroup": "[split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]]", "name": "[last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))]" }, "appSettings": { "type": "Microsoft.Web/sites/config", - "apiVersion": "2022-09-01", + "apiVersion": "2023-12-01", "name": "[format('{0}/{1}', parameters('appName'), 'appsettings')]", "kind": "[parameters('kind')]", - "properties": "[union(coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", + "properties": "[union(coalesce(parameters('currentAppSettings'), createObject()), coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-05-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", "dependsOn": [ "app", "appInsight", @@ -1677,7 +1685,7 @@ "_generator": { "name": "bicep", "version": "0.29.47.4906", - "templateHash": "15729572124587777376" + "templateHash": "13282951347078727812" }, "name": "Web/Function App Deployment Slots", "description": "This module deploys a Web or Function App Deployment Slot.", @@ -2639,7 +2647,8 @@ }, "appSettingsKeyValuePairs": { "value": "[parameters('appSettingsKeyValuePairs')]" - } + }, + "currentAppSettings": "[if(not(empty(resourceId('Microsoft.Web/sites/slots', parameters('appName'), parameters('name')))), createObject('value', list(format('{0}/config/appsettings', resourceId('Microsoft.Web/sites/slots', parameters('appName'), parameters('name'))), '2023-12-01').properties), createObject('value', createObject()))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -2649,7 +2658,7 @@ "_generator": { "name": "bicep", "version": "0.29.47.4906", - "templateHash": "7111332561212908044" + "templateHash": "9363357518124041583" }, "name": "Site Slot App Settings", "description": "This module deploys a Site Slot App Setting.", @@ -2715,6 +2724,13 @@ "metadata": { "description": "Optional. The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING." } + }, + "currentAppSettings": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. The current app settings." + } } }, "resources": { @@ -2756,7 +2772,7 @@ "apiVersion": "2022-09-01", "name": "[format('{0}/{1}/{2}', parameters('appName'), parameters('slotName'), 'appsettings')]", "kind": "[parameters('kind')]", - "properties": "[union(coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", + "properties": "[union(coalesce(parameters('currentAppSettings'), createObject()), coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", "dependsOn": [ "appInsight", "app::slot", @@ -5111,7 +5127,7 @@ "metadata": { "description": "The principal ID of the system assigned identity." }, - "value": "[coalesce(tryGet(tryGet(reference('app', '2022-09-01', 'full'), 'identity'), 'principalId'), '')]" + "value": "[coalesce(tryGet(tryGet(reference('app', '2023-12-01', 'full'), 'identity'), 'principalId'), '')]" }, "slotSystemAssignedMIPrincipalIds": { "type": "array", @@ -5128,7 +5144,7 @@ "metadata": { "description": "The location the resource was deployed into." }, - "value": "[reference('app', '2022-09-01', 'full').location]" + "value": "[reference('app', '2023-12-01', 'full').location]" }, "defaultHostname": { "type": "string", diff --git a/avm/res/web/site/slot/config--appsettings/README.md b/avm/res/web/site/slot/config--appsettings/README.md index 6be3108fe1..a4eaf7b5e3 100644 --- a/avm/res/web/site/slot/config--appsettings/README.md +++ b/avm/res/web/site/slot/config--appsettings/README.md @@ -36,6 +36,7 @@ This module deploys a Site Slot App Setting. | :-- | :-- | :-- | | [`appInsightResourceId`](#parameter-appinsightresourceid) | string | Resource ID of the app insight to leverage for this resource. | | [`appSettingsKeyValuePairs`](#parameter-appsettingskeyvaluepairs) | object | The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING. | +| [`currentAppSettings`](#parameter-currentappsettings) | object | The current app settings. | | [`storageAccountResourceId`](#parameter-storageaccountresourceid) | string | Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions. | | [`storageAccountUseIdentityAuthentication`](#parameter-storageaccountuseidentityauthentication) | bool | If the provided storage account requires Identity based authentication ('allowSharedKeyAccess' is set to false). When set to true, the minimum role assignment required for the App Service Managed Identity to the storage account is 'Storage Blob Data Owner'. | @@ -91,6 +92,14 @@ The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDas - Required: No - Type: object +### Parameter: `currentAppSettings` + +The current app settings. + +- Required: No +- Type: object +- Default: `{}` + ### Parameter: `storageAccountResourceId` Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions. diff --git a/avm/res/web/site/slot/config--appsettings/main.bicep b/avm/res/web/site/slot/config--appsettings/main.bicep index 74095161a8..2fbe7b81d4 100644 --- a/avm/res/web/site/slot/config--appsettings/main.bicep +++ b/avm/res/web/site/slot/config--appsettings/main.bicep @@ -37,6 +37,9 @@ param appInsightResourceId string? @description('Optional. The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING.') param appSettingsKeyValuePairs object? +@description('Optional. The current app settings.') +param currentAppSettings object = {} + var azureWebJobsValues = !empty(storageAccountResourceId) && !(storageAccountUseIdentityAuthentication) ? { AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' @@ -54,7 +57,12 @@ var appInsightsValues = !empty(appInsightResourceId) } : {} -var expandedAppSettings = union(appSettingsKeyValuePairs ?? {}, azureWebJobsValues, appInsightsValues) +var expandedAppSettings = union( + currentAppSettings ?? {}, + appSettingsKeyValuePairs ?? {}, + azureWebJobsValues, + appInsightsValues +) resource app 'Microsoft.Web/sites@2022-09-01' existing = { name: appName diff --git a/avm/res/web/site/slot/config--appsettings/main.json b/avm/res/web/site/slot/config--appsettings/main.json index 48ed22304b..af2de6024a 100644 --- a/avm/res/web/site/slot/config--appsettings/main.json +++ b/avm/res/web/site/slot/config--appsettings/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.29.47.4906", - "templateHash": "7111332561212908044" + "templateHash": "9363357518124041583" }, "name": "Site Slot App Settings", "description": "This module deploys a Site Slot App Setting.", @@ -72,6 +72,13 @@ "metadata": { "description": "Optional. The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING." } + }, + "currentAppSettings": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. The current app settings." + } } }, "resources": { @@ -113,7 +120,7 @@ "apiVersion": "2022-09-01", "name": "[format('{0}/{1}/{2}', parameters('appName'), parameters('slotName'), 'appsettings')]", "kind": "[parameters('kind')]", - "properties": "[union(coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", + "properties": "[union(coalesce(parameters('currentAppSettings'), createObject()), coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", "dependsOn": [ "appInsight", "app::slot", diff --git a/avm/res/web/site/slot/main.bicep b/avm/res/web/site/slot/main.bicep index b871bf0960..95b2e0eff7 100644 --- a/avm/res/web/site/slot/main.bicep +++ b/avm/res/web/site/slot/main.bicep @@ -264,6 +264,7 @@ module slot_appsettings 'config--appsettings/main.bicep' = if (!empty(appSetting storageAccountUseIdentityAuthentication: storageAccountUseIdentityAuthentication appInsightResourceId: appInsightResourceId appSettingsKeyValuePairs: appSettingsKeyValuePairs + currentAppSettings: !empty(slot.id) ? list('${slot.id}/config/appsettings', '2023-12-01').properties : {} } } diff --git a/avm/res/web/site/slot/main.json b/avm/res/web/site/slot/main.json index 8f8f81e34a..3120546cad 100644 --- a/avm/res/web/site/slot/main.json +++ b/avm/res/web/site/slot/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.29.47.4906", - "templateHash": "15729572124587777376" + "templateHash": "13282951347078727812" }, "name": "Web/Function App Deployment Slots", "description": "This module deploys a Web or Function App Deployment Slot.", @@ -968,7 +968,8 @@ }, "appSettingsKeyValuePairs": { "value": "[parameters('appSettingsKeyValuePairs')]" - } + }, + "currentAppSettings": "[if(not(empty(resourceId('Microsoft.Web/sites/slots', parameters('appName'), parameters('name')))), createObject('value', list(format('{0}/config/appsettings', resourceId('Microsoft.Web/sites/slots', parameters('appName'), parameters('name'))), '2023-12-01').properties), createObject('value', createObject()))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -978,7 +979,7 @@ "_generator": { "name": "bicep", "version": "0.29.47.4906", - "templateHash": "7111332561212908044" + "templateHash": "9363357518124041583" }, "name": "Site Slot App Settings", "description": "This module deploys a Site Slot App Setting.", @@ -1044,6 +1045,13 @@ "metadata": { "description": "Optional. The app settings key-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING." } + }, + "currentAppSettings": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. The current app settings." + } } }, "resources": { @@ -1085,7 +1093,7 @@ "apiVersion": "2022-09-01", "name": "[format('{0}/{1}/{2}', parameters('appName'), parameters('slotName'), 'appsettings')]", "kind": "[parameters('kind')]", - "properties": "[union(coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", + "properties": "[union(coalesce(parameters('currentAppSettings'), createObject()), coalesce(parameters('appSettingsKeyValuePairs'), createObject()), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(coalesce(parameters('storageAccountResourceId'), '//'), '/')[2], split(coalesce(parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), '2023-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), union(createObject('AzureWebJobsStorage__accountName', last(split(coalesce(parameters('storageAccountResourceId'), 'dummyName'), '/'))), createObject('AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob)), createObject())), if(not(empty(parameters('appInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('appInsight').ConnectionString), createObject()))]", "dependsOn": [ "appInsight", "app::slot", diff --git a/avm/res/web/site/tests/e2e/functionApp.settings/dependencies.bicep b/avm/res/web/site/tests/e2e/functionApp.settings/dependencies.bicep new file mode 100644 index 0000000000..dd34e10b1c --- /dev/null +++ b/avm/res/web/site/tests/e2e/functionApp.settings/dependencies.bicep @@ -0,0 +1,21 @@ +@description('Optional. The location to deploy resources to.') +param location string = resourceGroup().location + +@description('Required. The name of the Server Farm to create.') +param serverFarmName string + +resource serverFarm 'Microsoft.Web/serverfarms@2022-03-01' = { + name: serverFarmName + location: location + sku: { + name: 'S1' + tier: 'Standard' + size: 'S1' + family: 'S' + capacity: 1 + } + properties: {} +} + +@description('The resource ID of the created Server Farm.') +output serverFarmResourceId string = serverFarm.id diff --git a/avm/res/web/site/tests/e2e/functionApp.settings/main.test.bicep b/avm/res/web/site/tests/e2e/functionApp.settings/main.test.bicep new file mode 100644 index 0000000000..a62f7217a1 --- /dev/null +++ b/avm/res/web/site/tests/e2e/functionApp.settings/main.test.bicep @@ -0,0 +1,67 @@ +targetScope = 'subscription' + +metadata name = 'Function App, using only defaults' +metadata description = 'This instance deploys the module as Function App with the minimum set of required parameters.' + +// ========== // +// Parameters // +// ========== // + +@description('Optional. The name of the resource group to deploy for testing purposes.') +@maxLength(90) +param resourceGroupName string = 'dep-${namePrefix}-web.sites-${serviceShort}-rg' + +@description('Optional. The location to deploy resources to.') +param resourceLocation string = deployment().location + +@description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints.') +param serviceShort string = 'wsfaset' + +@description('Optional. A token to inject into the name of each resource.') +param namePrefix string = '#_namePrefix_#' + +// ============ // +// Dependencies // +// ============ // + +// General resources +// ================= +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: resourceGroupName + location: resourceLocation +} + +module nestedDependencies 'dependencies.bicep' = { + scope: resourceGroup + name: '${uniqueString(deployment().name, resourceLocation)}-nestedDependencies' + params: { + serverFarmName: 'dep-${namePrefix}-sf-${serviceShort}' + location: resourceLocation + } +} + +// ============== // +// Test Execution // +// ============== // + +@batchSize(1) +module testDeployment '../../../main.bicep' = [ + for iteration in ['init', 'idem']: { + scope: resourceGroup + name: '${uniqueString(deployment().name, resourceLocation)}-test-${serviceShort}-${iteration}' + params: { + name: '${namePrefix}${serviceShort}001' + location: resourceLocation + kind: 'functionapp' + serverFarmResourceId: nestedDependencies.outputs.serverFarmResourceId + appSettingsKeyValuePairs: { + AzureFunctionsJobHost__logging__logLevel__default: 'Trace' + FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_WORKER_RUNTIME: 'dotnet' + } + } + dependsOn: [ + nestedDependencies + ] + } +] diff --git a/avm/res/web/site/version.json b/avm/res/web/site/version.json index 0f81d22abc..b8b30a0125 100644 --- a/avm/res/web/site/version.json +++ b/avm/res/web/site/version.json @@ -1,6 +1,6 @@ { "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#", - "version": "0.8", + "version": "0.9", "pathFilters": [ "./main.json" ]