From adeb379b03dda26e8d37fa177e8c65e4a4005cb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:53:54 +0000 Subject: [PATCH 1/2] fix: remove subscription scope from dbWakeCustomRole to fix BCP139 Bicep error The dbWakeCustomRole resource in database.bicep had scope: subscription() which is invalid in a resource-group-scoped Bicep file (BCP139). Bicep validates this at compile time even when the resource is conditional (deployDbWakeRole: false). Removing the explicit scope deploys the custom role at resource group scope, which is valid for Microsoft.Authorization/roleDefinitions and follows the principle of least privilege (Constitution XIV). Agent-Logs-Url: https://github.com/microsoft/CommunityManagement-Sample-Spec-Kit/sessions/07c1698b-5fdb-4292-a6a3-3c5176050bb5 Co-authored-by: MikeWedderburn-Clarke <5323631+MikeWedderburn-Clarke@users.noreply.github.com> --- infra/main.json | 2470 ++++++++++++++++++++++++++++++++++ infra/modules/database.bicep | 1 - 2 files changed, 2470 insertions(+), 1 deletion(-) create mode 100644 infra/main.json diff --git a/infra/main.json b/infra/main.json new file mode 100644 index 0000000..102a048 --- /dev/null +++ b/infra/main.json @@ -0,0 +1,2470 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "5427124162675187423" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "metadata": { + "description": "Environment name (staging, production, or nightly)" + } + }, + "location": { + "type": "string", + "defaultValue": "eastus2", + "metadata": { + "description": "Azure region" + } + }, + "imageTag": { + "type": "string", + "metadata": { + "description": "Container image tag (git SHA)" + } + }, + "dbAdminLogin": { + "type": "securestring", + "metadata": { + "description": "PostgreSQL admin username" + } + }, + "dbAdminPassword": { + "type": "securestring", + "metadata": { + "description": "PostgreSQL admin password" + } + }, + "stripeSecretKey": { + "type": "securestring", + "metadata": { + "description": "Stripe API secret key" + } + }, + "stripeWebhookSecret": { + "type": "securestring", + "metadata": { + "description": "Stripe webhook signing secret" + } + }, + "stripeClientId": { + "type": "securestring", + "metadata": { + "description": "Stripe Connect client ID" + } + }, + "nextAuthSecret": { + "type": "securestring", + "metadata": { + "description": "NextAuth session encryption key" + } + }, + "entraClientId": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Entra External ID application (client) ID" + } + }, + "entraTenantId": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Entra External ID tenant UUID" + } + }, + "entraTenantDomain": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Entra External ID CIAM tenant subdomain (e.g. \"acroyogacommunity\" for acroyogacommunity.ciamlogin.com)" + } + }, + "customDomainHostname": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Custom domain hostname (optional)" + } + }, + "minReplicas": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Minimum Container App instances" + } + }, + "maxReplicas": { + "type": "int", + "defaultValue": 10, + "metadata": { + "description": "Maximum Container App instances" + } + }, + "dbSkuName": { + "type": "string", + "defaultValue": "Standard_B1ms", + "metadata": { + "description": "PostgreSQL SKU" + } + }, + "dbStorageSizeGB": { + "type": "int", + "defaultValue": 32, + "metadata": { + "description": "PostgreSQL storage in GB" + } + }, + "cpuCores": { + "type": "string", + "defaultValue": "0.5", + "metadata": { + "description": "CPU cores per container instance" + } + }, + "memorySize": { + "type": "string", + "defaultValue": "1Gi", + "metadata": { + "description": "Memory per container instance" + } + }, + "alertEmailAddress": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Email address for alert notifications" + } + }, + "githubOwnerId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "GitHub repository owner (organisation) numeric ID (e.g. \"6154722\"). When set together with githubRepoId, OIDC federated identity credentials are provisioned on the managed identity so GitHub Actions can authenticate without stored secrets." + } + }, + "githubRepoId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "GitHub repository numeric ID (e.g. \"1182392763\"). Required when githubOwnerId is set." + } + }, + "deployFrontDoor": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Deploy Azure Front Door CDN. Set to false for non-user-facing environments like nightly." + } + }, + "deployContainerRegistry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Deploy a Container Registry in this resource group. Set to false when using a shared ACR from another resource group." + } + }, + "sharedContainerRegistryLoginServer": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Shared Container Registry login server URL. Required when deployContainerRegistry is false." + } + }, + "deployMonitoringAlerts": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Deploy monitoring alert rules. Set to false for cost-optimized environments." + } + }, + "deployDbWakeRole": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Deploy the DB Wake custom role (requires subscription-level Microsoft.Authorization/roleDefinitions/write). Set to false for environments where the deploying identity only has resource-group-scoped permissions." + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "managed-identity", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "environmentName": { + "value": "[parameters('environmentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "githubOwnerId": { + "value": "[parameters('githubOwnerId')]" + }, + "githubRepoId": { + "value": "[parameters('githubRepoId')]" + }, + "addMainBranchFic": { + "value": "[equals(parameters('environmentName'), 'staging')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "14364185402530517229" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "metadata": { + "description": "Environment name (staging or production)" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region" + } + }, + "githubOwnerId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "GitHub repository owner (organisation) numeric ID (e.g. \"6154722\"). When set together with githubRepoId, OIDC federated identity credentials are created using the org-level customised subject format (Constitution XIV)." + } + }, + "githubRepoId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "GitHub repository numeric ID (e.g. \"1182392763\"). Required when githubOwnerId is set." + } + }, + "addMainBranchFic": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "When true, add a federated credential for the main branch push event (used by the build-and-push CI job). Only set to true for the staging environment." + } + } + }, + "variables": { + "identityName": "[format('id-acroyoga-{0}', parameters('environmentName'))]", + "gitHubIssuer": "https://token.actions.githubusercontent.com", + "ficAudiences": [ + "api://AzureADTokenExchange" + ] + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[variables('identityName')]", + "location": "[parameters('location')]", + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "condition": "[and(not(empty(parameters('githubOwnerId'))), not(empty(parameters('githubRepoId'))))]", + "type": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials", + "apiVersion": "2023-01-31", + "name": "[format('{0}/{1}', variables('identityName'), format('github-actions-env-{0}', parameters('environmentName')))]", + "properties": { + "issuer": "[variables('gitHubIssuer')]", + "subject": "[format('repository_owner_id:{0}:repository_id:{1}:environment:{2}', parameters('githubOwnerId'), parameters('githubRepoId'), parameters('environmentName'))]", + "audiences": "[variables('ficAudiences')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]" + ] + }, + { + "condition": "[and(and(parameters('addMainBranchFic'), not(empty(parameters('githubOwnerId')))), not(empty(parameters('githubRepoId'))))]", + "type": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials", + "apiVersion": "2023-01-31", + "name": "[format('{0}/{1}', variables('identityName'), 'github-actions-main-branch')]", + "properties": { + "issuer": "[variables('gitHubIssuer')]", + "subject": "[format('repository_owner_id:{0}:repository_id:{1}:ref:refs/heads/main', parameters('githubOwnerId'), parameters('githubRepoId'))]", + "audiences": "[variables('ficAudiences')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]" + ] + } + ], + "outputs": { + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), '2023-01-31').principalId]" + }, + "clientId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), '2023-01-31').clientId]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]" + }, + "name": { + "type": "string", + "value": "[variables('identityName')]" + } + } + } + } + }, + { + "condition": "[parameters('deployContainerRegistry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "container-registry", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "managedIdentityPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.principalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "10097197159664031238" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region" + } + }, + "managedIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of managed identity for AcrPull role" + } + } + }, + "variables": { + "uniqueSuffix": "[uniqueString(resourceGroup().id)]", + "registryName": "[format('acracroyoga{0}', variables('uniqueSuffix'))]", + "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d", + "acrPushRoleId": "8311e382-0749-4cb8-b61a-304f252e45ec" + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[variables('registryName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Basic" + }, + "properties": { + "adminUserEnabled": false + }, + "tags": { + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', variables('registryName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', variables('registryName')), parameters('managedIdentityPrincipalId'), variables('acrPullRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]", + "principalId": "[parameters('managedIdentityPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', variables('registryName'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', variables('registryName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', variables('registryName')), parameters('managedIdentityPrincipalId'), variables('acrPushRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPushRoleId'))]", + "principalId": "[parameters('managedIdentityPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', variables('registryName'))]" + ] + } + ], + "outputs": { + "loginServer": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', variables('registryName')), '2023-07-01').loginServer]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "monitoring", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "environmentName": { + "value": "[parameters('environmentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "alertEmailAddress": { + "value": "[parameters('alertEmailAddress')]" + }, + "enableAlertRules": { + "value": false + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "8244233152229045120" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "metadata": { + "description": "Environment name (staging or production)" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region" + } + }, + "alertEmailAddress": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Email address for alert action group notifications" + } + }, + "enableAlertRules": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Whether to create metric alert rules" + } + }, + "appInsightsResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Application Insights resource ID for alert scoping" + } + } + }, + "variables": { + "logAnalyticsName": "[format('log-acroyoga-{0}', parameters('environmentName'))]", + "appInsightsName": "[format('appi-acroyoga-{0}', parameters('environmentName'))]", + "actionGroupName": "[format('ag-acroyoga-{0}', parameters('environmentName'))]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2023-09-01", + "name": "[variables('logAnalyticsName')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30 + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" + ] + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('alertEmailAddress'), '')))]", + "type": "Microsoft.Insights/actionGroups", + "apiVersion": "2023-01-01", + "name": "[variables('actionGroupName')]", + "location": "global", + "properties": { + "groupShortName": "acroyoga", + "enabled": true, + "emailReceivers": [ + { + "name": "ops-email", + "emailAddress": "[parameters('alertEmailAddress')]" + } + ] + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", + "type": "Microsoft.Insights/metricAlerts", + "apiVersion": "2018-03-01", + "name": "[format('alert-http5xx-{0}', parameters('environmentName'))]", + "location": "global", + "properties": { + "severity": 2, + "enabled": true, + "scopes": [ + "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" + ], + "evaluationFrequency": "PT1M", + "windowSize": "PT5M", + "criteria": { + "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", + "allOf": [ + { + "name": "http5xx", + "metricName": "requests/failed", + "metricNamespace": "microsoft.insights/components", + "operator": "GreaterThan", + "threshold": 5, + "timeAggregation": "Count", + "criterionType": "StaticThresholdCriterion" + } + ] + }, + "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", + "type": "Microsoft.Insights/metricAlerts", + "apiVersion": "2018-03-01", + "name": "[format('alert-response-time-{0}', parameters('environmentName'))]", + "location": "global", + "properties": { + "severity": 3, + "enabled": true, + "scopes": [ + "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" + ], + "evaluationFrequency": "PT1M", + "windowSize": "PT5M", + "criteria": { + "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", + "allOf": [ + { + "name": "responseTime", + "metricName": "requests/duration", + "metricNamespace": "microsoft.insights/components", + "operator": "GreaterThan", + "threshold": 2000, + "timeAggregation": "Average", + "criterionType": "StaticThresholdCriterion" + } + ] + }, + "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", + "type": "Microsoft.Insights/metricAlerts", + "apiVersion": "2018-03-01", + "name": "[format('alert-container-restart-{0}', parameters('environmentName'))]", + "location": "global", + "properties": { + "severity": 2, + "enabled": true, + "scopes": [ + "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" + ], + "evaluationFrequency": "PT1M", + "windowSize": "PT10M", + "criteria": { + "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", + "allOf": [ + { + "name": "containerRestarts", + "metricName": "exceptions/count", + "metricNamespace": "microsoft.insights/components", + "operator": "GreaterThan", + "threshold": 3, + "timeAggregation": "Count", + "criterionType": "StaticThresholdCriterion" + } + ] + }, + "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", + "type": "Microsoft.Insights/metricAlerts", + "apiVersion": "2018-03-01", + "name": "[format('alert-db-connection-{0}', parameters('environmentName'))]", + "location": "global", + "properties": { + "severity": 1, + "enabled": true, + "scopes": [ + "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" + ], + "evaluationFrequency": "PT1M", + "windowSize": "PT5M", + "criteria": { + "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", + "allOf": [ + { + "name": "dbConnectionFailures", + "metricName": "dependencies/failed", + "metricNamespace": "microsoft.insights/components", + "operator": "GreaterThan", + "threshold": 0, + "timeAggregation": "Count", + "criterionType": "StaticThresholdCriterion" + } + ] + }, + "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + } + ], + "outputs": { + "logAnalyticsWorkspaceId": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" + }, + "appInsightsResourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + }, + "appInsightsConnectionString": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').ConnectionString]" + }, + "appInsightsInstrumentationKey": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').InstrumentationKey]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "database", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "environmentName": { + "value": "[parameters('environmentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "adminLogin": { + "value": "[parameters('dbAdminLogin')]" + }, + "adminPassword": { + "value": "[parameters('dbAdminPassword')]" + }, + "skuName": { + "value": "[parameters('dbSkuName')]" + }, + "storageSizeGB": { + "value": "[parameters('dbStorageSizeGB')]" + }, + "managedIdentityPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.principalId.value]" + }, + "managedIdentityClientId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.clientId.value]" + }, + "deployDbWakeRole": { + "value": "[parameters('deployDbWakeRole')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "2284794750875142805" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "metadata": { + "description": "Environment name (staging or production)" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region" + } + }, + "adminLogin": { + "type": "securestring", + "metadata": { + "description": "PostgreSQL admin username" + } + }, + "adminPassword": { + "type": "securestring", + "metadata": { + "description": "PostgreSQL admin password" + } + }, + "skuName": { + "type": "string", + "defaultValue": "Standard_B1ms", + "metadata": { + "description": "PostgreSQL SKU name" + } + }, + "storageSizeGB": { + "type": "int", + "defaultValue": 32, + "metadata": { + "description": "Storage size in GB" + } + }, + "managedIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the managed identity for Entra admin" + } + }, + "managedIdentityClientId": { + "type": "string", + "metadata": { + "description": "Client ID of the managed identity (used as DB username for token auth)" + } + }, + "deployDbWakeRole": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Deploy the DB Wake custom role and assignment. Set to false when the deploying identity does not have subscription-level Microsoft.Authorization/roleDefinitions/write permission (e.g. nightly)." + } + } + }, + "variables": { + "uniqueSuffix": "[uniqueString(resourceGroup().id)]", + "serverName": "[format('psql-acro-{0}-{1}', parameters('environmentName'), variables('uniqueSuffix'))]", + "databaseName": "acroyoga" + }, + "resources": [ + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers", + "apiVersion": "2023-12-01-preview", + "name": "[variables('serverName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('skuName')]", + "tier": "Burstable" + }, + "properties": { + "version": "16", + "administratorLogin": "[parameters('adminLogin')]", + "administratorLoginPassword": "[parameters('adminPassword')]", + "authConfig": { + "activeDirectoryAuth": "Enabled", + "passwordAuth": "Enabled", + "tenantId": "[subscription().tenantId]" + }, + "storage": { + "storageSizeGB": "[parameters('storageSizeGB')]", + "autoGrow": "Enabled" + }, + "backup": { + "backupRetentionDays": "[if(equals(parameters('environmentName'), 'production'), 14, 7)]", + "geoRedundantBackup": "Disabled" + }, + "highAvailability": { + "mode": "Disabled" + }, + "network": { + "publicNetworkAccess": "Enabled" + } + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers/administrators", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', variables('serverName'), parameters('managedIdentityPrincipalId'))]", + "properties": { + "principalName": "[format('id-acroyoga-{0}', parameters('environmentName'))]", + "principalType": "ServicePrincipal", + "tenantId": "[subscription().tenantId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/configurations', variables('serverName'), 'require_secure_transport')]" + ] + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers/configurations", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', variables('serverName'), 'require_secure_transport')]", + "properties": { + "value": "on", + "source": "user-override" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" + ] + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', variables('serverName'), 'AllowAzureServices')]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "0.0.0.0" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" + ] + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers/databases", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', variables('serverName'), variables('databaseName'))]", + "properties": { + "charset": "UTF8", + "collation": "en_US.utf8" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" + ] + }, + { + "condition": "[parameters('deployDbWakeRole')]", + "type": "Microsoft.Authorization/roleDefinitions", + "apiVersion": "2022-04-01", + "name": "[guid('db-wake-role', subscription().subscriptionId, resourceGroup().id)]", + "properties": { + "roleName": "[format('AcroYoga DB Wake - {0}', parameters('environmentName'))]", + "description": "Allows starting and reading the AcroYoga PostgreSQL Flexible Server. Assigned to the Container App Managed Identity for database wake functionality.", + "type": "CustomRole", + "permissions": [ + { + "actions": [ + "Microsoft.DBforPostgreSQL/flexibleServers/read", + "Microsoft.DBforPostgreSQL/flexibleServers/start/action" + ], + "notActions": [], + "dataActions": [], + "notDataActions": [] + } + ], + "assignableScopes": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" + ] + }, + { + "condition": "[parameters('deployDbWakeRole')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]", + "name": "[guid(resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName')), parameters('managedIdentityPrincipalId'), 'db-wake')]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', guid('db-wake-role', subscription().subscriptionId, resourceGroup().id))]", + "principalId": "[parameters('managedIdentityPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Authorization/roleDefinitions', guid('db-wake-role', subscription().subscriptionId, resourceGroup().id))]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" + ] + } + ], + "outputs": { + "serverFqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName')), '2023-12-01-preview').fullyQualifiedDomainName]" + }, + "connectionString": { + "type": "string", + "value": "[format('postgresql://{0}:{1}@{2}:5432/{3}?sslmode=require', parameters('adminLogin'), parameters('adminPassword'), reference(resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName')), '2023-12-01-preview').fullyQualifiedDomainName, variables('databaseName'))]" + }, + "databaseName": { + "type": "string", + "value": "[variables('databaseName')]" + }, + "serverHost": { + "type": "string", + "value": "[reference(resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName')), '2023-12-01-preview').fullyQualifiedDomainName]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storage", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "environmentName": { + "value": "[parameters('environmentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "managedIdentityPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.principalId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "11709574374180075184" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "metadata": { + "description": "Environment name (staging or production)" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region" + } + }, + "managedIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of managed identity for Storage Blob Data Contributor role" + } + } + }, + "variables": { + "uniqueSuffix": "[uniqueString(resourceGroup().id)]", + "storageAccountName": "[take(format('stacroyoga{0}{1}', parameters('environmentName'), variables('uniqueSuffix')), 24)]", + "storageBlobContributorRoleId": "ba92f5b4-2d11-453d-a403-e96b0029c9fe" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-05-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "accessTier": "Hot", + "allowBlobPublicAccess": false, + "minimumTlsVersion": "TLS1_2", + "supportsHttpsTrafficOnly": true + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', variables('storageAccountName'), 'default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', 'media')]", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), parameters('managedIdentityPrincipalId'), variables('storageBlobContributorRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('storageBlobContributorRoleId'))]", + "principalId": "[parameters('managedIdentityPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ] + } + ], + "outputs": { + "blobEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-05-01').primaryEndpoints.blob]" + }, + "accountName": { + "type": "string", + "value": "[variables('storageAccountName')]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "key-vault", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "environmentName": { + "value": "[parameters('environmentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "managedIdentityPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.principalId.value]" + }, + "logAnalyticsWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" + }, + "secrets": { + "value": { + "databaseUrl": "[reference(resourceId('Microsoft.Resources/deployments', 'database'), '2025-04-01').outputs.connectionString.value]", + "nextAuthSecret": "[parameters('nextAuthSecret')]", + "nextAuthUrl": "[if(not(equals(parameters('customDomainHostname'), '')), format('https://{0}', parameters('customDomainHostname')), 'https://placeholder.azurecontainerapps.io')]", + "stripeSecretKey": "[parameters('stripeSecretKey')]", + "stripeWebhookSecret": "[parameters('stripeWebhookSecret')]", + "stripeClientId": "[parameters('stripeClientId')]", + "applicationInsightsConnectionString": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]", + "entraClientId": "[parameters('entraClientId')]", + "entraTenantId": "[parameters('entraTenantId')]" + } + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "1045468276900666757" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "metadata": { + "description": "Environment name (staging or production)" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region" + } + }, + "managedIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of managed identity for Key Vault Secrets User role" + } + }, + "secrets": { + "type": "secureObject", + "defaultValue": {}, + "metadata": { + "description": "Secrets to populate in Key Vault (key-value pairs)" + } + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Log Analytics workspace ID for diagnostic audit logs" + } + } + }, + "variables": { + "uniqueSuffix": "[uniqueString(resourceGroup().id)]", + "vaultName": "[take(format('kv-acro-{0}-{1}', parameters('environmentName'), variables('uniqueSuffix')), 24)]", + "kvSecretsUserRoleId": "4633458b-17de-408a-b874-0445c86b69e6" + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2023-07-01", + "name": "[variables('vaultName')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "family": "A", + "name": "standard" + }, + "tenantId": "[subscription().tenantId]", + "enableRbacAuthorization": true, + "enableSoftDelete": true, + "softDeleteRetentionInDays": 90, + "enablePurgeProtection": true + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]", + "name": "[guid(resourceId('Microsoft.KeyVault/vaults', variables('vaultName')), parameters('managedIdentityPrincipalId'), variables('kvSecretsUserRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('kvSecretsUserRoleId'))]", + "principalId": "[parameters('managedIdentityPrincipalId')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[contains(parameters('secrets'), 'databaseUrl')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('vaultName'), 'database-url')]", + "properties": { + "value": "[coalesce(tryGet(parameters('secrets'), 'databaseUrl'), '')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[contains(parameters('secrets'), 'nextAuthSecret')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('vaultName'), 'nextauth-secret')]", + "properties": { + "value": "[coalesce(tryGet(parameters('secrets'), 'nextAuthSecret'), '')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[contains(parameters('secrets'), 'nextAuthUrl')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('vaultName'), 'nextauth-url')]", + "properties": { + "value": "[coalesce(tryGet(parameters('secrets'), 'nextAuthUrl'), '')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[contains(parameters('secrets'), 'stripeSecretKey')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('vaultName'), 'stripe-secret-key')]", + "properties": { + "value": "[coalesce(tryGet(parameters('secrets'), 'stripeSecretKey'), '')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[contains(parameters('secrets'), 'stripeWebhookSecret')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('vaultName'), 'stripe-webhook-secret')]", + "properties": { + "value": "[coalesce(tryGet(parameters('secrets'), 'stripeWebhookSecret'), '')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[contains(parameters('secrets'), 'stripeClientId')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('vaultName'), 'stripe-client-id')]", + "properties": { + "value": "[coalesce(tryGet(parameters('secrets'), 'stripeClientId'), '')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[contains(parameters('secrets'), 'applicationInsightsConnectionString')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('vaultName'), 'applicationinsights-connection-string')]", + "properties": { + "value": "[coalesce(tryGet(parameters('secrets'), 'applicationInsightsConnectionString'), '')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[contains(parameters('secrets'), 'entraClientId')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('vaultName'), 'entra-client-id')]", + "properties": { + "value": "[parameters('secrets').entraClientId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[contains(parameters('secrets'), 'entraTenantId')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('vaultName'), 'entra-tenant-id')]", + "properties": { + "value": "[parameters('secrets').entraTenantId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + }, + { + "condition": "[not(equals(parameters('logAnalyticsWorkspaceId'), ''))]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]", + "name": "kv-diagnostics", + "properties": { + "workspaceId": "[parameters('logAnalyticsWorkspaceId')]", + "logs": [ + { + "categoryGroup": "audit", + "enabled": true + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" + ] + } + ], + "outputs": { + "vaultName": { + "type": "string", + "value": "[variables('vaultName')]" + }, + "vaultUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.KeyVault/vaults', variables('vaultName')), '2023-07-01').vaultUri]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'database')]", + "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]", + "[resourceId('Microsoft.Resources/deployments', 'monitoring')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "container-apps", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "environmentName": { + "value": "[parameters('environmentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "containerRegistryLoginServer": "[if(parameters('deployContainerRegistry'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'container-registry'), '2025-04-01').outputs.loginServer.value), createObject('value', parameters('sharedContainerRegistryLoginServer')))]", + "imageTag": { + "value": "[parameters('imageTag')]" + }, + "managedIdentityId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.resourceId.value]" + }, + "managedIdentityClientId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.clientId.value]" + }, + "managedIdentityName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.name.value]" + }, + "keyVaultName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'key-vault'), '2025-04-01').outputs.vaultName.value]" + }, + "appInsightsConnectionString": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]" + }, + "logAnalyticsWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" + }, + "minReplicas": { + "value": "[parameters('minReplicas')]" + }, + "maxReplicas": { + "value": "[parameters('maxReplicas')]" + }, + "cpuCores": { + "value": "[parameters('cpuCores')]" + }, + "memorySize": { + "value": "[parameters('memorySize')]" + }, + "storageBlobEndpoint": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2025-04-01').outputs.blobEndpoint.value]" + }, + "pgHost": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'database'), '2025-04-01').outputs.serverHost.value]" + }, + "pgDatabase": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'database'), '2025-04-01').outputs.databaseName.value]" + }, + "entraTenantDomain": { + "value": "[parameters('entraTenantDomain')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "5943353407180863231" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "metadata": { + "description": "Environment name (staging or production)" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region" + } + }, + "containerRegistryLoginServer": { + "type": "string", + "metadata": { + "description": "ACR login server URL" + } + }, + "imageTag": { + "type": "string", + "metadata": { + "description": "Container image tag" + } + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "User-assigned managed identity resource ID" + } + }, + "managedIdentityClientId": { + "type": "string", + "metadata": { + "description": "User-assigned managed identity client ID" + } + }, + "managedIdentityName": { + "type": "string", + "metadata": { + "description": "User-assigned managed identity name (for PostgreSQL Entra auth user)" + } + }, + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Key Vault name for secret references" + } + }, + "appInsightsConnectionString": { + "type": "string", + "metadata": { + "description": "Application Insights connection string" + } + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "metadata": { + "description": "Log Analytics workspace resource ID" + } + }, + "storageBlobEndpoint": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Azure Storage blob endpoint URL for Managed Identity access" + } + }, + "pgHost": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "PostgreSQL server hostname for Managed Identity token auth" + } + }, + "pgDatabase": { + "type": "string", + "defaultValue": "acroyoga", + "metadata": { + "description": "PostgreSQL database name" + } + }, + "minReplicas": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Minimum replicas" + } + }, + "maxReplicas": { + "type": "int", + "defaultValue": 10, + "metadata": { + "description": "Maximum replicas" + } + }, + "cpuCores": { + "type": "string", + "defaultValue": "0.5", + "metadata": { + "description": "CPU cores per instance" + } + }, + "memorySize": { + "type": "string", + "defaultValue": "1Gi", + "metadata": { + "description": "Memory per instance" + } + }, + "entraTenantDomain": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Entra External ID CIAM tenant subdomain (e.g. \"acroyogacommunity\")" + } + } + }, + "variables": { + "environmentResourceName": "[format('cae-acroyoga-{0}', parameters('environmentName'))]", + "appName": "[format('ca-acroyoga-web-{0}', parameters('environmentName'))]", + "imageName": "[format('{0}/acroyoga-web:{1}', parameters('containerRegistryLoginServer'), parameters('imageTag'))]" + }, + "resources": [ + { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2024-03-01", + "name": "[variables('environmentResourceName')]", + "location": "[parameters('location')]", + "properties": { + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference(parameters('logAnalyticsWorkspaceId'), '2023-09-01').customerId]", + "sharedKey": "[listKeys(parameters('logAnalyticsWorkspaceId'), '2023-09-01').primarySharedKey]" + } + }, + "workloadProfiles": [ + { + "name": "Consumption", + "workloadProfileType": "Consumption" + } + ] + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "type": "Microsoft.App/containerApps", + "apiVersion": "2024-03-01", + "name": "[variables('appName')]", + "location": "[parameters('location')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('managedIdentityId'))]": {} + } + }, + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', variables('environmentResourceName'))]", + "configuration": { + "activeRevisionsMode": "Multiple", + "ingress": { + "external": true, + "targetPort": 3000, + "transport": "http" + }, + "registries": [ + { + "server": "[parameters('containerRegistryLoginServer')]", + "identity": "[parameters('managedIdentityId')]" + } + ], + "secrets": [ + { + "name": "database-url", + "keyVaultUrl": "[format('https://{0}{1}/secrets/database-url', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "identity": "[parameters('managedIdentityId')]" + }, + { + "name": "nextauth-secret", + "keyVaultUrl": "[format('https://{0}{1}/secrets/nextauth-secret', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "identity": "[parameters('managedIdentityId')]" + }, + { + "name": "nextauth-url", + "keyVaultUrl": "[format('https://{0}{1}/secrets/nextauth-url', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "identity": "[parameters('managedIdentityId')]" + }, + { + "name": "stripe-secret-key", + "keyVaultUrl": "[format('https://{0}{1}/secrets/stripe-secret-key', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "identity": "[parameters('managedIdentityId')]" + }, + { + "name": "stripe-webhook-secret", + "keyVaultUrl": "[format('https://{0}{1}/secrets/stripe-webhook-secret', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "identity": "[parameters('managedIdentityId')]" + }, + { + "name": "stripe-client-id", + "keyVaultUrl": "[format('https://{0}{1}/secrets/stripe-client-id', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "identity": "[parameters('managedIdentityId')]" + }, + { + "name": "applicationinsights-connection-string", + "keyVaultUrl": "[format('https://{0}{1}/secrets/applicationinsights-connection-string', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "identity": "[parameters('managedIdentityId')]" + }, + { + "name": "entra-client-id", + "keyVaultUrl": "[format('https://{0}{1}/secrets/entra-client-id', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "identity": "[parameters('managedIdentityId')]" + }, + { + "name": "entra-tenant-id", + "keyVaultUrl": "[format('https://{0}{1}/secrets/entra-tenant-id', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "identity": "[parameters('managedIdentityId')]" + } + ] + }, + "template": { + "containers": [ + { + "name": "web", + "image": "[variables('imageName')]", + "resources": { + "cpu": "[json(parameters('cpuCores'))]", + "memory": "[parameters('memorySize')]" + }, + "env": [ + { + "name": "DATABASE_URL", + "secretRef": "database-url" + }, + { + "name": "NEXTAUTH_SECRET", + "secretRef": "nextauth-secret" + }, + { + "name": "NEXTAUTH_URL", + "secretRef": "nextauth-url" + }, + { + "name": "STRIPE_SECRET_KEY", + "secretRef": "stripe-secret-key" + }, + { + "name": "STRIPE_WEBHOOK_SECRET", + "secretRef": "stripe-webhook-secret" + }, + { + "name": "STRIPE_CLIENT_ID", + "secretRef": "stripe-client-id" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "secretRef": "applicationinsights-connection-string" + }, + { + "name": "ENTRA_CLIENT_ID", + "secretRef": "entra-client-id" + }, + { + "name": "ENTRA_TENANT_ID", + "secretRef": "entra-tenant-id" + }, + { + "name": "NODE_ENV", + "value": "production" + }, + { + "name": "HOSTNAME", + "value": "0.0.0.0" + }, + { + "name": "PORT", + "value": "3000" + }, + { + "name": "AZURE_CLIENT_ID", + "value": "[parameters('managedIdentityClientId')]" + }, + { + "name": "AZURE_STORAGE_ACCOUNT_URL", + "value": "[parameters('storageBlobEndpoint')]" + }, + { + "name": "AZURE_SUBSCRIPTION_ID", + "value": "[subscription().subscriptionId]" + }, + { + "name": "AZURE_RESOURCE_GROUP", + "value": "[resourceGroup().name]" + }, + { + "name": "ENVIRONMENT_NAME", + "value": "[parameters('environmentName')]" + }, + { + "name": "PGHOST", + "value": "[parameters('pgHost')]" + }, + { + "name": "PGDATABASE", + "value": "[parameters('pgDatabase')]" + }, + { + "name": "PGUSER", + "value": "[parameters('managedIdentityName')]" + }, + { + "name": "ENTRA_TENANT_DOMAIN", + "value": "[parameters('entraTenantDomain')]" + } + ], + "probes": [ + { + "type": "Liveness", + "httpGet": { + "path": "/api/health", + "port": 3000 + }, + "periodSeconds": 10, + "timeoutSeconds": 5, + "failureThreshold": 3 + }, + { + "type": "Readiness", + "httpGet": { + "path": "/api/ready", + "port": 3000 + }, + "periodSeconds": 5, + "timeoutSeconds": 10, + "failureThreshold": 10 + }, + { + "type": "Startup", + "httpGet": { + "path": "/api/health", + "port": 3000 + }, + "periodSeconds": 5, + "timeoutSeconds": 5, + "failureThreshold": 30 + } + ] + } + ], + "scale": { + "minReplicas": "[parameters('minReplicas')]", + "maxReplicas": "[parameters('maxReplicas')]", + "rules": [ + { + "name": "http-scale", + "http": { + "metadata": { + "concurrentRequests": "20" + } + } + } + ] + } + } + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep", + "azd-service-name": "web" + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', variables('environmentResourceName'))]" + ] + } + ], + "outputs": { + "fqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/containerApps', variables('appName')), '2024-03-01').configuration.ingress.fqdn]" + }, + "environmentId": { + "type": "string", + "value": "[resourceId('Microsoft.App/managedEnvironments', variables('environmentResourceName'))]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'database')]", + "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]", + "[resourceId('Microsoft.Resources/deployments', 'key-vault')]", + "[resourceId('Microsoft.Resources/deployments', 'monitoring')]", + "[resourceId('Microsoft.Resources/deployments', 'container-registry')]", + "[resourceId('Microsoft.Resources/deployments', 'storage')]" + ] + }, + { + "condition": "[parameters('deployFrontDoor')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "front-door", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "originHostname": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'container-apps'), '2025-04-01').outputs.fqdn.value]" + }, + "customDomainHostname": { + "value": "[parameters('customDomainHostname')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "12581537020446223132" + } + }, + "parameters": { + "originHostname": { + "type": "string", + "metadata": { + "description": "Container App FQDN (backend origin)" + } + }, + "customDomainHostname": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Custom domain hostname (optional)" + } + }, + "wafPolicyName": { + "type": "string", + "defaultValue": "wafacroyoga", + "metadata": { + "description": "WAF policy name (alphanumeric only)" + } + } + }, + "variables": { + "uniqueSuffix": "[uniqueString(resourceGroup().id)]", + "frontDoorName": "afd-acroyoga", + "originGroupName": "default", + "originName": "web", + "routeName": "default-route", + "endpointName": "[format('acro-{0}', variables('uniqueSuffix'))]" + }, + "resources": [ + { + "type": "Microsoft.Network/FrontDoorWebApplicationFirewallPolicies", + "apiVersion": "2024-02-01", + "name": "[parameters('wafPolicyName')]", + "location": "global", + "sku": { + "name": "Standard_AzureFrontDoor" + }, + "properties": { + "policySettings": { + "enabledState": "Enabled", + "mode": "Prevention" + }, + "managedRules": { + "managedRuleSets": [] + } + }, + "tags": { + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "type": "Microsoft.Cdn/profiles", + "apiVersion": "2024-02-01", + "name": "[variables('frontDoorName')]", + "location": "global", + "sku": { + "name": "Standard_AzureFrontDoor" + }, + "tags": { + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "type": "Microsoft.Cdn/profiles/afdEndpoints", + "apiVersion": "2024-02-01", + "name": "[format('{0}/{1}', variables('frontDoorName'), variables('endpointName'))]", + "location": "global", + "properties": { + "enabledState": "Enabled" + }, + "dependsOn": [ + "[resourceId('Microsoft.Cdn/profiles', variables('frontDoorName'))]" + ] + }, + { + "type": "Microsoft.Cdn/profiles/originGroups", + "apiVersion": "2024-02-01", + "name": "[format('{0}/{1}', variables('frontDoorName'), variables('originGroupName'))]", + "properties": { + "loadBalancingSettings": { + "sampleSize": 4, + "successfulSamplesRequired": 3 + }, + "healthProbeSettings": { + "probePath": "/api/health", + "probeRequestType": "HEAD", + "probeProtocol": "Https", + "probeIntervalInSeconds": 30 + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Cdn/profiles', variables('frontDoorName'))]" + ] + }, + { + "type": "Microsoft.Cdn/profiles/originGroups/origins", + "apiVersion": "2024-02-01", + "name": "[format('{0}/{1}/{2}', variables('frontDoorName'), variables('originGroupName'), variables('originName'))]", + "properties": { + "hostName": "[parameters('originHostname')]", + "httpPort": 80, + "httpsPort": 443, + "originHostHeader": "[parameters('originHostname')]", + "priority": 1, + "weight": 1000 + }, + "dependsOn": [ + "[resourceId('Microsoft.Cdn/profiles/originGroups', variables('frontDoorName'), variables('originGroupName'))]" + ] + }, + { + "type": "Microsoft.Cdn/profiles/afdEndpoints/routes", + "apiVersion": "2024-02-01", + "name": "[format('{0}/{1}/{2}', variables('frontDoorName'), variables('endpointName'), variables('routeName'))]", + "properties": { + "originGroup": { + "id": "[resourceId('Microsoft.Cdn/profiles/originGroups', variables('frontDoorName'), variables('originGroupName'))]" + }, + "supportedProtocols": [ + "Http", + "Https" + ], + "patternsToMatch": [ + "/*" + ], + "forwardingProtocol": "HttpsOnly", + "linkToDefaultDomain": "Enabled", + "httpsRedirect": "Enabled", + "cacheConfiguration": { + "queryStringCachingBehavior": "UseQueryString", + "compressionSettings": { + "isCompressionEnabled": true, + "contentTypesToCompress": [ + "text/html", + "text/css", + "application/javascript", + "application/json", + "image/svg+xml" + ] + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Cdn/profiles/afdEndpoints', variables('frontDoorName'), variables('endpointName'))]", + "[resourceId('Microsoft.Cdn/profiles/originGroups/origins', variables('frontDoorName'), variables('originGroupName'), variables('originName'))]", + "[resourceId('Microsoft.Cdn/profiles/originGroups', variables('frontDoorName'), variables('originGroupName'))]" + ] + }, + { + "type": "Microsoft.Cdn/profiles/securityPolicies", + "apiVersion": "2024-02-01", + "name": "[format('{0}/{1}', variables('frontDoorName'), 'waf-policy')]", + "properties": { + "parameters": { + "type": "WebApplicationFirewall", + "wafPolicy": { + "id": "[resourceId('Microsoft.Network/FrontDoorWebApplicationFirewallPolicies', parameters('wafPolicyName'))]" + }, + "associations": [ + { + "domains": [ + { + "id": "[resourceId('Microsoft.Cdn/profiles/afdEndpoints', variables('frontDoorName'), variables('endpointName'))]" + } + ], + "patternsToMatch": [ + "/*" + ] + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Cdn/profiles/afdEndpoints', variables('frontDoorName'), variables('endpointName'))]", + "[resourceId('Microsoft.Cdn/profiles', variables('frontDoorName'))]", + "[resourceId('Microsoft.Network/FrontDoorWebApplicationFirewallPolicies', parameters('wafPolicyName'))]" + ] + } + ], + "outputs": { + "endpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Cdn/profiles/afdEndpoints', variables('frontDoorName'), variables('endpointName')), '2024-02-01').hostName]" + }, + "frontDoorId": { + "type": "string", + "value": "[resourceId('Microsoft.Cdn/profiles', variables('frontDoorName'))]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'container-apps')]" + ] + }, + { + "condition": "[parameters('deployMonitoringAlerts')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "monitoring-alerts", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "environmentName": { + "value": "[parameters('environmentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "alertEmailAddress": { + "value": "[parameters('alertEmailAddress')]" + }, + "enableAlertRules": { + "value": true + }, + "appInsightsResourceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsResourceId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "8244233152229045120" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "metadata": { + "description": "Environment name (staging or production)" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region" + } + }, + "alertEmailAddress": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Email address for alert action group notifications" + } + }, + "enableAlertRules": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Whether to create metric alert rules" + } + }, + "appInsightsResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Application Insights resource ID for alert scoping" + } + } + }, + "variables": { + "logAnalyticsName": "[format('log-acroyoga-{0}', parameters('environmentName'))]", + "appInsightsName": "[format('appi-acroyoga-{0}', parameters('environmentName'))]", + "actionGroupName": "[format('ag-acroyoga-{0}', parameters('environmentName'))]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2023-09-01", + "name": "[variables('logAnalyticsName')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30 + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" + ] + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('alertEmailAddress'), '')))]", + "type": "Microsoft.Insights/actionGroups", + "apiVersion": "2023-01-01", + "name": "[variables('actionGroupName')]", + "location": "global", + "properties": { + "groupShortName": "acroyoga", + "enabled": true, + "emailReceivers": [ + { + "name": "ops-email", + "emailAddress": "[parameters('alertEmailAddress')]" + } + ] + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + } + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", + "type": "Microsoft.Insights/metricAlerts", + "apiVersion": "2018-03-01", + "name": "[format('alert-http5xx-{0}', parameters('environmentName'))]", + "location": "global", + "properties": { + "severity": 2, + "enabled": true, + "scopes": [ + "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" + ], + "evaluationFrequency": "PT1M", + "windowSize": "PT5M", + "criteria": { + "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", + "allOf": [ + { + "name": "http5xx", + "metricName": "requests/failed", + "metricNamespace": "microsoft.insights/components", + "operator": "GreaterThan", + "threshold": 5, + "timeAggregation": "Count", + "criterionType": "StaticThresholdCriterion" + } + ] + }, + "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", + "type": "Microsoft.Insights/metricAlerts", + "apiVersion": "2018-03-01", + "name": "[format('alert-response-time-{0}', parameters('environmentName'))]", + "location": "global", + "properties": { + "severity": 3, + "enabled": true, + "scopes": [ + "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" + ], + "evaluationFrequency": "PT1M", + "windowSize": "PT5M", + "criteria": { + "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", + "allOf": [ + { + "name": "responseTime", + "metricName": "requests/duration", + "metricNamespace": "microsoft.insights/components", + "operator": "GreaterThan", + "threshold": 2000, + "timeAggregation": "Average", + "criterionType": "StaticThresholdCriterion" + } + ] + }, + "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", + "type": "Microsoft.Insights/metricAlerts", + "apiVersion": "2018-03-01", + "name": "[format('alert-container-restart-{0}', parameters('environmentName'))]", + "location": "global", + "properties": { + "severity": 2, + "enabled": true, + "scopes": [ + "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" + ], + "evaluationFrequency": "PT1M", + "windowSize": "PT10M", + "criteria": { + "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", + "allOf": [ + { + "name": "containerRestarts", + "metricName": "exceptions/count", + "metricNamespace": "microsoft.insights/components", + "operator": "GreaterThan", + "threshold": 3, + "timeAggregation": "Count", + "criterionType": "StaticThresholdCriterion" + } + ] + }, + "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", + "type": "Microsoft.Insights/metricAlerts", + "apiVersion": "2018-03-01", + "name": "[format('alert-db-connection-{0}', parameters('environmentName'))]", + "location": "global", + "properties": { + "severity": 1, + "enabled": true, + "scopes": [ + "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" + ], + "evaluationFrequency": "PT1M", + "windowSize": "PT5M", + "criteria": { + "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", + "allOf": [ + { + "name": "dbConnectionFailures", + "metricName": "dependencies/failed", + "metricNamespace": "microsoft.insights/components", + "operator": "GreaterThan", + "threshold": 0, + "timeAggregation": "Count", + "criterionType": "StaticThresholdCriterion" + } + ] + }, + "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" + }, + "tags": { + "environment": "[parameters('environmentName')]", + "project": "acroyoga-community", + "managedBy": "bicep" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + } + ], + "outputs": { + "logAnalyticsWorkspaceId": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" + }, + "appInsightsResourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + }, + "appInsightsConnectionString": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').ConnectionString]" + }, + "appInsightsInstrumentationKey": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').InstrumentationKey]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'monitoring')]" + ] + } + ], + "outputs": { + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "value": "[if(parameters('deployContainerRegistry'), reference(resourceId('Microsoft.Resources/deployments', 'container-registry'), '2025-04-01').outputs.loginServer.value, parameters('sharedContainerRegistryLoginServer'))]" + }, + "containerAppFqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'container-apps'), '2025-04-01').outputs.fqdn.value]" + }, + "frontDoorEndpoint": { + "type": "string", + "value": "[if(parameters('deployFrontDoor'), reference(resourceId('Microsoft.Resources/deployments', 'front-door'), '2025-04-01').outputs.endpoint.value, '')]" + }, + "containerRegistryLoginServer": { + "type": "string", + "value": "[if(parameters('deployContainerRegistry'), reference(resourceId('Microsoft.Resources/deployments', 'container-registry'), '2025-04-01').outputs.loginServer.value, parameters('sharedContainerRegistryLoginServer'))]" + } + } +} \ No newline at end of file diff --git a/infra/modules/database.bicep b/infra/modules/database.bicep index cdc0687..08139b6 100644 --- a/infra/modules/database.bicep +++ b/infra/modules/database.bicep @@ -117,7 +117,6 @@ resource database 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-12-0 // (Constitution XIV — least-privilege Managed Identity access) resource dbWakeCustomRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' = if (deployDbWakeRole) { - scope: subscription() name: guid('db-wake-role', subscription().subscriptionId, resourceGroup().id) properties: { roleName: 'AcroYoga DB Wake - ${environmentName}' From b11de953fce7fab7a846b7f94d666afa484836ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:54:08 +0000 Subject: [PATCH 2/2] chore: remove accidentally committed Bicep build artifact Co-authored-by: MikeWedderburn-Clarke <5323631+MikeWedderburn-Clarke@users.noreply.github.com> --- infra/main.json | 2470 ----------------------------------------------- 1 file changed, 2470 deletions(-) delete mode 100644 infra/main.json diff --git a/infra/main.json b/infra/main.json deleted file mode 100644 index 102a048..0000000 --- a/infra/main.json +++ /dev/null @@ -1,2470 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "5427124162675187423" - } - }, - "parameters": { - "environmentName": { - "type": "string", - "metadata": { - "description": "Environment name (staging, production, or nightly)" - } - }, - "location": { - "type": "string", - "defaultValue": "eastus2", - "metadata": { - "description": "Azure region" - } - }, - "imageTag": { - "type": "string", - "metadata": { - "description": "Container image tag (git SHA)" - } - }, - "dbAdminLogin": { - "type": "securestring", - "metadata": { - "description": "PostgreSQL admin username" - } - }, - "dbAdminPassword": { - "type": "securestring", - "metadata": { - "description": "PostgreSQL admin password" - } - }, - "stripeSecretKey": { - "type": "securestring", - "metadata": { - "description": "Stripe API secret key" - } - }, - "stripeWebhookSecret": { - "type": "securestring", - "metadata": { - "description": "Stripe webhook signing secret" - } - }, - "stripeClientId": { - "type": "securestring", - "metadata": { - "description": "Stripe Connect client ID" - } - }, - "nextAuthSecret": { - "type": "securestring", - "metadata": { - "description": "NextAuth session encryption key" - } - }, - "entraClientId": { - "type": "securestring", - "defaultValue": "", - "metadata": { - "description": "Entra External ID application (client) ID" - } - }, - "entraTenantId": { - "type": "securestring", - "defaultValue": "", - "metadata": { - "description": "Entra External ID tenant UUID" - } - }, - "entraTenantDomain": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Entra External ID CIAM tenant subdomain (e.g. \"acroyogacommunity\" for acroyogacommunity.ciamlogin.com)" - } - }, - "customDomainHostname": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Custom domain hostname (optional)" - } - }, - "minReplicas": { - "type": "int", - "defaultValue": 0, - "metadata": { - "description": "Minimum Container App instances" - } - }, - "maxReplicas": { - "type": "int", - "defaultValue": 10, - "metadata": { - "description": "Maximum Container App instances" - } - }, - "dbSkuName": { - "type": "string", - "defaultValue": "Standard_B1ms", - "metadata": { - "description": "PostgreSQL SKU" - } - }, - "dbStorageSizeGB": { - "type": "int", - "defaultValue": 32, - "metadata": { - "description": "PostgreSQL storage in GB" - } - }, - "cpuCores": { - "type": "string", - "defaultValue": "0.5", - "metadata": { - "description": "CPU cores per container instance" - } - }, - "memorySize": { - "type": "string", - "defaultValue": "1Gi", - "metadata": { - "description": "Memory per container instance" - } - }, - "alertEmailAddress": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Email address for alert notifications" - } - }, - "githubOwnerId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "GitHub repository owner (organisation) numeric ID (e.g. \"6154722\"). When set together with githubRepoId, OIDC federated identity credentials are provisioned on the managed identity so GitHub Actions can authenticate without stored secrets." - } - }, - "githubRepoId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "GitHub repository numeric ID (e.g. \"1182392763\"). Required when githubOwnerId is set." - } - }, - "deployFrontDoor": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy Azure Front Door CDN. Set to false for non-user-facing environments like nightly." - } - }, - "deployContainerRegistry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy a Container Registry in this resource group. Set to false when using a shared ACR from another resource group." - } - }, - "sharedContainerRegistryLoginServer": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Shared Container Registry login server URL. Required when deployContainerRegistry is false." - } - }, - "deployMonitoringAlerts": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy monitoring alert rules. Set to false for cost-optimized environments." - } - }, - "deployDbWakeRole": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy the DB Wake custom role (requires subscription-level Microsoft.Authorization/roleDefinitions/write). Set to false for environments where the deploying identity only has resource-group-scoped permissions." - } - } - }, - "resources": [ - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "managed-identity", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "environmentName": { - "value": "[parameters('environmentName')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "githubOwnerId": { - "value": "[parameters('githubOwnerId')]" - }, - "githubRepoId": { - "value": "[parameters('githubRepoId')]" - }, - "addMainBranchFic": { - "value": "[equals(parameters('environmentName'), 'staging')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "14364185402530517229" - } - }, - "parameters": { - "environmentName": { - "type": "string", - "metadata": { - "description": "Environment name (staging or production)" - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region" - } - }, - "githubOwnerId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "GitHub repository owner (organisation) numeric ID (e.g. \"6154722\"). When set together with githubRepoId, OIDC federated identity credentials are created using the org-level customised subject format (Constitution XIV)." - } - }, - "githubRepoId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "GitHub repository numeric ID (e.g. \"1182392763\"). Required when githubOwnerId is set." - } - }, - "addMainBranchFic": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "When true, add a federated credential for the main branch push event (used by the build-and-push CI job). Only set to true for the staging environment." - } - } - }, - "variables": { - "identityName": "[format('id-acroyoga-{0}', parameters('environmentName'))]", - "gitHubIssuer": "https://token.actions.githubusercontent.com", - "ficAudiences": [ - "api://AzureADTokenExchange" - ] - }, - "resources": [ - { - "type": "Microsoft.ManagedIdentity/userAssignedIdentities", - "apiVersion": "2023-01-31", - "name": "[variables('identityName')]", - "location": "[parameters('location')]", - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "condition": "[and(not(empty(parameters('githubOwnerId'))), not(empty(parameters('githubRepoId'))))]", - "type": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials", - "apiVersion": "2023-01-31", - "name": "[format('{0}/{1}', variables('identityName'), format('github-actions-env-{0}', parameters('environmentName')))]", - "properties": { - "issuer": "[variables('gitHubIssuer')]", - "subject": "[format('repository_owner_id:{0}:repository_id:{1}:environment:{2}', parameters('githubOwnerId'), parameters('githubRepoId'), parameters('environmentName'))]", - "audiences": "[variables('ficAudiences')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]" - ] - }, - { - "condition": "[and(and(parameters('addMainBranchFic'), not(empty(parameters('githubOwnerId')))), not(empty(parameters('githubRepoId'))))]", - "type": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials", - "apiVersion": "2023-01-31", - "name": "[format('{0}/{1}', variables('identityName'), 'github-actions-main-branch')]", - "properties": { - "issuer": "[variables('gitHubIssuer')]", - "subject": "[format('repository_owner_id:{0}:repository_id:{1}:ref:refs/heads/main', parameters('githubOwnerId'), parameters('githubRepoId'))]", - "audiences": "[variables('ficAudiences')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]" - ] - } - ], - "outputs": { - "principalId": { - "type": "string", - "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), '2023-01-31').principalId]" - }, - "clientId": { - "type": "string", - "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), '2023-01-31').clientId]" - }, - "resourceId": { - "type": "string", - "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]" - }, - "name": { - "type": "string", - "value": "[variables('identityName')]" - } - } - } - } - }, - { - "condition": "[parameters('deployContainerRegistry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "container-registry", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "managedIdentityPrincipalId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.principalId.value]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "10097197159664031238" - } - }, - "parameters": { - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region" - } - }, - "managedIdentityPrincipalId": { - "type": "string", - "metadata": { - "description": "Principal ID of managed identity for AcrPull role" - } - } - }, - "variables": { - "uniqueSuffix": "[uniqueString(resourceGroup().id)]", - "registryName": "[format('acracroyoga{0}', variables('uniqueSuffix'))]", - "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d", - "acrPushRoleId": "8311e382-0749-4cb8-b61a-304f252e45ec" - }, - "resources": [ - { - "type": "Microsoft.ContainerRegistry/registries", - "apiVersion": "2023-07-01", - "name": "[variables('registryName')]", - "location": "[parameters('location')]", - "sku": { - "name": "Basic" - }, - "properties": { - "adminUserEnabled": false - }, - "tags": { - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.ContainerRegistry/registries', variables('registryName'))]", - "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', variables('registryName')), parameters('managedIdentityPrincipalId'), variables('acrPullRoleId'))]", - "properties": { - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]", - "principalId": "[parameters('managedIdentityPrincipalId')]", - "principalType": "ServicePrincipal" - }, - "dependsOn": [ - "[resourceId('Microsoft.ContainerRegistry/registries', variables('registryName'))]" - ] - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.ContainerRegistry/registries', variables('registryName'))]", - "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', variables('registryName')), parameters('managedIdentityPrincipalId'), variables('acrPushRoleId'))]", - "properties": { - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPushRoleId'))]", - "principalId": "[parameters('managedIdentityPrincipalId')]", - "principalType": "ServicePrincipal" - }, - "dependsOn": [ - "[resourceId('Microsoft.ContainerRegistry/registries', variables('registryName'))]" - ] - } - ], - "outputs": { - "loginServer": { - "type": "string", - "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', variables('registryName')), '2023-07-01').loginServer]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]" - ] - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "monitoring", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "environmentName": { - "value": "[parameters('environmentName')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "alertEmailAddress": { - "value": "[parameters('alertEmailAddress')]" - }, - "enableAlertRules": { - "value": false - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "8244233152229045120" - } - }, - "parameters": { - "environmentName": { - "type": "string", - "metadata": { - "description": "Environment name (staging or production)" - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region" - } - }, - "alertEmailAddress": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Email address for alert action group notifications" - } - }, - "enableAlertRules": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Whether to create metric alert rules" - } - }, - "appInsightsResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Application Insights resource ID for alert scoping" - } - } - }, - "variables": { - "logAnalyticsName": "[format('log-acroyoga-{0}', parameters('environmentName'))]", - "appInsightsName": "[format('appi-acroyoga-{0}', parameters('environmentName'))]", - "actionGroupName": "[format('ag-acroyoga-{0}', parameters('environmentName'))]" - }, - "resources": [ - { - "type": "Microsoft.OperationalInsights/workspaces", - "apiVersion": "2023-09-01", - "name": "[variables('logAnalyticsName')]", - "location": "[parameters('location')]", - "properties": { - "sku": { - "name": "PerGB2018" - }, - "retentionInDays": 30 - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "type": "Microsoft.Insights/components", - "apiVersion": "2020-02-02", - "name": "[variables('appInsightsName')]", - "location": "[parameters('location')]", - "kind": "web", - "properties": { - "Application_Type": "web", - "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" - ] - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('alertEmailAddress'), '')))]", - "type": "Microsoft.Insights/actionGroups", - "apiVersion": "2023-01-01", - "name": "[variables('actionGroupName')]", - "location": "global", - "properties": { - "groupShortName": "acroyoga", - "enabled": true, - "emailReceivers": [ - { - "name": "ops-email", - "emailAddress": "[parameters('alertEmailAddress')]" - } - ] - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", - "type": "Microsoft.Insights/metricAlerts", - "apiVersion": "2018-03-01", - "name": "[format('alert-http5xx-{0}', parameters('environmentName'))]", - "location": "global", - "properties": { - "severity": 2, - "enabled": true, - "scopes": [ - "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" - ], - "evaluationFrequency": "PT1M", - "windowSize": "PT5M", - "criteria": { - "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", - "allOf": [ - { - "name": "http5xx", - "metricName": "requests/failed", - "metricNamespace": "microsoft.insights/components", - "operator": "GreaterThan", - "threshold": 5, - "timeAggregation": "Count", - "criterionType": "StaticThresholdCriterion" - } - ] - }, - "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", - "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - ] - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", - "type": "Microsoft.Insights/metricAlerts", - "apiVersion": "2018-03-01", - "name": "[format('alert-response-time-{0}', parameters('environmentName'))]", - "location": "global", - "properties": { - "severity": 3, - "enabled": true, - "scopes": [ - "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" - ], - "evaluationFrequency": "PT1M", - "windowSize": "PT5M", - "criteria": { - "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", - "allOf": [ - { - "name": "responseTime", - "metricName": "requests/duration", - "metricNamespace": "microsoft.insights/components", - "operator": "GreaterThan", - "threshold": 2000, - "timeAggregation": "Average", - "criterionType": "StaticThresholdCriterion" - } - ] - }, - "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", - "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - ] - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", - "type": "Microsoft.Insights/metricAlerts", - "apiVersion": "2018-03-01", - "name": "[format('alert-container-restart-{0}', parameters('environmentName'))]", - "location": "global", - "properties": { - "severity": 2, - "enabled": true, - "scopes": [ - "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" - ], - "evaluationFrequency": "PT1M", - "windowSize": "PT10M", - "criteria": { - "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", - "allOf": [ - { - "name": "containerRestarts", - "metricName": "exceptions/count", - "metricNamespace": "microsoft.insights/components", - "operator": "GreaterThan", - "threshold": 3, - "timeAggregation": "Count", - "criterionType": "StaticThresholdCriterion" - } - ] - }, - "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", - "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - ] - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", - "type": "Microsoft.Insights/metricAlerts", - "apiVersion": "2018-03-01", - "name": "[format('alert-db-connection-{0}', parameters('environmentName'))]", - "location": "global", - "properties": { - "severity": 1, - "enabled": true, - "scopes": [ - "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" - ], - "evaluationFrequency": "PT1M", - "windowSize": "PT5M", - "criteria": { - "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", - "allOf": [ - { - "name": "dbConnectionFailures", - "metricName": "dependencies/failed", - "metricNamespace": "microsoft.insights/components", - "operator": "GreaterThan", - "threshold": 0, - "timeAggregation": "Count", - "criterionType": "StaticThresholdCriterion" - } - ] - }, - "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", - "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - ] - } - ], - "outputs": { - "logAnalyticsWorkspaceId": { - "type": "string", - "value": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" - }, - "appInsightsResourceId": { - "type": "string", - "value": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - }, - "appInsightsConnectionString": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').ConnectionString]" - }, - "appInsightsInstrumentationKey": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').InstrumentationKey]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "database", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "environmentName": { - "value": "[parameters('environmentName')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "adminLogin": { - "value": "[parameters('dbAdminLogin')]" - }, - "adminPassword": { - "value": "[parameters('dbAdminPassword')]" - }, - "skuName": { - "value": "[parameters('dbSkuName')]" - }, - "storageSizeGB": { - "value": "[parameters('dbStorageSizeGB')]" - }, - "managedIdentityPrincipalId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.principalId.value]" - }, - "managedIdentityClientId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.clientId.value]" - }, - "deployDbWakeRole": { - "value": "[parameters('deployDbWakeRole')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "2284794750875142805" - } - }, - "parameters": { - "environmentName": { - "type": "string", - "metadata": { - "description": "Environment name (staging or production)" - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region" - } - }, - "adminLogin": { - "type": "securestring", - "metadata": { - "description": "PostgreSQL admin username" - } - }, - "adminPassword": { - "type": "securestring", - "metadata": { - "description": "PostgreSQL admin password" - } - }, - "skuName": { - "type": "string", - "defaultValue": "Standard_B1ms", - "metadata": { - "description": "PostgreSQL SKU name" - } - }, - "storageSizeGB": { - "type": "int", - "defaultValue": 32, - "metadata": { - "description": "Storage size in GB" - } - }, - "managedIdentityPrincipalId": { - "type": "string", - "metadata": { - "description": "Principal ID of the managed identity for Entra admin" - } - }, - "managedIdentityClientId": { - "type": "string", - "metadata": { - "description": "Client ID of the managed identity (used as DB username for token auth)" - } - }, - "deployDbWakeRole": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy the DB Wake custom role and assignment. Set to false when the deploying identity does not have subscription-level Microsoft.Authorization/roleDefinitions/write permission (e.g. nightly)." - } - } - }, - "variables": { - "uniqueSuffix": "[uniqueString(resourceGroup().id)]", - "serverName": "[format('psql-acro-{0}-{1}', parameters('environmentName'), variables('uniqueSuffix'))]", - "databaseName": "acroyoga" - }, - "resources": [ - { - "type": "Microsoft.DBforPostgreSQL/flexibleServers", - "apiVersion": "2023-12-01-preview", - "name": "[variables('serverName')]", - "location": "[parameters('location')]", - "sku": { - "name": "[parameters('skuName')]", - "tier": "Burstable" - }, - "properties": { - "version": "16", - "administratorLogin": "[parameters('adminLogin')]", - "administratorLoginPassword": "[parameters('adminPassword')]", - "authConfig": { - "activeDirectoryAuth": "Enabled", - "passwordAuth": "Enabled", - "tenantId": "[subscription().tenantId]" - }, - "storage": { - "storageSizeGB": "[parameters('storageSizeGB')]", - "autoGrow": "Enabled" - }, - "backup": { - "backupRetentionDays": "[if(equals(parameters('environmentName'), 'production'), 14, 7)]", - "geoRedundantBackup": "Disabled" - }, - "highAvailability": { - "mode": "Disabled" - }, - "network": { - "publicNetworkAccess": "Enabled" - } - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "type": "Microsoft.DBforPostgreSQL/flexibleServers/administrators", - "apiVersion": "2023-12-01-preview", - "name": "[format('{0}/{1}', variables('serverName'), parameters('managedIdentityPrincipalId'))]", - "properties": { - "principalName": "[format('id-acroyoga-{0}', parameters('environmentName'))]", - "principalType": "ServicePrincipal", - "tenantId": "[subscription().tenantId]" - }, - "dependsOn": [ - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]", - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/configurations', variables('serverName'), 'require_secure_transport')]" - ] - }, - { - "type": "Microsoft.DBforPostgreSQL/flexibleServers/configurations", - "apiVersion": "2023-12-01-preview", - "name": "[format('{0}/{1}', variables('serverName'), 'require_secure_transport')]", - "properties": { - "value": "on", - "source": "user-override" - }, - "dependsOn": [ - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" - ] - }, - { - "type": "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules", - "apiVersion": "2023-12-01-preview", - "name": "[format('{0}/{1}', variables('serverName'), 'AllowAzureServices')]", - "properties": { - "startIpAddress": "0.0.0.0", - "endIpAddress": "0.0.0.0" - }, - "dependsOn": [ - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" - ] - }, - { - "type": "Microsoft.DBforPostgreSQL/flexibleServers/databases", - "apiVersion": "2023-12-01-preview", - "name": "[format('{0}/{1}', variables('serverName'), variables('databaseName'))]", - "properties": { - "charset": "UTF8", - "collation": "en_US.utf8" - }, - "dependsOn": [ - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" - ] - }, - { - "condition": "[parameters('deployDbWakeRole')]", - "type": "Microsoft.Authorization/roleDefinitions", - "apiVersion": "2022-04-01", - "name": "[guid('db-wake-role', subscription().subscriptionId, resourceGroup().id)]", - "properties": { - "roleName": "[format('AcroYoga DB Wake - {0}', parameters('environmentName'))]", - "description": "Allows starting and reading the AcroYoga PostgreSQL Flexible Server. Assigned to the Container App Managed Identity for database wake functionality.", - "type": "CustomRole", - "permissions": [ - { - "actions": [ - "Microsoft.DBforPostgreSQL/flexibleServers/read", - "Microsoft.DBforPostgreSQL/flexibleServers/start/action" - ], - "notActions": [], - "dataActions": [], - "notDataActions": [] - } - ], - "assignableScopes": [ - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" - ] - }, - "dependsOn": [ - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" - ] - }, - { - "condition": "[parameters('deployDbWakeRole')]", - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]", - "name": "[guid(resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName')), parameters('managedIdentityPrincipalId'), 'db-wake')]", - "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', guid('db-wake-role', subscription().subscriptionId, resourceGroup().id))]", - "principalId": "[parameters('managedIdentityPrincipalId')]", - "principalType": "ServicePrincipal" - }, - "dependsOn": [ - "[resourceId('Microsoft.Authorization/roleDefinitions', guid('db-wake-role', subscription().subscriptionId, resourceGroup().id))]", - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName'))]" - ] - } - ], - "outputs": { - "serverFqdn": { - "type": "string", - "value": "[reference(resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName')), '2023-12-01-preview').fullyQualifiedDomainName]" - }, - "connectionString": { - "type": "string", - "value": "[format('postgresql://{0}:{1}@{2}:5432/{3}?sslmode=require', parameters('adminLogin'), parameters('adminPassword'), reference(resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName')), '2023-12-01-preview').fullyQualifiedDomainName, variables('databaseName'))]" - }, - "databaseName": { - "type": "string", - "value": "[variables('databaseName')]" - }, - "serverHost": { - "type": "string", - "value": "[reference(resourceId('Microsoft.DBforPostgreSQL/flexibleServers', variables('serverName')), '2023-12-01-preview').fullyQualifiedDomainName]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]" - ] - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "storage", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "environmentName": { - "value": "[parameters('environmentName')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "managedIdentityPrincipalId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.principalId.value]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "11709574374180075184" - } - }, - "parameters": { - "environmentName": { - "type": "string", - "metadata": { - "description": "Environment name (staging or production)" - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region" - } - }, - "managedIdentityPrincipalId": { - "type": "string", - "metadata": { - "description": "Principal ID of managed identity for Storage Blob Data Contributor role" - } - } - }, - "variables": { - "uniqueSuffix": "[uniqueString(resourceGroup().id)]", - "storageAccountName": "[take(format('stacroyoga{0}{1}', parameters('environmentName'), variables('uniqueSuffix')), 24)]", - "storageBlobContributorRoleId": "ba92f5b4-2d11-453d-a403-e96b0029c9fe" - }, - "resources": [ - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2023-05-01", - "name": "[variables('storageAccountName')]", - "location": "[parameters('location')]", - "sku": { - "name": "Standard_LRS" - }, - "kind": "StorageV2", - "properties": { - "accessTier": "Hot", - "allowBlobPublicAccess": false, - "minimumTlsVersion": "TLS1_2", - "supportsHttpsTrafficOnly": true - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "type": "Microsoft.Storage/storageAccounts/blobServices", - "apiVersion": "2023-05-01", - "name": "[format('{0}/{1}', variables('storageAccountName'), 'default')]", - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" - ] - }, - { - "type": "Microsoft.Storage/storageAccounts/blobServices/containers", - "apiVersion": "2023-05-01", - "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', 'media')]", - "properties": { - "publicAccess": "None" - }, - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]" - ] - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), parameters('managedIdentityPrincipalId'), variables('storageBlobContributorRoleId'))]", - "properties": { - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('storageBlobContributorRoleId'))]", - "principalId": "[parameters('managedIdentityPrincipalId')]", - "principalType": "ServicePrincipal" - }, - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" - ] - } - ], - "outputs": { - "blobEndpoint": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-05-01').primaryEndpoints.blob]" - }, - "accountName": { - "type": "string", - "value": "[variables('storageAccountName')]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]" - ] - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "key-vault", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "environmentName": { - "value": "[parameters('environmentName')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "managedIdentityPrincipalId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.principalId.value]" - }, - "logAnalyticsWorkspaceId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" - }, - "secrets": { - "value": { - "databaseUrl": "[reference(resourceId('Microsoft.Resources/deployments', 'database'), '2025-04-01').outputs.connectionString.value]", - "nextAuthSecret": "[parameters('nextAuthSecret')]", - "nextAuthUrl": "[if(not(equals(parameters('customDomainHostname'), '')), format('https://{0}', parameters('customDomainHostname')), 'https://placeholder.azurecontainerapps.io')]", - "stripeSecretKey": "[parameters('stripeSecretKey')]", - "stripeWebhookSecret": "[parameters('stripeWebhookSecret')]", - "stripeClientId": "[parameters('stripeClientId')]", - "applicationInsightsConnectionString": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]", - "entraClientId": "[parameters('entraClientId')]", - "entraTenantId": "[parameters('entraTenantId')]" - } - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "1045468276900666757" - } - }, - "parameters": { - "environmentName": { - "type": "string", - "metadata": { - "description": "Environment name (staging or production)" - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region" - } - }, - "managedIdentityPrincipalId": { - "type": "string", - "metadata": { - "description": "Principal ID of managed identity for Key Vault Secrets User role" - } - }, - "secrets": { - "type": "secureObject", - "defaultValue": {}, - "metadata": { - "description": "Secrets to populate in Key Vault (key-value pairs)" - } - }, - "logAnalyticsWorkspaceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Log Analytics workspace ID for diagnostic audit logs" - } - } - }, - "variables": { - "uniqueSuffix": "[uniqueString(resourceGroup().id)]", - "vaultName": "[take(format('kv-acro-{0}-{1}', parameters('environmentName'), variables('uniqueSuffix')), 24)]", - "kvSecretsUserRoleId": "4633458b-17de-408a-b874-0445c86b69e6" - }, - "resources": [ - { - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2023-07-01", - "name": "[variables('vaultName')]", - "location": "[parameters('location')]", - "properties": { - "sku": { - "family": "A", - "name": "standard" - }, - "tenantId": "[subscription().tenantId]", - "enableRbacAuthorization": true, - "enableSoftDelete": true, - "softDeleteRetentionInDays": 90, - "enablePurgeProtection": true - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]", - "name": "[guid(resourceId('Microsoft.KeyVault/vaults', variables('vaultName')), parameters('managedIdentityPrincipalId'), variables('kvSecretsUserRoleId'))]", - "properties": { - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('kvSecretsUserRoleId'))]", - "principalId": "[parameters('managedIdentityPrincipalId')]", - "principalType": "ServicePrincipal" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[contains(parameters('secrets'), 'databaseUrl')]", - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('vaultName'), 'database-url')]", - "properties": { - "value": "[coalesce(tryGet(parameters('secrets'), 'databaseUrl'), '')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[contains(parameters('secrets'), 'nextAuthSecret')]", - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('vaultName'), 'nextauth-secret')]", - "properties": { - "value": "[coalesce(tryGet(parameters('secrets'), 'nextAuthSecret'), '')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[contains(parameters('secrets'), 'nextAuthUrl')]", - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('vaultName'), 'nextauth-url')]", - "properties": { - "value": "[coalesce(tryGet(parameters('secrets'), 'nextAuthUrl'), '')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[contains(parameters('secrets'), 'stripeSecretKey')]", - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('vaultName'), 'stripe-secret-key')]", - "properties": { - "value": "[coalesce(tryGet(parameters('secrets'), 'stripeSecretKey'), '')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[contains(parameters('secrets'), 'stripeWebhookSecret')]", - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('vaultName'), 'stripe-webhook-secret')]", - "properties": { - "value": "[coalesce(tryGet(parameters('secrets'), 'stripeWebhookSecret'), '')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[contains(parameters('secrets'), 'stripeClientId')]", - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('vaultName'), 'stripe-client-id')]", - "properties": { - "value": "[coalesce(tryGet(parameters('secrets'), 'stripeClientId'), '')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[contains(parameters('secrets'), 'applicationInsightsConnectionString')]", - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('vaultName'), 'applicationinsights-connection-string')]", - "properties": { - "value": "[coalesce(tryGet(parameters('secrets'), 'applicationInsightsConnectionString'), '')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[contains(parameters('secrets'), 'entraClientId')]", - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('vaultName'), 'entra-client-id')]", - "properties": { - "value": "[parameters('secrets').entraClientId]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[contains(parameters('secrets'), 'entraTenantId')]", - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('vaultName'), 'entra-tenant-id')]", - "properties": { - "value": "[parameters('secrets').entraTenantId]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - }, - { - "condition": "[not(equals(parameters('logAnalyticsWorkspaceId'), ''))]", - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]", - "name": "kv-diagnostics", - "properties": { - "workspaceId": "[parameters('logAnalyticsWorkspaceId')]", - "logs": [ - { - "categoryGroup": "audit", - "enabled": true - } - ] - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]" - ] - } - ], - "outputs": { - "vaultName": { - "type": "string", - "value": "[variables('vaultName')]" - }, - "vaultUri": { - "type": "string", - "value": "[reference(resourceId('Microsoft.KeyVault/vaults', variables('vaultName')), '2023-07-01').vaultUri]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'database')]", - "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]", - "[resourceId('Microsoft.Resources/deployments', 'monitoring')]" - ] - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "container-apps", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "environmentName": { - "value": "[parameters('environmentName')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "containerRegistryLoginServer": "[if(parameters('deployContainerRegistry'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'container-registry'), '2025-04-01').outputs.loginServer.value), createObject('value', parameters('sharedContainerRegistryLoginServer')))]", - "imageTag": { - "value": "[parameters('imageTag')]" - }, - "managedIdentityId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.resourceId.value]" - }, - "managedIdentityClientId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.clientId.value]" - }, - "managedIdentityName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'managed-identity'), '2025-04-01').outputs.name.value]" - }, - "keyVaultName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'key-vault'), '2025-04-01').outputs.vaultName.value]" - }, - "appInsightsConnectionString": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]" - }, - "logAnalyticsWorkspaceId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" - }, - "minReplicas": { - "value": "[parameters('minReplicas')]" - }, - "maxReplicas": { - "value": "[parameters('maxReplicas')]" - }, - "cpuCores": { - "value": "[parameters('cpuCores')]" - }, - "memorySize": { - "value": "[parameters('memorySize')]" - }, - "storageBlobEndpoint": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2025-04-01').outputs.blobEndpoint.value]" - }, - "pgHost": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'database'), '2025-04-01').outputs.serverHost.value]" - }, - "pgDatabase": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'database'), '2025-04-01').outputs.databaseName.value]" - }, - "entraTenantDomain": { - "value": "[parameters('entraTenantDomain')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "5943353407180863231" - } - }, - "parameters": { - "environmentName": { - "type": "string", - "metadata": { - "description": "Environment name (staging or production)" - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region" - } - }, - "containerRegistryLoginServer": { - "type": "string", - "metadata": { - "description": "ACR login server URL" - } - }, - "imageTag": { - "type": "string", - "metadata": { - "description": "Container image tag" - } - }, - "managedIdentityId": { - "type": "string", - "metadata": { - "description": "User-assigned managed identity resource ID" - } - }, - "managedIdentityClientId": { - "type": "string", - "metadata": { - "description": "User-assigned managed identity client ID" - } - }, - "managedIdentityName": { - "type": "string", - "metadata": { - "description": "User-assigned managed identity name (for PostgreSQL Entra auth user)" - } - }, - "keyVaultName": { - "type": "string", - "metadata": { - "description": "Key Vault name for secret references" - } - }, - "appInsightsConnectionString": { - "type": "string", - "metadata": { - "description": "Application Insights connection string" - } - }, - "logAnalyticsWorkspaceId": { - "type": "string", - "metadata": { - "description": "Log Analytics workspace resource ID" - } - }, - "storageBlobEndpoint": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Azure Storage blob endpoint URL for Managed Identity access" - } - }, - "pgHost": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "PostgreSQL server hostname for Managed Identity token auth" - } - }, - "pgDatabase": { - "type": "string", - "defaultValue": "acroyoga", - "metadata": { - "description": "PostgreSQL database name" - } - }, - "minReplicas": { - "type": "int", - "defaultValue": 0, - "metadata": { - "description": "Minimum replicas" - } - }, - "maxReplicas": { - "type": "int", - "defaultValue": 10, - "metadata": { - "description": "Maximum replicas" - } - }, - "cpuCores": { - "type": "string", - "defaultValue": "0.5", - "metadata": { - "description": "CPU cores per instance" - } - }, - "memorySize": { - "type": "string", - "defaultValue": "1Gi", - "metadata": { - "description": "Memory per instance" - } - }, - "entraTenantDomain": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Entra External ID CIAM tenant subdomain (e.g. \"acroyogacommunity\")" - } - } - }, - "variables": { - "environmentResourceName": "[format('cae-acroyoga-{0}', parameters('environmentName'))]", - "appName": "[format('ca-acroyoga-web-{0}', parameters('environmentName'))]", - "imageName": "[format('{0}/acroyoga-web:{1}', parameters('containerRegistryLoginServer'), parameters('imageTag'))]" - }, - "resources": [ - { - "type": "Microsoft.App/managedEnvironments", - "apiVersion": "2024-03-01", - "name": "[variables('environmentResourceName')]", - "location": "[parameters('location')]", - "properties": { - "appLogsConfiguration": { - "destination": "log-analytics", - "logAnalyticsConfiguration": { - "customerId": "[reference(parameters('logAnalyticsWorkspaceId'), '2023-09-01').customerId]", - "sharedKey": "[listKeys(parameters('logAnalyticsWorkspaceId'), '2023-09-01').primarySharedKey]" - } - }, - "workloadProfiles": [ - { - "name": "Consumption", - "workloadProfileType": "Consumption" - } - ] - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "type": "Microsoft.App/containerApps", - "apiVersion": "2024-03-01", - "name": "[variables('appName')]", - "location": "[parameters('location')]", - "identity": { - "type": "UserAssigned", - "userAssignedIdentities": { - "[format('{0}', parameters('managedIdentityId'))]": {} - } - }, - "properties": { - "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', variables('environmentResourceName'))]", - "configuration": { - "activeRevisionsMode": "Multiple", - "ingress": { - "external": true, - "targetPort": 3000, - "transport": "http" - }, - "registries": [ - { - "server": "[parameters('containerRegistryLoginServer')]", - "identity": "[parameters('managedIdentityId')]" - } - ], - "secrets": [ - { - "name": "database-url", - "keyVaultUrl": "[format('https://{0}{1}/secrets/database-url', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", - "identity": "[parameters('managedIdentityId')]" - }, - { - "name": "nextauth-secret", - "keyVaultUrl": "[format('https://{0}{1}/secrets/nextauth-secret', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", - "identity": "[parameters('managedIdentityId')]" - }, - { - "name": "nextauth-url", - "keyVaultUrl": "[format('https://{0}{1}/secrets/nextauth-url', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", - "identity": "[parameters('managedIdentityId')]" - }, - { - "name": "stripe-secret-key", - "keyVaultUrl": "[format('https://{0}{1}/secrets/stripe-secret-key', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", - "identity": "[parameters('managedIdentityId')]" - }, - { - "name": "stripe-webhook-secret", - "keyVaultUrl": "[format('https://{0}{1}/secrets/stripe-webhook-secret', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", - "identity": "[parameters('managedIdentityId')]" - }, - { - "name": "stripe-client-id", - "keyVaultUrl": "[format('https://{0}{1}/secrets/stripe-client-id', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", - "identity": "[parameters('managedIdentityId')]" - }, - { - "name": "applicationinsights-connection-string", - "keyVaultUrl": "[format('https://{0}{1}/secrets/applicationinsights-connection-string', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", - "identity": "[parameters('managedIdentityId')]" - }, - { - "name": "entra-client-id", - "keyVaultUrl": "[format('https://{0}{1}/secrets/entra-client-id', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", - "identity": "[parameters('managedIdentityId')]" - }, - { - "name": "entra-tenant-id", - "keyVaultUrl": "[format('https://{0}{1}/secrets/entra-tenant-id', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", - "identity": "[parameters('managedIdentityId')]" - } - ] - }, - "template": { - "containers": [ - { - "name": "web", - "image": "[variables('imageName')]", - "resources": { - "cpu": "[json(parameters('cpuCores'))]", - "memory": "[parameters('memorySize')]" - }, - "env": [ - { - "name": "DATABASE_URL", - "secretRef": "database-url" - }, - { - "name": "NEXTAUTH_SECRET", - "secretRef": "nextauth-secret" - }, - { - "name": "NEXTAUTH_URL", - "secretRef": "nextauth-url" - }, - { - "name": "STRIPE_SECRET_KEY", - "secretRef": "stripe-secret-key" - }, - { - "name": "STRIPE_WEBHOOK_SECRET", - "secretRef": "stripe-webhook-secret" - }, - { - "name": "STRIPE_CLIENT_ID", - "secretRef": "stripe-client-id" - }, - { - "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", - "secretRef": "applicationinsights-connection-string" - }, - { - "name": "ENTRA_CLIENT_ID", - "secretRef": "entra-client-id" - }, - { - "name": "ENTRA_TENANT_ID", - "secretRef": "entra-tenant-id" - }, - { - "name": "NODE_ENV", - "value": "production" - }, - { - "name": "HOSTNAME", - "value": "0.0.0.0" - }, - { - "name": "PORT", - "value": "3000" - }, - { - "name": "AZURE_CLIENT_ID", - "value": "[parameters('managedIdentityClientId')]" - }, - { - "name": "AZURE_STORAGE_ACCOUNT_URL", - "value": "[parameters('storageBlobEndpoint')]" - }, - { - "name": "AZURE_SUBSCRIPTION_ID", - "value": "[subscription().subscriptionId]" - }, - { - "name": "AZURE_RESOURCE_GROUP", - "value": "[resourceGroup().name]" - }, - { - "name": "ENVIRONMENT_NAME", - "value": "[parameters('environmentName')]" - }, - { - "name": "PGHOST", - "value": "[parameters('pgHost')]" - }, - { - "name": "PGDATABASE", - "value": "[parameters('pgDatabase')]" - }, - { - "name": "PGUSER", - "value": "[parameters('managedIdentityName')]" - }, - { - "name": "ENTRA_TENANT_DOMAIN", - "value": "[parameters('entraTenantDomain')]" - } - ], - "probes": [ - { - "type": "Liveness", - "httpGet": { - "path": "/api/health", - "port": 3000 - }, - "periodSeconds": 10, - "timeoutSeconds": 5, - "failureThreshold": 3 - }, - { - "type": "Readiness", - "httpGet": { - "path": "/api/ready", - "port": 3000 - }, - "periodSeconds": 5, - "timeoutSeconds": 10, - "failureThreshold": 10 - }, - { - "type": "Startup", - "httpGet": { - "path": "/api/health", - "port": 3000 - }, - "periodSeconds": 5, - "timeoutSeconds": 5, - "failureThreshold": 30 - } - ] - } - ], - "scale": { - "minReplicas": "[parameters('minReplicas')]", - "maxReplicas": "[parameters('maxReplicas')]", - "rules": [ - { - "name": "http-scale", - "http": { - "metadata": { - "concurrentRequests": "20" - } - } - } - ] - } - } - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep", - "azd-service-name": "web" - }, - "dependsOn": [ - "[resourceId('Microsoft.App/managedEnvironments', variables('environmentResourceName'))]" - ] - } - ], - "outputs": { - "fqdn": { - "type": "string", - "value": "[reference(resourceId('Microsoft.App/containerApps', variables('appName')), '2024-03-01').configuration.ingress.fqdn]" - }, - "environmentId": { - "type": "string", - "value": "[resourceId('Microsoft.App/managedEnvironments', variables('environmentResourceName'))]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'database')]", - "[resourceId('Microsoft.Resources/deployments', 'managed-identity')]", - "[resourceId('Microsoft.Resources/deployments', 'key-vault')]", - "[resourceId('Microsoft.Resources/deployments', 'monitoring')]", - "[resourceId('Microsoft.Resources/deployments', 'container-registry')]", - "[resourceId('Microsoft.Resources/deployments', 'storage')]" - ] - }, - { - "condition": "[parameters('deployFrontDoor')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "front-door", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "originHostname": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'container-apps'), '2025-04-01').outputs.fqdn.value]" - }, - "customDomainHostname": { - "value": "[parameters('customDomainHostname')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "12581537020446223132" - } - }, - "parameters": { - "originHostname": { - "type": "string", - "metadata": { - "description": "Container App FQDN (backend origin)" - } - }, - "customDomainHostname": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Custom domain hostname (optional)" - } - }, - "wafPolicyName": { - "type": "string", - "defaultValue": "wafacroyoga", - "metadata": { - "description": "WAF policy name (alphanumeric only)" - } - } - }, - "variables": { - "uniqueSuffix": "[uniqueString(resourceGroup().id)]", - "frontDoorName": "afd-acroyoga", - "originGroupName": "default", - "originName": "web", - "routeName": "default-route", - "endpointName": "[format('acro-{0}', variables('uniqueSuffix'))]" - }, - "resources": [ - { - "type": "Microsoft.Network/FrontDoorWebApplicationFirewallPolicies", - "apiVersion": "2024-02-01", - "name": "[parameters('wafPolicyName')]", - "location": "global", - "sku": { - "name": "Standard_AzureFrontDoor" - }, - "properties": { - "policySettings": { - "enabledState": "Enabled", - "mode": "Prevention" - }, - "managedRules": { - "managedRuleSets": [] - } - }, - "tags": { - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "type": "Microsoft.Cdn/profiles", - "apiVersion": "2024-02-01", - "name": "[variables('frontDoorName')]", - "location": "global", - "sku": { - "name": "Standard_AzureFrontDoor" - }, - "tags": { - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "type": "Microsoft.Cdn/profiles/afdEndpoints", - "apiVersion": "2024-02-01", - "name": "[format('{0}/{1}', variables('frontDoorName'), variables('endpointName'))]", - "location": "global", - "properties": { - "enabledState": "Enabled" - }, - "dependsOn": [ - "[resourceId('Microsoft.Cdn/profiles', variables('frontDoorName'))]" - ] - }, - { - "type": "Microsoft.Cdn/profiles/originGroups", - "apiVersion": "2024-02-01", - "name": "[format('{0}/{1}', variables('frontDoorName'), variables('originGroupName'))]", - "properties": { - "loadBalancingSettings": { - "sampleSize": 4, - "successfulSamplesRequired": 3 - }, - "healthProbeSettings": { - "probePath": "/api/health", - "probeRequestType": "HEAD", - "probeProtocol": "Https", - "probeIntervalInSeconds": 30 - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Cdn/profiles', variables('frontDoorName'))]" - ] - }, - { - "type": "Microsoft.Cdn/profiles/originGroups/origins", - "apiVersion": "2024-02-01", - "name": "[format('{0}/{1}/{2}', variables('frontDoorName'), variables('originGroupName'), variables('originName'))]", - "properties": { - "hostName": "[parameters('originHostname')]", - "httpPort": 80, - "httpsPort": 443, - "originHostHeader": "[parameters('originHostname')]", - "priority": 1, - "weight": 1000 - }, - "dependsOn": [ - "[resourceId('Microsoft.Cdn/profiles/originGroups', variables('frontDoorName'), variables('originGroupName'))]" - ] - }, - { - "type": "Microsoft.Cdn/profiles/afdEndpoints/routes", - "apiVersion": "2024-02-01", - "name": "[format('{0}/{1}/{2}', variables('frontDoorName'), variables('endpointName'), variables('routeName'))]", - "properties": { - "originGroup": { - "id": "[resourceId('Microsoft.Cdn/profiles/originGroups', variables('frontDoorName'), variables('originGroupName'))]" - }, - "supportedProtocols": [ - "Http", - "Https" - ], - "patternsToMatch": [ - "/*" - ], - "forwardingProtocol": "HttpsOnly", - "linkToDefaultDomain": "Enabled", - "httpsRedirect": "Enabled", - "cacheConfiguration": { - "queryStringCachingBehavior": "UseQueryString", - "compressionSettings": { - "isCompressionEnabled": true, - "contentTypesToCompress": [ - "text/html", - "text/css", - "application/javascript", - "application/json", - "image/svg+xml" - ] - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Cdn/profiles/afdEndpoints', variables('frontDoorName'), variables('endpointName'))]", - "[resourceId('Microsoft.Cdn/profiles/originGroups/origins', variables('frontDoorName'), variables('originGroupName'), variables('originName'))]", - "[resourceId('Microsoft.Cdn/profiles/originGroups', variables('frontDoorName'), variables('originGroupName'))]" - ] - }, - { - "type": "Microsoft.Cdn/profiles/securityPolicies", - "apiVersion": "2024-02-01", - "name": "[format('{0}/{1}', variables('frontDoorName'), 'waf-policy')]", - "properties": { - "parameters": { - "type": "WebApplicationFirewall", - "wafPolicy": { - "id": "[resourceId('Microsoft.Network/FrontDoorWebApplicationFirewallPolicies', parameters('wafPolicyName'))]" - }, - "associations": [ - { - "domains": [ - { - "id": "[resourceId('Microsoft.Cdn/profiles/afdEndpoints', variables('frontDoorName'), variables('endpointName'))]" - } - ], - "patternsToMatch": [ - "/*" - ] - } - ] - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Cdn/profiles/afdEndpoints', variables('frontDoorName'), variables('endpointName'))]", - "[resourceId('Microsoft.Cdn/profiles', variables('frontDoorName'))]", - "[resourceId('Microsoft.Network/FrontDoorWebApplicationFirewallPolicies', parameters('wafPolicyName'))]" - ] - } - ], - "outputs": { - "endpoint": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Cdn/profiles/afdEndpoints', variables('frontDoorName'), variables('endpointName')), '2024-02-01').hostName]" - }, - "frontDoorId": { - "type": "string", - "value": "[resourceId('Microsoft.Cdn/profiles', variables('frontDoorName'))]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'container-apps')]" - ] - }, - { - "condition": "[parameters('deployMonitoringAlerts')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "monitoring-alerts", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "environmentName": { - "value": "[parameters('environmentName')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "alertEmailAddress": { - "value": "[parameters('alertEmailAddress')]" - }, - "enableAlertRules": { - "value": true - }, - "appInsightsResourceId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsResourceId.value]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "8244233152229045120" - } - }, - "parameters": { - "environmentName": { - "type": "string", - "metadata": { - "description": "Environment name (staging or production)" - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region" - } - }, - "alertEmailAddress": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Email address for alert action group notifications" - } - }, - "enableAlertRules": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Whether to create metric alert rules" - } - }, - "appInsightsResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Application Insights resource ID for alert scoping" - } - } - }, - "variables": { - "logAnalyticsName": "[format('log-acroyoga-{0}', parameters('environmentName'))]", - "appInsightsName": "[format('appi-acroyoga-{0}', parameters('environmentName'))]", - "actionGroupName": "[format('ag-acroyoga-{0}', parameters('environmentName'))]" - }, - "resources": [ - { - "type": "Microsoft.OperationalInsights/workspaces", - "apiVersion": "2023-09-01", - "name": "[variables('logAnalyticsName')]", - "location": "[parameters('location')]", - "properties": { - "sku": { - "name": "PerGB2018" - }, - "retentionInDays": 30 - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "type": "Microsoft.Insights/components", - "apiVersion": "2020-02-02", - "name": "[variables('appInsightsName')]", - "location": "[parameters('location')]", - "kind": "web", - "properties": { - "Application_Type": "web", - "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" - ] - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('alertEmailAddress'), '')))]", - "type": "Microsoft.Insights/actionGroups", - "apiVersion": "2023-01-01", - "name": "[variables('actionGroupName')]", - "location": "global", - "properties": { - "groupShortName": "acroyoga", - "enabled": true, - "emailReceivers": [ - { - "name": "ops-email", - "emailAddress": "[parameters('alertEmailAddress')]" - } - ] - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - } - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", - "type": "Microsoft.Insights/metricAlerts", - "apiVersion": "2018-03-01", - "name": "[format('alert-http5xx-{0}', parameters('environmentName'))]", - "location": "global", - "properties": { - "severity": 2, - "enabled": true, - "scopes": [ - "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" - ], - "evaluationFrequency": "PT1M", - "windowSize": "PT5M", - "criteria": { - "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", - "allOf": [ - { - "name": "http5xx", - "metricName": "requests/failed", - "metricNamespace": "microsoft.insights/components", - "operator": "GreaterThan", - "threshold": 5, - "timeAggregation": "Count", - "criterionType": "StaticThresholdCriterion" - } - ] - }, - "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", - "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - ] - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", - "type": "Microsoft.Insights/metricAlerts", - "apiVersion": "2018-03-01", - "name": "[format('alert-response-time-{0}', parameters('environmentName'))]", - "location": "global", - "properties": { - "severity": 3, - "enabled": true, - "scopes": [ - "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" - ], - "evaluationFrequency": "PT1M", - "windowSize": "PT5M", - "criteria": { - "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", - "allOf": [ - { - "name": "responseTime", - "metricName": "requests/duration", - "metricNamespace": "microsoft.insights/components", - "operator": "GreaterThan", - "threshold": 2000, - "timeAggregation": "Average", - "criterionType": "StaticThresholdCriterion" - } - ] - }, - "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", - "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - ] - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", - "type": "Microsoft.Insights/metricAlerts", - "apiVersion": "2018-03-01", - "name": "[format('alert-container-restart-{0}', parameters('environmentName'))]", - "location": "global", - "properties": { - "severity": 2, - "enabled": true, - "scopes": [ - "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" - ], - "evaluationFrequency": "PT1M", - "windowSize": "PT10M", - "criteria": { - "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", - "allOf": [ - { - "name": "containerRestarts", - "metricName": "exceptions/count", - "metricNamespace": "microsoft.insights/components", - "operator": "GreaterThan", - "threshold": 3, - "timeAggregation": "Count", - "criterionType": "StaticThresholdCriterion" - } - ] - }, - "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", - "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - ] - }, - { - "condition": "[and(parameters('enableAlertRules'), not(equals(parameters('appInsightsResourceId'), '')))]", - "type": "Microsoft.Insights/metricAlerts", - "apiVersion": "2018-03-01", - "name": "[format('alert-db-connection-{0}', parameters('environmentName'))]", - "location": "global", - "properties": { - "severity": 1, - "enabled": true, - "scopes": [ - "[if(not(equals(parameters('appInsightsResourceId'), '')), parameters('appInsightsResourceId'), resourceId('Microsoft.Insights/components', variables('appInsightsName')))]" - ], - "evaluationFrequency": "PT1M", - "windowSize": "PT5M", - "criteria": { - "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria", - "allOf": [ - { - "name": "dbConnectionFailures", - "metricName": "dependencies/failed", - "metricNamespace": "microsoft.insights/components", - "operator": "GreaterThan", - "threshold": 0, - "timeAggregation": "Count", - "criterionType": "StaticThresholdCriterion" - } - ] - }, - "actions": "[if(not(equals(parameters('alertEmailAddress'), '')), createArray(createObject('actionGroupId', resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName')))), createArray())]" - }, - "tags": { - "environment": "[parameters('environmentName')]", - "project": "acroyoga-community", - "managedBy": "bicep" - }, - "dependsOn": [ - "[resourceId('Microsoft.Insights/actionGroups', variables('actionGroupName'))]", - "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - ] - } - ], - "outputs": { - "logAnalyticsWorkspaceId": { - "type": "string", - "value": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" - }, - "appInsightsResourceId": { - "type": "string", - "value": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" - }, - "appInsightsConnectionString": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').ConnectionString]" - }, - "appInsightsInstrumentationKey": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').InstrumentationKey]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'monitoring')]" - ] - } - ], - "outputs": { - "AZURE_CONTAINER_REGISTRY_ENDPOINT": { - "type": "string", - "value": "[if(parameters('deployContainerRegistry'), reference(resourceId('Microsoft.Resources/deployments', 'container-registry'), '2025-04-01').outputs.loginServer.value, parameters('sharedContainerRegistryLoginServer'))]" - }, - "containerAppFqdn": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'container-apps'), '2025-04-01').outputs.fqdn.value]" - }, - "frontDoorEndpoint": { - "type": "string", - "value": "[if(parameters('deployFrontDoor'), reference(resourceId('Microsoft.Resources/deployments', 'front-door'), '2025-04-01').outputs.endpoint.value, '')]" - }, - "containerRegistryLoginServer": { - "type": "string", - "value": "[if(parameters('deployContainerRegistry'), reference(resourceId('Microsoft.Resources/deployments', 'container-registry'), '2025-04-01').outputs.loginServer.value, parameters('sharedContainerRegistryLoginServer'))]" - } - } -} \ No newline at end of file