From 855e0c54c2aeaa66493cbf3afe602ebb4e512445 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:42:33 +0000 Subject: [PATCH 1/6] Initial plan From e4f6dcf526d840c5964324411e02b889a238b94a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:45:30 +0000 Subject: [PATCH 2/6] Initial analysis - planning CDN removal and custom domain migration Co-authored-by: jhueppauff <20532954+jhueppauff@users.noreply.github.com> --- Templates/resources.json | 600 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 600 insertions(+) create mode 100644 Templates/resources.json diff --git a/Templates/resources.json b/Templates/resources.json new file mode 100644 index 0000000..be02251 --- /dev/null +++ b/Templates/resources.json @@ -0,0 +1,600 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "11184959616547663006" + } + }, + "parameters": { + "functionEngineName": { + "type": "string", + "defaultValue": "func-blog-engine-we-prod-001" + }, + "functionFrontendName": { + "type": "string", + "defaultValue": "func-blog-engine-we-prod-001" + }, + "profileProperties": { + "type": "object" + }, + "endpointProperties": { + "type": "object" + }, + "aadClientId": { + "type": "string" + }, + "aadTenant": { + "type": "string" + }, + "cosmosDbisZoneRedundant": { + "type": "bool" + }, + "cosmosDbName": { + "type": "string" + }, + "staticWebAppName": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "westeurope" + }, + "serviceBusName": { + "type": "string", + "defaultValue": "sb-blog-we-prod-001" + } + }, + "variables": { + "appInsightName_var": "[replace(parameters('functionEngineName'), 'func', 'appi')]", + "appPlanName_var": "[replace(parameters('functionEngineName'), 'func', 'plan')]", + "storageNameWeb_var": "stblogstaticweprod001", + "storageFunction_var": "stblogfuncweprod001", + "cdnProfileName_var": "[replace(parameters('functionEngineName'), 'func', 'cdn')]", + "cdnEndpointName": "[replace(parameters('functionFrontendName'), 'func', 'cdnedp')]", + "serviceBusReceiverRoleId": "4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0", + "serviceBusSenderRoleId": "69a216fc-b8fb-44d8-bc22-1f3c2cd27a39", + "blogDataContributorRoleId": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "blogDataOwnerRoleId": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b" + }, + "resources": [ + { + "type": "Microsoft.Web/staticSites", + "apiVersion": "2022-03-01", + "name": "[parameters('staticWebAppName')]", + "location": "[parameters('location')]", + "tags": {}, + "properties": { + "repositoryUrl": "https://github.com/jhueppauff/ServerlessBlog", + "branch": "main", + "buildProperties": { + "appLocation": "EditorNG", + "apiLocation": "", + "appArtifactLocation": "wwwroot" + } + }, + "sku": { + "tier": "Free", + "name": "Free" + } + }, + { + "type": "Microsoft.ServiceBus/namespaces", + "apiVersion": "2021-11-01", + "name": "[parameters('serviceBusName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Basic" + } + }, + { + "type": "Microsoft.ServiceBus/namespaces/queues", + "apiVersion": "2021-11-01", + "name": "[format('{0}/{1}', parameters('serviceBusName'), 'scheduled')]", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName'))]" + ] + }, + { + "type": "Microsoft.ServiceBus/namespaces/queues", + "apiVersion": "2021-11-01", + "name": "[format('{0}/{1}', parameters('serviceBusName'), 'created')]", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageNameWeb_var'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var')), resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), variables('blogDataContributorRoleId'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), '2022-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataContributorRoleId'))]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionFrontendName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageNameWeb_var'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('blogDataContributorRoleId'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataContributorRoleId'))]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunction_var'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), variables('blogDataOwnerRoleId'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), '2022-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataOwnerRoleId'))]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionFrontendName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunction_var'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), variables('serviceBusReceiverRoleId'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), '2022-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataContributorRoleId'))]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionFrontendName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunction_var'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('blogDataOwnerRoleId'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataOwnerRoleId'))]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunction_var'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('serviceBusReceiverRoleId'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataContributorRoleId'))]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.ServiceBus/namespaces/{0}', parameters('serviceBusName'))]", + "name": "[guid(resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('serviceBusReceiverRoleId'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('serviceBusReceiverRoleId'))]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]", + "[resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.ServiceBus/namespaces/{0}', parameters('serviceBusName'))]", + "name": "[guid(resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('serviceBusSenderRoleId'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('serviceBusSenderRoleId'))]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]", + "[resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName'))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-09-01", + "name": "[variables('storageNameWeb_var')]", + "location": "[parameters('location')]", + "kind": "StorageV2", + "sku": { + "name": "Standard_LRS" + }, + "properties": { + "accessTier": "Hot", + "minimumTlsVersion": "TLS1_2", + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2021-09-01", + "name": "[format('{0}/{1}', variables('storageNameWeb_var'), 'default')]", + "properties": { + "changeFeed": { + "enabled": true + }, + "restorePolicy": { + "enabled": true, + "days": 6 + }, + "containerDeleteRetentionPolicy": { + "enabled": true, + "days": 7 + }, + "cors": { + "corsRules": [] + }, + "deleteRetentionPolicy": { + "enabled": true, + "days": 7 + }, + "isVersioningEnabled": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-09-01", + "name": "[variables('storageFunction_var')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "networkAcls": { + "bypass": "AzureServices", + "virtualNetworkRules": [], + "ipRules": [], + "defaultAction": "Allow" + }, + "supportsHttpsTrafficOnly": true, + "encryption": { + "services": { + "file": { + "enabled": true + }, + "blob": { + "enabled": true + } + }, + "keySource": "Microsoft.Storage" + }, + "accessTier": "Hot", + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false + } + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2022-03-01", + "name": "[variables('appPlanName_var')]", + "location": "[parameters('location')]", + "sku": { + "name": "Y1", + "tier": "Dynamic" + }, + "properties": {} + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2022-03-01", + "name": "[parameters('functionEngineName')]", + "location": "[parameters('location')]", + "kind": "functionapp", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName_var'))]", + "siteConfig": { + "minTlsVersion": "1.2", + "netFrameworkVersion": "v8.0", + "appSettings": [ + { + "name": "OpenApi__Auth__TenantId", + "value": "72e647c0-4a7a-4959-bee5-14c8615d8ae5" + }, + { + "name": "AzureWebJobsStorage", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunction_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), '2021-09-01').keys[0].value)]" + }, + { + "name": "AzureStorageConnection", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageNameWeb_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var')), '2021-09-01').keys[0].value)]" + }, + { + "name": "CosmosDBConnection", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};TableEndpoint=https://{2}.table.cosmos.azure.com:443/;', parameters('cosmosDbName'), listKeys(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName')), '2020-04-01').primaryMasterKey, parameters('cosmosDbName'))]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunction_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), '2021-09-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(parameters('functionEngineName'))]" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~4" + }, + { + "name": "ServiceBusConnection__fullyQualifiedNamespace", + "value": "[format('{0}.servicebus.windows.net', parameters('serviceBusName'))]" + }, + { + "name": "DeletionDays", + "value": "32" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightName_var')), '2020-02-02').ConnectionString]" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "dotnet-isolated" + }, + { + "name": "WEBSITE_RUN_FROM_PACKAGE", + "value": "1" + }, + { + "name": "WEBSITE_ENABLE_SYNC_UPDATE_SITE", + "value": "true" + }, + { + "name": "AzureWebJobsDisableHomepage", + "value": "true" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/components', variables('appInsightName_var'))]", + "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName_var'))]", + "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName'))]", + "[resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + ] + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[format('{0}/{1}', parameters('functionEngineName'), 'authsettings')]", + "properties": { + "enabled": true, + "unauthenticatedClientAction": "RedirectToLoginPage", + "tokenStoreEnabled": true, + "defaultProvider": "AzureActiveDirectory", + "clientId": "[parameters('aadClientId')]", + "issuer": "[format('https://sts.windows.net/{0}/', parameters('aadTenant'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]" + ] + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2022-03-01", + "name": "[parameters('functionFrontendName')]", + "location": "[parameters('location')]", + "kind": "functionapp", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName_var'))]", + "siteConfig": { + "minTlsVersion": "1.2", + "netFrameworkVersion": "v8.0", + "appSettings": [ + { + "name": "AzureWebJobsStorage", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunction_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), '2021-09-01').keys[0].value)]" + }, + { + "name": "AzureStorageConnection", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageNameWeb_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var')), '2021-09-01').keys[0].value)]" + }, + { + "name": "CosmosDBConnection", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};TableEndpoint=https://{2}.table.cosmos.azure.com:443/;', parameters('cosmosDbName'), listKeys(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName')), '2020-04-01').primaryMasterKey, parameters('cosmosDbName'))]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunction_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), '2021-09-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(parameters('functionFrontendName'))]" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~4" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightName_var')), '2020-02-02').ConnectionString]" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "dotnet-isolated" + }, + { + "name": "WEBSITE_RUN_FROM_PACKAGE", + "value": "1" + }, + { + "name": "WEBSITE_ENABLE_SYNC_UPDATE_SITE", + "value": "true" + }, + { + "name": "AzureWebJobsDisableHomepage", + "value": "true" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/components', variables('appInsightName_var'))]", + "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName_var'))]", + "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + ] + }, + { + "type": "Microsoft.Cdn/profiles", + "apiVersion": "2019-04-15", + "name": "[variables('cdnProfileName_var')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_Microsoft" + }, + "properties": "[parameters('profileProperties')]" + }, + { + "type": "Microsoft.Cdn/profiles/endpoints", + "apiVersion": "2019-04-15", + "name": "[format('{0}/{1}', variables('cdnProfileName_var'), variables('cdnEndpointName'))]", + "location": "[parameters('location')]", + "properties": "[parameters('endpointProperties')]", + "dependsOn": [ + "[resourceId('Microsoft.Cdn/profiles', variables('cdnProfileName_var'))]" + ] + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightName_var')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', replace(variables('appInsightName_var'), 'appi', 'log'))]", + "IngestionMode": "LogAnalytics", + "RetentionInDays": 30, + "Application_Type": "web", + "Flow_Type": "Redfield", + "Request_Source": "IbizaAIExtension" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', replace(variables('appInsightName_var'), 'appi', 'log'))]" + ] + }, + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2021-12-01-preview", + "name": "[replace(variables('appInsightName_var'), 'appi', 'log')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30, + "workspaceCapping": { + "dailyQuotaGb": 1 + } + } + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2022-05-15", + "name": "[parameters('cosmosDbName')]", + "kind": "GlobalDocumentDB", + "location": "[parameters('location')]", + "properties": { + "databaseAccountOfferType": "Standard", + "locations": [ + { + "failoverPriority": 0, + "locationName": "[parameters('location')]", + "isZoneRedundant": "[parameters('cosmosDbisZoneRedundant')]" + } + ], + "backupPolicy": { + "type": "Periodic", + "periodicModeProperties": { + "backupIntervalInMinutes": 1440, + "backupRetentionIntervalInHours": 48, + "backupStorageRedundancy": "Local" + } + }, + "isVirtualNetworkFilterEnabled": false, + "virtualNetworkRules": [], + "ipRules": [], + "enableMultipleWriteLocations": false, + "capabilities": [ + { + "name": "EnableTable" + } + ], + "enableFreeTier": true + } + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts/tables", + "apiVersion": "2021-04-15", + "name": "[format('{0}/{1}', parameters('cosmosDbName'), 'metadata')]", + "properties": { + "resource": { + "id": "metadata" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName'))]" + ] + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts/tables", + "apiVersion": "2021-04-15", + "name": "[format('{0}/{1}', parameters('cosmosDbName'), 'metrics')]", + "properties": { + "resource": { + "id": "metrics" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName'))]" + ] + } + ] +} \ No newline at end of file From 55c2691eca63dfeb91e37ca9b8cf4a0a78ba5d1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:48:23 +0000 Subject: [PATCH 3/6] Remove Azure CDN and add custom domain support to Function Frontend Co-authored-by: jhueppauff <20532954+jhueppauff@users.noreply.github.com> --- README.md | 2 +- Templates/resources.bicep | 27 ++++------ Templates/resources.json | 37 +++++-------- Templates/resources.parameters.json | 80 +---------------------------- 4 files changed, 27 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 13a7889..eb511e8 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Components used - Azure Functions - Azure Storage (Blob, Table and Queues) -- Azure CDN - Application Insights +- Custom Domain Support (optional) ## How to run diff --git a/Templates/resources.bicep b/Templates/resources.bicep index 3b0836b..d1704ff 100644 --- a/Templates/resources.bicep +++ b/Templates/resources.bicep @@ -1,7 +1,6 @@ param functionEngineName string = 'func-blog-engine-we-prod-001' param functionFrontendName string = 'func-blog-engine-we-prod-001' -param profileProperties object -param endpointProperties object +param customDomainName string = '' param aadClientId string param aadTenant string param cosmosDbisZoneRedundant bool @@ -14,8 +13,6 @@ var appInsightName_var = replace(functionEngineName, 'func', 'appi') var appPlanName_var = replace(functionEngineName, 'func', 'plan') var storageNameWeb_var = 'stblogstaticweprod001' var storageFunction_var = 'stblogfuncweprod001' -var cdnProfileName_var = replace(functionEngineName, 'func', 'cdn') -var cdnEndpointName = replace(functionFrontendName, 'func', 'cdnedp') var serviceBusReceiverRoleId = '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0' var serviceBusSenderRoleId = '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39' var blogDataContributorRoleId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' @@ -340,6 +337,7 @@ resource functionFrontend 'Microsoft.Web/sites@2022-03-01' = { } properties: { serverFarmId: appPlan.id + httpsOnly: true siteConfig: { minTlsVersion: '1.2' netFrameworkVersion: 'v8.0' @@ -393,20 +391,15 @@ resource functionFrontend 'Microsoft.Web/sites@2022-03-01' = { } } -resource cdnProfileName 'microsoft.cdn/profiles@2019-04-15' = { - name: cdnProfileName_var - location: location - sku: { - name: 'Standard_Microsoft' +// Custom domain binding for the frontend function (optional) +resource functionFrontendCustomDomain 'Microsoft.Web/sites/hostNameBindings@2022-03-01' = if (!empty(customDomainName)) { + parent: functionFrontend + name: customDomainName + properties: { + hostNameType: 'Verified' + sslState: 'SniEnabled' + thumbprint: null } - properties: profileProperties -} - -resource cdnProfileName_cdnEndpointName 'microsoft.cdn/profiles/endpoints@2019-04-15' = { - parent: cdnProfileName - name: cdnEndpointName - location: location - properties: endpointProperties } resource appInsight 'Microsoft.Insights/components@2020-02-02' = { diff --git a/Templates/resources.json b/Templates/resources.json index be02251..36b5838 100644 --- a/Templates/resources.json +++ b/Templates/resources.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.37.4.10188", - "templateHash": "11184959616547663006" + "templateHash": "3009564044815152776" } }, "parameters": { @@ -17,11 +17,9 @@ "type": "string", "defaultValue": "func-blog-engine-we-prod-001" }, - "profileProperties": { - "type": "object" - }, - "endpointProperties": { - "type": "object" + "customDomainName": { + "type": "string", + "defaultValue": "" }, "aadClientId": { "type": "string" @@ -52,8 +50,6 @@ "appPlanName_var": "[replace(parameters('functionEngineName'), 'func', 'plan')]", "storageNameWeb_var": "stblogstaticweprod001", "storageFunction_var": "stblogfuncweprod001", - "cdnProfileName_var": "[replace(parameters('functionEngineName'), 'func', 'cdn')]", - "cdnEndpointName": "[replace(parameters('functionFrontendName'), 'func', 'cdnedp')]", "serviceBusReceiverRoleId": "4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0", "serviceBusSenderRoleId": "69a216fc-b8fb-44d8-bc22-1f3c2cd27a39", "blogDataContributorRoleId": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", @@ -423,6 +419,7 @@ }, "properties": { "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName_var'))]", + "httpsOnly": true, "siteConfig": { "minTlsVersion": "1.2", "netFrameworkVersion": "v8.0", @@ -483,23 +480,17 @@ ] }, { - "type": "Microsoft.Cdn/profiles", - "apiVersion": "2019-04-15", - "name": "[variables('cdnProfileName_var')]", - "location": "[parameters('location')]", - "sku": { - "name": "Standard_Microsoft" + "condition": "[not(empty(parameters('customDomainName')))]", + "type": "Microsoft.Web/sites/hostNameBindings", + "apiVersion": "2022-03-01", + "name": "[format('{0}/{1}', parameters('functionFrontendName'), parameters('customDomainName'))]", + "properties": { + "hostNameType": "Verified", + "sslState": "SniEnabled", + "thumbprint": null }, - "properties": "[parameters('profileProperties')]" - }, - { - "type": "Microsoft.Cdn/profiles/endpoints", - "apiVersion": "2019-04-15", - "name": "[format('{0}/{1}', variables('cdnProfileName_var'), variables('cdnEndpointName'))]", - "location": "[parameters('location')]", - "properties": "[parameters('endpointProperties')]", "dependsOn": [ - "[resourceId('Microsoft.Cdn/profiles', variables('cdnProfileName_var'))]" + "[resourceId('Microsoft.Web/sites', parameters('functionFrontendName'))]" ] }, { diff --git a/Templates/resources.parameters.json b/Templates/resources.parameters.json index a08964c..5737e84 100644 --- a/Templates/resources.parameters.json +++ b/Templates/resources.parameters.json @@ -8,8 +8,8 @@ "functionFrontendName": { "value": "func-blog-frontend-we-prod-001" }, - "profileProperties": { - "value": {} + "customDomainName": { + "value": "" }, "aadClientId": { "value": "4d459e38-e3e7-4aec-808e-d5961cf821b2" @@ -25,82 +25,6 @@ }, "staticWebAppName": { "value": "sapp-blobeditor-we-prod-001" - }, - "endpointProperties": { - "value": { - "isHttpAllowed": true, - "isHttpsAllowed": true, - "originHostHeader": "func-blog-frontend-we-prod-001.azurewebsites.net", - "origins": [{ - "name": "frontend", - "properties": { - "hostName": "func-blog-frontend-we-prod-001.azurewebsites.net", - "httpPort": 80, - "httpsPort": 443 - } - }], - "isCompressionEnabled": true, - "contentTypesToCompress": [ - "text/plain", - "text/html", - "text/css", - "text/javascript", - "application/x-javascript", - "application/javascript", - "application/json", - "application/xml" - ], - "deliveryPolicy": { - "rules": [{ - "name": "forceHttps", - "order": 1, - "conditions": [{ - "name": "RequestScheme", - "parameters": { - "@odata.type": "#Microsoft.Azure.Cdn.Models.DeliveryRuleRequestSchemeConditionParameters", - "operator": "Equal", - "negateCondition": false, - "matchValues": [ - "HTTP" - ] - } - }], - "actions": [{ - "name": "UrlRedirect", - "parameters": { - "@odata.type": "#Microsoft.Azure.Cdn.Models.DeliveryRuleUrlRedirectActionParameters", - "redirectType": "Found", - "destinationProtocol": "Https" - } - }] - }, - { - "name": "cacheControl", - "order": 2, - "conditions": [{ - "name": "RequestHeader", - "parameters": { - "@odata.type": "#Microsoft.Azure.Cdn.Models.DeliveryRuleRequestHeaderConditionParameters", - "operator": "Any", - "selector": "Cache-Control", - "negateCondition": true, - "matchValues": [], - "transforms": [] - } - }], - "actions": [{ - "name": "CacheExpiration", - "parameters": { - "@odata.type": "#Microsoft.Azure.Cdn.Models.DeliveryRuleCacheExpirationActionParameters", - "cacheBehavior": "SetIfMissing", - "cacheDuration": "01:00:00", - "cacheType": "All" - } - }] - } - ] - } - } } } } \ No newline at end of file From 933bbc98609203204023703e0ded9137b33916e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:56:27 +0000 Subject: [PATCH 4/6] Migrate to Azure Verified Modules patterns and update API versions Co-authored-by: jhueppauff <20532954+jhueppauff@users.noreply.github.com> --- README.md | 13 ++ Templates/resources.bicep | 161 +++++++++++++++--------- Templates/resources.json | 253 ++++++++++++++++++++++---------------- 3 files changed, 262 insertions(+), 165 deletions(-) diff --git a/README.md b/README.md index eb511e8..7acd16b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,19 @@ Components used [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fgithub.com%2Fjhueppauff%2FServerlessBlog%2Fblob%2Fmain%2FTemplates%2Fresources.json) +### Custom Domain Configuration + +To add a custom domain to your blog: + +1. Deploy the infrastructure using the Bicep template +2. Configure your DNS to point to the Azure Function Frontend: + - Add a CNAME record pointing to `.azurewebsites.net` +3. Update the `customDomainName` parameter in `resources.parameters.json` with your domain (e.g., `blog.yourdomain.com`) +4. Redeploy the template to bind the custom domain +5. Azure will automatically provision an SSL certificate for HTTPS + +Note: The custom domain parameter is optional. If left empty, the blog will be accessible via the default Azure Function URL. + ## How to customize Currently the Blog has some static assets sitting in the Frontend and Engine Function. If you like to change HTML, CSS you need to update the html files in the statics Folder. diff --git a/Templates/resources.bicep b/Templates/resources.bicep index d1704ff..fe580b9 100644 --- a/Templates/resources.bicep +++ b/Templates/resources.bicep @@ -1,28 +1,51 @@ +// ========== Parameters ========== +@description('Name of the backend engine function app') param functionEngineName string = 'func-blog-engine-we-prod-001' + +@description('Name of the frontend function app') param functionFrontendName string = 'func-blog-engine-we-prod-001' + +@description('Custom domain name for the frontend function (optional). Leave empty to use default azurewebsites.net domain') param customDomainName string = '' + +@description('Azure Active Directory client ID for authentication') param aadClientId string + +@description('Azure Active Directory tenant for authentication') param aadTenant string + +@description('Enable zone redundancy for Cosmos DB') param cosmosDbisZoneRedundant bool + +@description('Name of the Cosmos DB account') param cosmosDbName string + +@description('Name of the static web app for the editor') param staticWebAppName string + +@description('Azure region for resource deployment') param location string = 'westeurope' + +@description('Name of the Service Bus namespace') param serviceBusName string = 'sb-blog-we-prod-001' -var appInsightName_var = replace(functionEngineName, 'func', 'appi') -var appPlanName_var = replace(functionEngineName, 'func', 'plan') -var storageNameWeb_var = 'stblogstaticweprod001' -var storageFunction_var = 'stblogfuncweprod001' -var serviceBusReceiverRoleId = '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0' -var serviceBusSenderRoleId = '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39' -var blogDataContributorRoleId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' -var blogDataOwnerRoleId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' +// ========== Variables ========== +var appInsightName = replace(functionEngineName, 'func', 'appi') +var appPlanName = replace(functionEngineName, 'func', 'plan') +var storageNameWeb = 'stblogstaticweprod001' +var storageFunctionName = 'stblogfuncweprod001' + +// Built-in Azure RBAC role IDs +var serviceBusReceiverRoleId = '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0' // Azure Service Bus Data Receiver +var serviceBusSenderRoleId = '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39' // Azure Service Bus Data Sender +var blogDataContributorRoleId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor +var blogDataOwnerRoleId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner -resource staticWebApp 'Microsoft.Web/staticSites@2022-03-01' = { +// ========== Static Web App (Editor) ========== +resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = { name: staticWebAppName location: location - tags: { - } + tags: {} properties: { repositoryUrl: 'https://github.com/jhueppauff/ServerlessBlog' branch: 'main' @@ -38,24 +61,29 @@ resource staticWebApp 'Microsoft.Web/staticSites@2022-03-01' = { } } -resource serviceBus 'Microsoft.ServiceBus/namespaces@2021-11-01' = { +// ========== Service Bus ========== +resource serviceBus 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = { name: serviceBusName location: location sku: { name: 'Basic' } + properties: { + minimumTlsVersion: '1.2' + } } -resource scheduledQueue 'Microsoft.ServiceBus/namespaces/queues@2021-11-01' = { +resource scheduledQueue 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = { name: 'scheduled' parent: serviceBus } -resource renderQueue 'Microsoft.ServiceBus/namespaces/queues@2021-11-01' = { +resource renderQueue 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = { name: 'created' parent: serviceBus } +// ========== RBAC Role Definitions (Existing) ========== resource blobDataContributorRoleDefenition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { scope: resourceGroup() name: blogDataContributorRoleId @@ -76,7 +104,8 @@ resource serviceBusSenderRoleDefenition 'Microsoft.Authorization/roleDefinitions name: serviceBusSenderRoleId } -// Frontend Storage +// ========== RBAC Role Assignments ========== +// Frontend Storage access resource rbacFunctionServiceStorageWebEngine 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(storageWeb.id, functionFrontend.id, blogDataContributorRoleId) scope: storageWeb @@ -158,8 +187,10 @@ resource rbacFunctionServiceBusSender 'Microsoft.Authorization/roleAssignments@2 } } -resource storageWeb 'Microsoft.Storage/storageAccounts@2021-09-01' = { - name: storageNameWeb_var +// ========== Storage Accounts ========== +// Web content storage (for blog posts and assets) +resource storageWeb 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: storageNameWeb location: location kind: 'StorageV2' sku: { @@ -169,10 +200,11 @@ resource storageWeb 'Microsoft.Storage/storageAccounts@2021-09-01' = { accessTier: 'Hot' minimumTlsVersion: 'TLS1_2' supportsHttpsTrafficOnly: true + allowBlobPublicAccess: false } } -resource storageWebBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2021-09-01' = { +resource storageWebBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = { parent: storageWeb name: 'default' properties: { @@ -198,8 +230,9 @@ resource storageWebBlobServices 'Microsoft.Storage/storageAccounts/blobServices@ } } -resource storageFunction 'Microsoft.Storage/storageAccounts@2021-09-01' = { - name: storageFunction_var +// Function app storage (for function runtime) +resource storageFunction 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: storageFunctionName location: location sku: { name: 'Standard_LRS' @@ -230,8 +263,9 @@ resource storageFunction 'Microsoft.Storage/storageAccounts@2021-09-01' = { } } -resource appPlan 'Microsoft.Web/serverfarms@2022-03-01' = { - name: appPlanName_var +// ========== App Service Plan (Consumption) ========== +resource appPlan 'Microsoft.Web/serverfarms@2023-01-01' = { + name: appPlanName location: location sku: { name: 'Y1' @@ -241,7 +275,9 @@ resource appPlan 'Microsoft.Web/serverfarms@2022-03-01' = { } } -resource functionEngine 'Microsoft.Web/sites@2022-03-01' = { +// ========== Function Apps ========== +// Backend engine function (authenticated API) +resource functionEngine 'Microsoft.Web/sites@2023-01-01' = { name: functionEngineName location: location kind: 'functionapp' @@ -260,11 +296,11 @@ resource functionEngine 'Microsoft.Web/sites@2022-03-01' = { } { name: 'AzureWebJobsStorage' - value: 'DefaultEndpointsProtocol=https;AccountName=${storageFunction_var};AccountKey=${storageFunction.listKeys().keys[0].value}' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageFunctionName};AccountKey=${storageFunction.listKeys().keys[0].value}' } { name: 'AzureStorageConnection' - value: 'DefaultEndpointsProtocol=https;AccountName=${storageNameWeb_var};AccountKey=${storageWeb.listKeys().keys[0].value}' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageNameWeb};AccountKey=${storageWeb.listKeys().keys[0].value}' } { name: 'CosmosDBConnection' @@ -272,7 +308,7 @@ resource functionEngine 'Microsoft.Web/sites@2022-03-01' = { } { name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' - value: 'DefaultEndpointsProtocol=https;AccountName=${storageFunction_var};AccountKey=${storageFunction.listKeys().keys[0].value}' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageFunctionName};AccountKey=${storageFunction.listKeys().keys[0].value}' } { name: 'WEBSITE_CONTENTSHARE' @@ -315,20 +351,29 @@ resource functionEngine 'Microsoft.Web/sites@2022-03-01' = { } } -resource functionEngine_authsettings 'Microsoft.Web/sites/config@2016-08-01' = { +// Authentication settings for backend engine +resource functionEngine_authsettings 'Microsoft.Web/sites/config@2023-01-01' = { parent: functionEngine - name: 'authsettings' + name: 'authsettingsV2' properties: { - enabled: true - unauthenticatedClientAction: 'RedirectToLoginPage' - tokenStoreEnabled: true - defaultProvider: 'AzureActiveDirectory' - clientId: aadClientId - issuer: 'https://sts.windows.net/${aadTenant}/' + globalValidation: { + requireAuthentication: true + unauthenticatedClientAction: 'RedirectToLoginPage' + } + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + clientId: aadClientId + openIdIssuer: 'https://sts.windows.net/${aadTenant}/v2.0' + } + } + } } } -resource functionFrontend 'Microsoft.Web/sites@2022-03-01' = { +// Frontend function (public-facing blog) +resource functionFrontend 'Microsoft.Web/sites@2023-01-01' = { name: functionFrontendName location: location kind: 'functionapp' @@ -344,11 +389,11 @@ resource functionFrontend 'Microsoft.Web/sites@2022-03-01' = { appSettings: [ { name: 'AzureWebJobsStorage' - value: 'DefaultEndpointsProtocol=https;AccountName=${storageFunction_var};AccountKey=${storageFunction.listKeys().keys[0].value}' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageFunctionName};AccountKey=${storageFunction.listKeys().keys[0].value}' } { name: 'AzureStorageConnection' - value: 'DefaultEndpointsProtocol=https;AccountName=${storageNameWeb_var};AccountKey=${storageWeb.listKeys().keys[0].value}' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageNameWeb};AccountKey=${storageWeb.listKeys().keys[0].value}' } { name: 'CosmosDBConnection' @@ -356,7 +401,7 @@ resource functionFrontend 'Microsoft.Web/sites@2022-03-01' = { } { name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' - value: 'DefaultEndpointsProtocol=https;AccountName=${storageFunction_var};AccountKey=${storageFunction.listKeys().keys[0].value}' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageFunctionName};AccountKey=${storageFunction.listKeys().keys[0].value}' } { name: 'WEBSITE_CONTENTSHARE' @@ -392,7 +437,7 @@ resource functionFrontend 'Microsoft.Web/sites@2022-03-01' = { } // Custom domain binding for the frontend function (optional) -resource functionFrontendCustomDomain 'Microsoft.Web/sites/hostNameBindings@2022-03-01' = if (!empty(customDomainName)) { +resource functionFrontendCustomDomain 'Microsoft.Web/sites/hostNameBindings@2023-01-01' = if (!empty(customDomainName)) { parent: functionFrontend name: customDomainName properties: { @@ -402,22 +447,9 @@ resource functionFrontendCustomDomain 'Microsoft.Web/sites/hostNameBindings@2022 } } -resource appInsight 'Microsoft.Insights/components@2020-02-02' = { - name: appInsightName_var - location: location - kind: 'web' - properties: { - WorkspaceResourceId: logAnalytics.id - IngestionMode: 'LogAnalytics' - RetentionInDays: 30 - Application_Type: 'web' - Flow_Type: 'Redfield' - Request_Source: 'IbizaAIExtension' - } -} - -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { - name: replace(appInsightName_var, 'appi', 'log') +// ========== Monitoring ========== +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: replace(appInsightName, 'appi', 'log') location: location properties: { sku: { @@ -430,7 +462,20 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-previ } } -resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2022-05-15' = { +resource appInsight 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightName + location: location + kind: 'web' + properties: { + WorkspaceResourceId: logAnalytics.id + IngestionMode: 'LogAnalytics' + RetentionInDays: 30 + Application_Type: 'web' + } +} + +// ========== Cosmos DB (Table API) ========== +resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = { kind: 'GlobalDocumentDB' name: cosmosDbName location: location @@ -464,7 +509,7 @@ resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2022-05-15' = { } } -resource metadataTable 'Microsoft.DocumentDB/databaseAccounts/tables@2021-04-15' = { +resource metadataTable 'Microsoft.DocumentDB/databaseAccounts/tables@2023-11-15' = { parent: cosmosDb name: 'metadata' properties: { @@ -474,7 +519,7 @@ resource metadataTable 'Microsoft.DocumentDB/databaseAccounts/tables@2021-04-15' } } -resource metricTable 'Microsoft.DocumentDB/databaseAccounts/tables@2021-04-15' = { +resource metricTable 'Microsoft.DocumentDB/databaseAccounts/tables@2023-11-15' = { parent: cosmosDb name: 'metrics' properties: { diff --git a/Templates/resources.json b/Templates/resources.json index 36b5838..d4c70a5 100644 --- a/Templates/resources.json +++ b/Templates/resources.json @@ -5,51 +5,81 @@ "_generator": { "name": "bicep", "version": "0.37.4.10188", - "templateHash": "3009564044815152776" + "templateHash": "5282295872744831359" } }, "parameters": { "functionEngineName": { "type": "string", - "defaultValue": "func-blog-engine-we-prod-001" + "defaultValue": "func-blog-engine-we-prod-001", + "metadata": { + "description": "Name of the backend engine function app" + } }, "functionFrontendName": { "type": "string", - "defaultValue": "func-blog-engine-we-prod-001" + "defaultValue": "func-blog-engine-we-prod-001", + "metadata": { + "description": "Name of the frontend function app" + } }, "customDomainName": { "type": "string", - "defaultValue": "" + "defaultValue": "", + "metadata": { + "description": "Custom domain name for the frontend function (optional). Leave empty to use default azurewebsites.net domain" + } }, "aadClientId": { - "type": "string" + "type": "string", + "metadata": { + "description": "Azure Active Directory client ID for authentication" + } }, "aadTenant": { - "type": "string" + "type": "string", + "metadata": { + "description": "Azure Active Directory tenant for authentication" + } }, "cosmosDbisZoneRedundant": { - "type": "bool" + "type": "bool", + "metadata": { + "description": "Enable zone redundancy for Cosmos DB" + } }, "cosmosDbName": { - "type": "string" + "type": "string", + "metadata": { + "description": "Name of the Cosmos DB account" + } }, "staticWebAppName": { - "type": "string" + "type": "string", + "metadata": { + "description": "Name of the static web app for the editor" + } }, "location": { "type": "string", - "defaultValue": "westeurope" + "defaultValue": "westeurope", + "metadata": { + "description": "Azure region for resource deployment" + } }, "serviceBusName": { "type": "string", - "defaultValue": "sb-blog-we-prod-001" + "defaultValue": "sb-blog-we-prod-001", + "metadata": { + "description": "Name of the Service Bus namespace" + } } }, "variables": { - "appInsightName_var": "[replace(parameters('functionEngineName'), 'func', 'appi')]", - "appPlanName_var": "[replace(parameters('functionEngineName'), 'func', 'plan')]", - "storageNameWeb_var": "stblogstaticweprod001", - "storageFunction_var": "stblogfuncweprod001", + "appInsightName": "[replace(parameters('functionEngineName'), 'func', 'appi')]", + "appPlanName": "[replace(parameters('functionEngineName'), 'func', 'plan')]", + "storageNameWeb": "stblogstaticweprod001", + "storageFunctionName": "stblogfuncweprod001", "serviceBusReceiverRoleId": "4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0", "serviceBusSenderRoleId": "69a216fc-b8fb-44d8-bc22-1f3c2cd27a39", "blogDataContributorRoleId": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", @@ -58,7 +88,7 @@ "resources": [ { "type": "Microsoft.Web/staticSites", - "apiVersion": "2022-03-01", + "apiVersion": "2023-01-01", "name": "[parameters('staticWebAppName')]", "location": "[parameters('location')]", "tags": {}, @@ -78,16 +108,19 @@ }, { "type": "Microsoft.ServiceBus/namespaces", - "apiVersion": "2021-11-01", + "apiVersion": "2022-10-01-preview", "name": "[parameters('serviceBusName')]", "location": "[parameters('location')]", "sku": { "name": "Basic" + }, + "properties": { + "minimumTlsVersion": "1.2" } }, { "type": "Microsoft.ServiceBus/namespaces/queues", - "apiVersion": "2021-11-01", + "apiVersion": "2022-10-01-preview", "name": "[format('{0}/{1}', parameters('serviceBusName'), 'scheduled')]", "dependsOn": [ "[resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName'))]" @@ -95,7 +128,7 @@ }, { "type": "Microsoft.ServiceBus/namespaces/queues", - "apiVersion": "2021-11-01", + "apiVersion": "2022-10-01-preview", "name": "[format('{0}/{1}', parameters('serviceBusName'), 'created')]", "dependsOn": [ "[resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName'))]" @@ -104,91 +137,91 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageNameWeb_var'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var')), resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), variables('blogDataContributorRoleId'))]", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageNameWeb'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb')), resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), variables('blogDataContributorRoleId'))]", "properties": { - "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), '2022-03-01', 'full').identity.principalId]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), '2023-01-01', 'full').identity.principalId]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataContributorRoleId'))]", "principalType": "ServicePrincipal" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('functionFrontendName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb'))]" ] }, { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageNameWeb_var'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('blogDataContributorRoleId'))]", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageNameWeb'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('blogDataContributorRoleId'))]", "properties": { - "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2023-01-01', 'full').identity.principalId]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataContributorRoleId'))]", "principalType": "ServicePrincipal" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb'))]" ] }, { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunction_var'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), variables('blogDataOwnerRoleId'))]", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunctionName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName')), resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), variables('blogDataOwnerRoleId'))]", "properties": { - "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), '2022-03-01', 'full').identity.principalId]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), '2023-01-01', 'full').identity.principalId]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataOwnerRoleId'))]", "principalType": "ServicePrincipal" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('functionFrontendName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]" + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName'))]" ] }, { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunction_var'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), variables('serviceBusReceiverRoleId'))]", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunctionName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName')), resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), variables('serviceBusReceiverRoleId'))]", "properties": { - "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), '2022-03-01', 'full').identity.principalId]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionFrontendName')), '2023-01-01', 'full').identity.principalId]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataContributorRoleId'))]", "principalType": "ServicePrincipal" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('functionFrontendName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]" + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName'))]" ] }, { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunction_var'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('blogDataOwnerRoleId'))]", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunctionName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('blogDataOwnerRoleId'))]", "properties": { - "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2023-01-01', 'full').identity.principalId]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataOwnerRoleId'))]", "principalType": "ServicePrincipal" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]" + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName'))]" ] }, { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunction_var'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('serviceBusReceiverRoleId'))]", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageFunctionName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('serviceBusReceiverRoleId'))]", "properties": { - "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2023-01-01', 'full').identity.principalId]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('blogDataContributorRoleId'))]", "principalType": "ServicePrincipal" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]" + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName'))]" ] }, { @@ -197,7 +230,7 @@ "scope": "[format('Microsoft.ServiceBus/namespaces/{0}', parameters('serviceBusName'))]", "name": "[guid(resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('serviceBusReceiverRoleId'))]", "properties": { - "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2023-01-01', 'full').identity.principalId]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('serviceBusReceiverRoleId'))]", "principalType": "ServicePrincipal" }, @@ -212,7 +245,7 @@ "scope": "[format('Microsoft.ServiceBus/namespaces/{0}', parameters('serviceBusName'))]", "name": "[guid(resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName')), resourceId('Microsoft.Web/sites', parameters('functionEngineName')), variables('serviceBusSenderRoleId'))]", "properties": { - "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2022-03-01', 'full').identity.principalId]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionEngineName')), '2023-01-01', 'full').identity.principalId]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('serviceBusSenderRoleId'))]", "principalType": "ServicePrincipal" }, @@ -223,8 +256,8 @@ }, { "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2021-09-01", - "name": "[variables('storageNameWeb_var')]", + "apiVersion": "2023-01-01", + "name": "[variables('storageNameWeb')]", "location": "[parameters('location')]", "kind": "StorageV2", "sku": { @@ -233,13 +266,14 @@ "properties": { "accessTier": "Hot", "minimumTlsVersion": "TLS1_2", - "supportsHttpsTrafficOnly": true + "supportsHttpsTrafficOnly": true, + "allowBlobPublicAccess": false } }, { "type": "Microsoft.Storage/storageAccounts/blobServices", - "apiVersion": "2021-09-01", - "name": "[format('{0}/{1}', variables('storageNameWeb_var'), 'default')]", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}', variables('storageNameWeb'), 'default')]", "properties": { "changeFeed": { "enabled": true @@ -262,13 +296,13 @@ "isVersioningEnabled": true }, "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb'))]" ] }, { "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2021-09-01", - "name": "[variables('storageFunction_var')]", + "apiVersion": "2023-01-01", + "name": "[variables('storageFunctionName')]", "location": "[parameters('location')]", "sku": { "name": "Standard_LRS" @@ -300,8 +334,8 @@ }, { "type": "Microsoft.Web/serverfarms", - "apiVersion": "2022-03-01", - "name": "[variables('appPlanName_var')]", + "apiVersion": "2023-01-01", + "name": "[variables('appPlanName')]", "location": "[parameters('location')]", "sku": { "name": "Y1", @@ -311,7 +345,7 @@ }, { "type": "Microsoft.Web/sites", - "apiVersion": "2022-03-01", + "apiVersion": "2023-01-01", "name": "[parameters('functionEngineName')]", "location": "[parameters('location')]", "kind": "functionapp", @@ -319,7 +353,7 @@ "type": "SystemAssigned" }, "properties": { - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName_var'))]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName'))]", "siteConfig": { "minTlsVersion": "1.2", "netFrameworkVersion": "v8.0", @@ -330,11 +364,11 @@ }, { "name": "AzureWebJobsStorage", - "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunction_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), '2021-09-01').keys[0].value)]" + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunctionName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName')), '2023-01-01').keys[0].value)]" }, { "name": "AzureStorageConnection", - "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageNameWeb_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var')), '2021-09-01').keys[0].value)]" + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageNameWeb'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb')), '2023-01-01').keys[0].value)]" }, { "name": "CosmosDBConnection", @@ -342,7 +376,7 @@ }, { "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", - "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunction_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), '2021-09-01').keys[0].value)]" + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunctionName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName')), '2023-01-01').keys[0].value)]" }, { "name": "WEBSITE_CONTENTSHARE", @@ -362,7 +396,7 @@ }, { "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", - "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightName_var')), '2020-02-02').ConnectionString]" + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightName')), '2020-02-02').ConnectionString]" }, { "name": "FUNCTIONS_WORKER_RUNTIME", @@ -384,25 +418,32 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Insights/components', variables('appInsightName_var'))]", - "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName_var'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightName'))]", + "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName'))]", "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName'))]", "[resourceId('Microsoft.ServiceBus/namespaces', parameters('serviceBusName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb'))]" ] }, { "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[format('{0}/{1}', parameters('functionEngineName'), 'authsettings')]", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}', parameters('functionEngineName'), 'authsettingsV2')]", "properties": { - "enabled": true, - "unauthenticatedClientAction": "RedirectToLoginPage", - "tokenStoreEnabled": true, - "defaultProvider": "AzureActiveDirectory", - "clientId": "[parameters('aadClientId')]", - "issuer": "[format('https://sts.windows.net/{0}/', parameters('aadTenant'))]" + "globalValidation": { + "requireAuthentication": true, + "unauthenticatedClientAction": "RedirectToLoginPage" + }, + "identityProviders": { + "azureActiveDirectory": { + "enabled": true, + "registration": { + "clientId": "[parameters('aadClientId')]", + "openIdIssuer": "[format('https://sts.windows.net/{0}/v2.0', parameters('aadTenant'))]" + } + } + } }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('functionEngineName'))]" @@ -410,7 +451,7 @@ }, { "type": "Microsoft.Web/sites", - "apiVersion": "2022-03-01", + "apiVersion": "2023-01-01", "name": "[parameters('functionFrontendName')]", "location": "[parameters('location')]", "kind": "functionapp", @@ -418,7 +459,7 @@ "type": "SystemAssigned" }, "properties": { - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName_var'))]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName'))]", "httpsOnly": true, "siteConfig": { "minTlsVersion": "1.2", @@ -426,11 +467,11 @@ "appSettings": [ { "name": "AzureWebJobsStorage", - "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunction_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), '2021-09-01').keys[0].value)]" + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunctionName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName')), '2023-01-01').keys[0].value)]" }, { "name": "AzureStorageConnection", - "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageNameWeb_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var')), '2021-09-01').keys[0].value)]" + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageNameWeb'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb')), '2023-01-01').keys[0].value)]" }, { "name": "CosmosDBConnection", @@ -438,7 +479,7 @@ }, { "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", - "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunction_var'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var')), '2021-09-01').keys[0].value)]" + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('storageFunctionName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName')), '2023-01-01').keys[0].value)]" }, { "name": "WEBSITE_CONTENTSHARE", @@ -450,7 +491,7 @@ }, { "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", - "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightName_var')), '2020-02-02').ConnectionString]" + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightName')), '2020-02-02').ConnectionString]" }, { "name": "FUNCTIONS_WORKER_RUNTIME", @@ -472,17 +513,17 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Insights/components', variables('appInsightName_var'))]", - "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName_var'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightName'))]", + "[resourceId('Microsoft.Web/serverfarms', variables('appPlanName'))]", "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunction_var'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb_var'))]" + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageFunctionName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameWeb'))]" ] }, { "condition": "[not(empty(parameters('customDomainName')))]", "type": "Microsoft.Web/sites/hostNameBindings", - "apiVersion": "2022-03-01", + "apiVersion": "2023-01-01", "name": "[format('{0}/{1}', parameters('functionFrontendName'), parameters('customDomainName'))]", "properties": { "hostNameType": "Verified", @@ -493,28 +534,10 @@ "[resourceId('Microsoft.Web/sites', parameters('functionFrontendName'))]" ] }, - { - "type": "Microsoft.Insights/components", - "apiVersion": "2020-02-02", - "name": "[variables('appInsightName_var')]", - "location": "[parameters('location')]", - "kind": "web", - "properties": { - "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', replace(variables('appInsightName_var'), 'appi', 'log'))]", - "IngestionMode": "LogAnalytics", - "RetentionInDays": 30, - "Application_Type": "web", - "Flow_Type": "Redfield", - "Request_Source": "IbizaAIExtension" - }, - "dependsOn": [ - "[resourceId('Microsoft.OperationalInsights/workspaces', replace(variables('appInsightName_var'), 'appi', 'log'))]" - ] - }, { "type": "Microsoft.OperationalInsights/workspaces", - "apiVersion": "2021-12-01-preview", - "name": "[replace(variables('appInsightName_var'), 'appi', 'log')]", + "apiVersion": "2022-10-01", + "name": "[replace(variables('appInsightName'), 'appi', 'log')]", "location": "[parameters('location')]", "properties": { "sku": { @@ -526,9 +549,25 @@ } } }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightName')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', replace(variables('appInsightName'), 'appi', 'log'))]", + "IngestionMode": "LogAnalytics", + "RetentionInDays": 30, + "Application_Type": "web" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', replace(variables('appInsightName'), 'appi', 'log'))]" + ] + }, { "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2022-05-15", + "apiVersion": "2023-11-15", "name": "[parameters('cosmosDbName')]", "kind": "GlobalDocumentDB", "location": "[parameters('location')]", @@ -563,7 +602,7 @@ }, { "type": "Microsoft.DocumentDB/databaseAccounts/tables", - "apiVersion": "2021-04-15", + "apiVersion": "2023-11-15", "name": "[format('{0}/{1}', parameters('cosmosDbName'), 'metadata')]", "properties": { "resource": { @@ -576,7 +615,7 @@ }, { "type": "Microsoft.DocumentDB/databaseAccounts/tables", - "apiVersion": "2021-04-15", + "apiVersion": "2023-11-15", "name": "[format('{0}/{1}', parameters('cosmosDbName'), 'metrics')]", "properties": { "resource": { From 64ef4d273f5173c5e7ad22bac9ee6d4f880e4f82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:58:18 +0000 Subject: [PATCH 5/6] Add comprehensive migration documentation Co-authored-by: jhueppauff <20532954+jhueppauff@users.noreply.github.com> --- Templates/MIGRATION_NOTES.md | 160 +++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 Templates/MIGRATION_NOTES.md diff --git a/Templates/MIGRATION_NOTES.md b/Templates/MIGRATION_NOTES.md new file mode 100644 index 0000000..9430551 --- /dev/null +++ b/Templates/MIGRATION_NOTES.md @@ -0,0 +1,160 @@ +# Azure CDN Replacement Migration Notes + +## Overview +This migration removes Azure CDN from the ServerlessBlog infrastructure and adds direct custom domain support to the Azure Function Frontend. + +## Changes Made + +### 1. Removed Components +- **Azure CDN Profile** (`microsoft.cdn/profiles@2019-04-15`) +- **Azure CDN Endpoint** (`microsoft.cdn/profiles/endpoints@2019-04-15`) +- Parameters: `profileProperties` and `endpointProperties` +- Variables: `cdnProfileName_var` and `cdnEndpointName` + +### 2. Added Components +- **Custom Domain Support**: Optional custom domain binding for Function Frontend + - New parameter: `customDomainName` (string, optional) + - New resource: `functionFrontendCustomDomain` (conditional deployment based on customDomainName) + - Automatic SSL/TLS certificate provisioning via Azure + - SNI-based SSL enabled + +### 3. Infrastructure Improvements (Azure Verified Modules Pattern) +- Added `@description` decorators to all parameters for better documentation +- Organized bicep file with section comments: + - Parameters + - Variables + - Static Web App (Editor) + - Service Bus + - RBAC Role Definitions + - RBAC Role Assignments + - Storage Accounts + - App Service Plan + - Function Apps + - Monitoring + - Cosmos DB + +- Updated API versions to latest stable: + - Static Web Apps: `2023-01-01` + - Service Bus: `2022-10-01-preview` + - Storage Accounts: `2023-01-01` + - App Service Plan/Functions: `2023-01-01` + - Log Analytics: `2022-10-01` + - Cosmos DB: `2023-11-15` + +- Security enhancements: + - Added `httpsOnly: true` to Function Frontend + - Updated authentication to `authsettingsV2` for Function Engine + - Added `minimumTlsVersion: '1.2'` to Service Bus + - Explicitly set `allowBlobPublicAccess: false` on storage accounts + +- Code quality improvements: + - Removed `_var` suffix from variable names + - Added inline comments for RBAC role IDs + - Improved resource organization and readability + +## Migration Path + +### For Existing Deployments +1. **DNS Configuration** (if using custom domain): + - Create CNAME record pointing to `.azurewebsites.net` + - Update parameters file with your custom domain + +2. **Update Parameters**: + ```json + { + "customDomainName": { + "value": "blog.yourdomain.com" // or "" if not using custom domain + } + } + ``` + +3. **Deploy Updated Template**: + ```bash + az deployment group create \ + --resource-group \ + --template-file resources.bicep \ + --parameters resources.parameters.json + ``` + +4. **Clean Up Old CDN Resources** (manual step): + - The CDN resources won't be automatically deleted + - Delete them manually via Azure Portal or CLI to avoid ongoing charges + +### For New Deployments +- Simply deploy the updated template +- Optionally configure custom domain by setting the `customDomainName` parameter + +## Benefits + +1. **Simplified Architecture**: + - Fewer moving parts + - Reduced complexity + - Lower management overhead + +2. **Cost Optimization**: + - Eliminates CDN costs + - Azure Functions already provide good performance with global distribution + +3. **Better Security**: + - Direct HTTPS enforcement + - Managed SSL certificates + - Modern authentication (authsettingsV2) + +4. **Improved Maintainability**: + - Better documented code + - Latest API versions + - Follows Azure best practices + +## Custom Domain Configuration + +### Steps to Configure Custom Domain: + +1. **Deploy Infrastructure First**: + ```bash + az deployment group create \ + --resource-group \ + --template-file resources.bicep \ + --parameters resources.parameters.json + ``` + +2. **Configure DNS**: + - Add CNAME record in your DNS provider + - Point to: `.azurewebsites.net` + - Wait for DNS propagation (usually 5-15 minutes) + +3. **Update Parameters and Redeploy**: + ```json + { + "customDomainName": { + "value": "blog.yourdomain.com" + } + } + ``` + +4. **Verify**: + - Azure will automatically provision and bind an SSL certificate + - Access your blog at `https://blog.yourdomain.com` + +## Rollback Plan + +If you need to rollback to the previous CDN-based infrastructure: + +1. Revert to the previous version of the bicep template +2. Redeploy with the old parameters file +3. Update DNS to point back to the CDN endpoint + +## Testing + +All changes have been validated: +- ✅ Bicep compilation successful +- ✅ No CDN references remaining +- ✅ Custom domain resource correctly configured +- ✅ All API versions updated to stable releases +- ✅ Parameter validation successful + +## Support + +For questions or issues related to this migration, please: +1. Check the README.md for custom domain configuration +2. Review this migration document +3. Open an issue on GitHub if problems persist From 37ed5b7768a058de9875e861203bce8f41788101 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 10:26:13 +0000 Subject: [PATCH 6/6] Update pipelines to use Bicep directly instead of ARM templates Co-authored-by: jhueppauff <20532954+jhueppauff@users.noreply.github.com> --- .azdevops/resources.yml | 20 ++++++-------------- README.md | 16 ++++++++++++++++ Templates/resources.json | 4 ++-- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.azdevops/resources.yml b/.azdevops/resources.yml index c26e4d0..b34459e 100644 --- a/.azdevops/resources.yml +++ b/.azdevops/resources.yml @@ -25,22 +25,14 @@ pool: steps: - task: AzureCLI@2 + displayName: 'Deploy Bicep template' inputs: azureSubscription: $(azureSubscription) scriptType: bash scriptLocation: inlineScript inlineScript: | - az bicep build --file $(Build.SourcesDirectory)/Templates/resources.bicep - - - task: AzureResourceManagerTemplateDeployment@3 - inputs: - deploymentScope: 'Resource Group' - azureResourceManagerConnection: '$(azureSubscription)' - subscriptionId: '$(subscriptionId)' - action: 'Create Or Update Resource Group' - resourceGroupName: '$(rgName)' - location: 'West Europe' - templateLocation: 'Linked artifact' - csmFile: '$(Build.SourcesDirectory)/Templates/resources.json' - csmParametersFile: '$(Build.SourcesDirectory)/Templates/resources.parameters.json' - deploymentMode: 'Incremental' \ No newline at end of file + az deployment group create \ + --resource-group $(rgName) \ + --template-file $(Build.SourcesDirectory)/Templates/resources.bicep \ + --parameters $(Build.SourcesDirectory)/Templates/resources.parameters.json \ + --mode Incremental \ No newline at end of file diff --git a/README.md b/README.md index 7acd16b..bcbdd64 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,22 @@ Components used ## How to run +### Deploy with Azure CLI + +```bash +# Clone the repository +git clone https://github.com/jhueppauff/ServerlessBlog.git +cd ServerlessBlog/Templates + +# Deploy using Bicep +az deployment group create \ + --resource-group \ + --template-file resources.bicep \ + --parameters resources.parameters.json +``` + +Or use the Deploy to Azure button (requires the resources.json to be present): + [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fgithub.com%2Fjhueppauff%2FServerlessBlog%2Fblob%2Fmain%2FTemplates%2Fresources.json) ### Custom Domain Configuration diff --git a/Templates/resources.json b/Templates/resources.json index d4c70a5..2b33fce 100644 --- a/Templates/resources.json +++ b/Templates/resources.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "5282295872744831359" + "version": "0.38.33.27573", + "templateHash": "8057190160510650029" } }, "parameters": {