Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Retain existing settings during deployment - avm/res/web/site #3311

Merged
merged 5 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 89 additions & 19 deletions avm/res/web/site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand All @@ -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_

Expand Down Expand Up @@ -515,7 +517,75 @@ module site 'br/public:avm/res/web/site:<version>' = {
</details>
<p>

### 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.


<details>

<summary>via Bicep module</summary>

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

</details>
<p>

<details>

<summary>via JSON Parameter file</summary>

```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": "<serverFarmResourceId>"
},
// Non-required parameters
"appSettingsKeyValuePairs": {
"value": {
"AzureFunctionsJobHost__logging__logLevel__default": "Trace",
"FUNCTIONS_EXTENSION_VERSION": "~4",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
}
},
"location": {
"value": "<location>"
}
}
}
```

</details>
<p>

### Example 4: _Web App, using only defaults_

This instance deploys the module as a Linux Web App with the minimum set of required parameters.

Expand Down Expand Up @@ -591,7 +661,7 @@ module site 'br/public:avm/res/web/site:<version>' = {
</details>
<p>

### Example 4: _Web App_
### Example 5: _Web App_

This instance deploys the module as Web App with the set of logs configuration.

Expand Down Expand Up @@ -723,7 +793,7 @@ module site 'br/public:avm/res/web/site:<version>' = {
</details>
<p>

### 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.

Expand Down Expand Up @@ -865,7 +935,7 @@ module site 'br/public:avm/res/web/site:<version>' = {
</details>
<p>

### 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.

Expand Down Expand Up @@ -921,7 +991,7 @@ module site 'br/public:avm/res/web/site:<version>' = {
</details>
<p>

### 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.

Expand Down Expand Up @@ -1389,7 +1459,7 @@ module site 'br/public:avm/res/web/site:<version>' = {
</details>
<p>

### 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.

Expand Down Expand Up @@ -1445,7 +1515,7 @@ module site 'br/public:avm/res/web/site:<version>' = {
</details>
<p>

### 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.

Expand Down Expand Up @@ -1907,7 +1977,7 @@ module site 'br/public:avm/res/web/site:<version>' = {
</details>
<p>

### Example 10: _Web App_
### Example 11: _Web App_

This instance deploys the module as Web App with the set of api management configuration.

Expand Down Expand Up @@ -2003,7 +2073,7 @@ module site 'br/public:avm/res/web/site:<version>' = {
</details>
<p>

### 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.

Expand Down
11 changes: 10 additions & 1 deletion avm/res/web/site/config--appsettings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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'. |

Expand Down Expand Up @@ -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.
Expand Down
16 changes: 12 additions & 4 deletions avm/res/web/site/config--appsettings/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Expand All @@ -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
}

Expand All @@ -62,15 +70,15 @@ 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],
split(storageAccountResourceId ?? '////', '/')[4]
)
}

resource appSettings 'Microsoft.Web/sites/config@2022-09-01' = {
resource appSettings 'Microsoft.Web/sites/config@2023-12-01' = {
name: 'appsettings'
kind: kind
parent: app
Expand Down
17 changes: 12 additions & 5 deletions avm/res/web/site/config--appsettings/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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": {
Expand All @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion avm/res/web/site/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 : {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of deployment would have an empty app.id?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be configurable? I fear that someone will open a ticket at some point to have full idempotency.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jikuja Certainly yes. IaC is about desired state and full update not incremental.
But I'm more concerned with a potential race condition.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jikuja Can you elaborate on what do you think when you say race condition ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jikuja Can you elaborate on what do you think when you say race condition ?

@pankajagrawal16 It seems the deployment is doing a fetch, then merge the properties. It is not an atomic operation. Two concurrent deployments (with different name) will fetch the same old values and only pushes its new properties. The last one to push will wins.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scrocquesel-ml150 Are you talking about multiple concurrent deployments of avm/res/web/site/main.bicep or concurrent deployment of avm/res/web/site/main.bicep main module and avm/res/web/site/config--appsettings/main.bicep?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scrocquesel-ml150 Are you talking about multiple concurrent deployments of avm/res/web/site/main.bicep or concurrent deployment of avm/res/web/site/main.bicep main module and avm/res/web/site/config--appsettings/main.bicep?

two avm/res/web/site/main.bicep will fetch the same existing values, then, after resource app is completed, each one will create a new avm/res/web/site/config--appsettings/main.bicep deployment with the same existing values and maybe different new values as appSettingsKeyValuePairs. I think if you want to be sure no race condition can occur, you can make an atomic deployment with the exact same deployment name because Azure will not permit to create a deployment of the same name if one exists and is not completed and will fail the deployment. So if you fetch the existing value and update the properties inside such a deployment, you have kind of a poor-man semaphore/lock.
I admit this is a race condition that is difficult to produce and everyone is using more or less this pattern without even knowing it exists. I guess it is a matter of complexity vs reliability.

Copy link
Contributor Author

@peterbud peterbud Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the issue you are describing here.

In case one is running the same deployments concurrently (it's already an anti pattern AFAIK) they will get the same appSettingsKeyValuePairs (i.e.: keeping values NOT defined in the bicep file, and merging them with the defined values from the bicep file) - so I don't see an issue or race condition: both deployment will result the same result even running concurrently.

In case somebody is running different deployments concurrently, then the last will win, aka the latest deployment will take precedence - which is normal. Also note, that was the behavior before this change already.

}
}

Expand Down
Loading