diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8af6c68698..e02ed01418 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: Ask a question - url: https://github.com/platform/platform/discussions/new/choose + url: https://github.com/platformplatform/PlatformPlatform/discussions/new/choose about: Start a new discussion on our GitHub Community pages - - name: Browse exisitng discussions - url: https://github.com/platform/platform/discussions + - name: Browse existing discussions + url: https://github.com/platformplatform/PlatformPlatform/discussions about: Visit our GitHub Community pages to view existing discussions (or click Discussions in the menu) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 5832e3a793..06ce88d5a0 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -6,7 +6,7 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this bug report! + Thanks for taking the time to fill out this feature request! - type: input id: contact attributes: @@ -19,7 +19,7 @@ body: id: detailed-description attributes: label: Detailed description? - description: Also tell us more about the bug, and what you were expecting. + description: Tell us more about the feature request and what you would like to see. value: | **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. diff --git a/.github/workflows/_deploy-container.yml b/.github/workflows/_deploy-container.yml index d71099c01b..5f2c254795 100644 --- a/.github/workflows/_deploy-container.yml +++ b/.github/workflows/_deploy-container.yml @@ -100,15 +100,16 @@ jobs: - name: Deploy Container run: | + CLUSTER_RESOURCE_GROUP_NAME="${{ env.UNIQUE_PREFIX }}-${{ env.ENVIRONMENT }}-${{ env.CLUSTER_LOCATION_ACRONYM }}" SUFFIX=$(echo "${{ inputs.version }}" | sed 's/\./-/g') - az containerapp update --name ${{ inputs.image_name }} --resource-group "${{ env.UNIQUE_PREFIX }}-${{ env.ENVIRONMENT }}-${{ env.CLUSTER_LOCATION_ACRONYM }}" --image "${{ env.UNIQUE_PREFIX }}${{ env.ENVIRONMENT }}.azurecr.io/${{ inputs.image_name }}:${{ inputs.version }}" --revision-suffix $SUFFIX + az containerapp update --name ${{ inputs.image_name }} --resource-group "$CLUSTER_RESOURCE_GROUP_NAME" --image "${{ env.UNIQUE_PREFIX }}${{ env.ENVIRONMENT }}.azurecr.io/${{ inputs.image_name }}:${{ inputs.version }}" --revision-suffix $SUFFIX echo "Waiting for the new revision to be active..." for i in {1..10}; do sleep 15 - RUNNING_STATUS=$(az containerapp revision list --name ${{ inputs.image_name }} --resource-group "${{ env.UNIQUE_PREFIX }}-${{ env.ENVIRONMENT }}-${{ env.CLUSTER_LOCATION_ACRONYM }}" --query "[?contains(name, '$SUFFIX')].properties.runningState" --output tsv) - HEALTH_STATUS=$(az containerapp revision list --name ${{ inputs.image_name }} --resource-group "${{ env.UNIQUE_PREFIX }}-${{ env.ENVIRONMENT }}-${{ env.CLUSTER_LOCATION_ACRONYM }}" --query "[?contains(name, '$SUFFIX')].properties.healthState" --output tsv) + RUNNING_STATUS=$(az containerapp revision list --name ${{ inputs.image_name }} --resource-group "$CLUSTER_RESOURCE_GROUP_NAME" --query "[?contains(name, '$SUFFIX')].properties.runningState" --output tsv) + HEALTH_STATUS=$(az containerapp revision list --name ${{ inputs.image_name }} --resource-group "$CLUSTER_RESOURCE_GROUP_NAME" --query "[?contains(name, '$SUFFIX')].properties.healthState" --output tsv) if [[ "$HEALTH_STATUS" == "Healthy" ]]; then echo "New revision is healthy. Running state: $RUNNING_STATUS" exit 0 diff --git a/.github/workflows/_deploy-infrastructure.yml b/.github/workflows/_deploy-infrastructure.yml index ea56acaf75..52cd14ce6d 100644 --- a/.github/workflows/_deploy-infrastructure.yml +++ b/.github/workflows/_deploy-infrastructure.yml @@ -80,6 +80,33 @@ jobs: id: deploy_cluster run: bash ./cloud-infrastructure/cluster/deploy-cluster.sh ${{ inputs.unique_prefix }} ${{ inputs.azure_environment }} ${{ inputs.cluster_location }} ${{ inputs.cluster_location_acronym }} ${{ inputs.sql_admin_object_id }} ${{ inputs.domain_name }} --plan + - name: Show DNS Configuration + if: ${{ inputs.domain_name != '' && inputs.domain_name != '-' }} + run: | + CLUSTER_RESOURCE_GROUP_NAME="${{ inputs.unique_prefix }}-${{ inputs.azure_environment }}-${{ inputs.cluster_location_acronym }}" + + # Try to get the Container Apps Environment details + env_details=$(az containerapp env show --name $CLUSTER_RESOURCE_GROUP_NAME --resource-group $CLUSTER_RESOURCE_GROUP_NAME 2>&1 || echo "") + + if [[ "$env_details" != "" ]] && [[ "$env_details" != *"ResourceNotFound"* ]] && [[ "$env_details" != *"ResourceGroupNotFound"* ]]; then + custom_domain_verification_id=$(echo "$env_details" | jq -r '.properties.customDomainConfiguration.customDomainVerificationId') + default_domain=$(echo "$env_details" | jq -r '.properties.defaultDomain') + + # Check if app-gateway already has the custom domain configured + app_gateway_details=$(az containerapp show --name app-gateway --resource-group $CLUSTER_RESOURCE_GROUP_NAME 2>&1 || echo "") + custom_domains=$(echo "$app_gateway_details" | jq -r '.properties.configuration.ingress.customDomains // []') + + if [[ "$custom_domains" != "[]" ]] && [[ "$custom_domains" != "null" ]]; then + echo "$(date +"%Y-%m-%dT%H:%M:%S") Custom domain '${{ inputs.domain_name }}' is already configured correctly." + else + echo "$(date +"%Y-%m-%dT%H:%M:%S") Please add the following DNS entries and then retry:" + echo "- A TXT record with the name 'asuid.${{ inputs.domain_name }}' and the value '$custom_domain_verification_id'." + echo "- A CNAME record with the Host name '${{ inputs.domain_name }}' that points to address 'app-gateway.$default_domain'." + fi + else + echo "$(date +"%Y-%m-%dT%H:%M:%S") DNS configuration instructions will be shown after the Container Apps Environment is created." + fi + deploy: name: Deploy if: ${{ needs.plan.outputs.should_deploy == 'true' }} diff --git a/.github/workflows/_migrate-database.yml b/.github/workflows/_migrate-database.yml index faaba53992..af18c0a5f0 100644 --- a/.github/workflows/_migrate-database.yml +++ b/.github/workflows/_migrate-database.yml @@ -51,7 +51,7 @@ jobs: env: UNIQUE_PREFIX: ${{ vars.UNIQUE_PREFIX }} TENANT_ID: ${{ vars.TENANT_ID }} - RESOURCE_GROUP_NAME: ${{ vars.UNIQUE_PREFIX }}-${{ inputs.azure_environment }}-${{ inputs.cluster_location_acronym }} + CLUSTER_RESOURCE_GROUP_NAME: ${{ vars.UNIQUE_PREFIX }}-${{ inputs.azure_environment }}-${{ inputs.cluster_location_acronym }} SQL_SERVER_NAME: ${{ vars.UNIQUE_PREFIX }}-${{ inputs.azure_environment }}-${{ inputs.cluster_location_acronym }} SQL_SERVER_FQDN: ${{ vars.UNIQUE_PREFIX }}-${{ inputs.azure_environment }}-${{ inputs.cluster_location_acronym }}.database.windows.net @@ -82,7 +82,7 @@ jobs: - name: Open Firewall working-directory: cloud-infrastructure/cluster env: - RESOURCE_GROUP_NAME: ${{ env.RESOURCE_GROUP_NAME }} + CLUSTER_RESOURCE_GROUP_NAME: ${{ env.CLUSTER_RESOURCE_GROUP_NAME }} SQL_SERVER_NAME: ${{ env.SQL_SERVER_NAME }} SQL_DATABASE_NAME: ${{ inputs.database_name }} run: bash ./firewall.sh open @@ -142,7 +142,7 @@ jobs: if: always() working-directory: cloud-infrastructure/cluster env: - RESOURCE_GROUP_NAME: ${{ env.RESOURCE_GROUP_NAME }} + CLUSTER_RESOURCE_GROUP_NAME: ${{ env.CLUSTER_RESOURCE_GROUP_NAME }} SQL_SERVER_NAME: ${{ env.SQL_SERVER_NAME }} SQL_DATABASE_NAME: ${{ inputs.database_name }} run: bash ./firewall.sh close @@ -190,7 +190,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - // Delete previous migration info comments + // Delete previous migration info comments for this specific database const comments = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, @@ -198,7 +198,7 @@ jobs: per_page: 100 }); - const MIGRATION_HEADER = '## Approve Database Migration'; + const MIGRATION_HEADER = '## Approve Database Migration `${{ inputs.database_name }}` database on `${{ inputs.azure_environment }}`'; for (const comment of comments.data) { if (comment.body && comment.body.startsWith(MIGRATION_HEADER)) { @@ -238,7 +238,7 @@ jobs: env: UNIQUE_PREFIX: ${{ vars.UNIQUE_PREFIX }} TENANT_ID: ${{ vars.TENANT_ID }} - RESOURCE_GROUP_NAME: ${{ vars.UNIQUE_PREFIX }}-${{ inputs.azure_environment }}-${{ inputs.cluster_location_acronym }} + CLUSTER_RESOURCE_GROUP_NAME: ${{ vars.UNIQUE_PREFIX }}-${{ inputs.azure_environment }}-${{ inputs.cluster_location_acronym }} SQL_SERVER_NAME: ${{ vars.UNIQUE_PREFIX }}-${{ inputs.azure_environment }}-${{ inputs.cluster_location_acronym }} SQL_SERVER_FQDN: ${{ vars.UNIQUE_PREFIX }}-${{ inputs.azure_environment }}-${{ inputs.cluster_location_acronym }}.database.windows.net @@ -269,7 +269,7 @@ jobs: - name: Open Firewall working-directory: cloud-infrastructure/cluster env: - RESOURCE_GROUP_NAME: ${{ env.RESOURCE_GROUP_NAME }} + CLUSTER_RESOURCE_GROUP_NAME: ${{ env.CLUSTER_RESOURCE_GROUP_NAME }} SQL_SERVER_NAME: ${{ env.SQL_SERVER_NAME }} SQL_DATABASE_NAME: ${{ inputs.database_name }} run: bash ./firewall.sh open @@ -289,7 +289,7 @@ jobs: if: always() working-directory: cloud-infrastructure/cluster env: - RESOURCE_GROUP_NAME: ${{ env.RESOURCE_GROUP_NAME }} + CLUSTER_RESOURCE_GROUP_NAME: ${{ env.CLUSTER_RESOURCE_GROUP_NAME }} SQL_SERVER_NAME: ${{ env.SQL_SERVER_NAME }} SQL_DATABASE_NAME: ${{ inputs.database_name }} run: bash ./firewall.sh close diff --git a/.gitignore b/.gitignore index f281134a18..102330c931 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# Bicep generated parameter files +cloud-infrastructure/cluster/main-cluster.parameters.json + # User-specific files *.rsuser *.suo diff --git a/cloud-infrastructure/README.md b/cloud-infrastructure/README.md index 21196ae70a..a70ad6d4e6 100644 --- a/cloud-infrastructure/README.md +++ b/cloud-infrastructure/README.md @@ -58,7 +58,8 @@ Examples of cluster-specific resources: - Communication Service: `ppdemo-stage-weu`, `ppdemo-prod-eus2` - Storage Accounts: `ppdemostageweuacctmgmt`, `ppdemoprodweudiagnostic` -Examples of environment-specific resources: +Examples of global resources (shared across all clusters in an environment): +- Resource Group: `ppdemo-stage-global`, `ppdemo-prod-global` - Application Insights: `ppdemo-stage`, `ppdemo-prod` - Log Analytics workspace: `ppdemo-stage`, `ppdemo-prod` - Container Registry: `ppdemostage`, `ppdemoprod` diff --git a/cloud-infrastructure/cluster/deploy-cluster.sh b/cloud-infrastructure/cluster/deploy-cluster.sh index 553ea25267..c96495d1c6 100755 --- a/cloud-infrastructure/cluster/deploy-cluster.sh +++ b/cloud-infrastructure/cluster/deploy-cluster.sh @@ -19,45 +19,53 @@ get_active_version() fi } -function is_domain_configured() { - # Get details about the container apps - local app_details=$(az containerapp show --name "$1" --resource-group "$2" 2>&1) - if [[ "$app_details" == *"ResourceNotFound"* ]] || [[ "$app_details" == *"ResourceGroupNotFound"* ]] || [[ "$app_details" == *"ERROR"* ]] ; then - echo "false" - else - local result=$(echo "$app_details" | jq -r '.properties.configuration.ingress.customDomains') - [[ "$result" != "null" ]] && echo "true" || echo "false" - fi -} if [[ "$DOMAIN_NAME" == "-" ]]; then # "-" is used to indicate that the domain is not configured DOMAIN_NAME="" fi -CONTAINER_REGISTRY_NAME=$UNIQUE_PREFIX$ENVIRONMENT -ENVIRONMENT_RESOURCE_GROUP_NAME="$UNIQUE_PREFIX-$ENVIRONMENT" -RESOURCE_GROUP_NAME="$ENVIRONMENT_RESOURCE_GROUP_NAME-$CLUSTER_LOCATION_ACRONYM" -IS_DOMAIN_CONFIGURED=$(is_domain_configured "app-gateway" "$RESOURCE_GROUP_NAME") +export UNIQUE_PREFIX +export ENVIRONMENT +export LOCATION=$CLUSTER_LOCATION +export DOMAIN_NAME +export SQL_ADMIN_OBJECT_ID + +export CONTAINER_REGISTRY_NAME=$UNIQUE_PREFIX$ENVIRONMENT +export GLOBAL_RESOURCE_GROUP_NAME="$UNIQUE_PREFIX-$ENVIRONMENT-global" +export CLUSTER_RESOURCE_GROUP_NAME="$UNIQUE_PREFIX-$ENVIRONMENT-$CLUSTER_LOCATION_ACRONYM" -APP_GATEWAY_VERSION=$(get_active_version "app-gateway" $RESOURCE_GROUP_NAME) -ACTIVE_ACCOUNT_MANAGEMENT_VERSION=$(get_active_version "account-management-api" $RESOURCE_GROUP_NAME) # The version from the API is use for both API and Workers -ACTIVE_BACK_OFFICE_VERSION=$(get_active_version "back-office-api" $RESOURCE_GROUP_NAME) # The version from the API is use for both API and Workers +export APP_GATEWAY_VERSION=$(get_active_version "app-gateway" $CLUSTER_RESOURCE_GROUP_NAME) +export ACCOUNT_MANAGEMENT_VERSION=$(get_active_version "account-management-api" $CLUSTER_RESOURCE_GROUP_NAME) # The version from the API is use for both API and Workers +export BACK_OFFICE_VERSION=$(get_active_version "back-office-api" $CLUSTER_RESOURCE_GROUP_NAME) # The version from the API is use for both API and Workers -az extension add --name application-insights --allow-preview true -APPLICATIONINSIGHTS_CONNECTION_STRING=$(az monitor app-insights component show --app $UNIQUE_PREFIX-$ENVIRONMENT --resource-group $UNIQUE_PREFIX-$ENVIRONMENT --query connectionString --output tsv) +az extension add --name application-insights --allow-preview true --only-show-errors + +# Check if Application Insights exists before trying to get connection string +if az group exists --name $GLOBAL_RESOURCE_GROUP_NAME 2>/dev/null | grep -q "true"; then + export APPLICATIONINSIGHTS_CONNECTION_STRING=$(az monitor app-insights component show --app $UNIQUE_PREFIX-$ENVIRONMENT --resource-group $GLOBAL_RESOURCE_GROUP_NAME --query connectionString --output tsv) +else + export APPLICATIONINSIGHTS_CONNECTION_STRING="" +fi CURRENT_DATE=$(date +'%Y-%m-%dT%H-%M') -DEPLOYMENT_COMMAND="az deployment sub create" -DEPLOYMENT_PARAMETERS="-l $CLUSTER_LOCATION -n $CURRENT_DATE-$RESOURCE_GROUP_NAME --output json -f ./main-cluster.bicep -p resourceGroupName=$RESOURCE_GROUP_NAME environmentResourceGroupName=$ENVIRONMENT_RESOURCE_GROUP_NAME environment=$ENVIRONMENT containerRegistryName=$CONTAINER_REGISTRY_NAME domainName=$DOMAIN_NAME isDomainConfigured=$IS_DOMAIN_CONFIGURED sqlAdminObjectId=$SQL_ADMIN_OBJECT_ID appGatewayVersion=$APP_GATEWAY_VERSION accountManagementVersion=$ACTIVE_ACCOUNT_MANAGEMENT_VERSION backOfficeVersion=$ACTIVE_BACK_OFFICE_VERSION applicationInsightsConnectionString=$APPLICATIONINSIGHTS_CONNECTION_STRING" +export REVISION_SUFFIX=$(printf "%04x" $RANDOM | head -c 4) cd "$(dirname "${BASH_SOURCE[0]}")" + +# Build the .bicepparam file to generate parameters.json +bicep build-params ./main-cluster.bicepparam --outfile ./main-cluster.parameters.json + +DEPLOYMENT_COMMAND="az deployment sub create" +DEPLOYMENT_PARAMETERS="-l $CLUSTER_LOCATION -n $CURRENT_DATE-$CLUSTER_RESOURCE_GROUP_NAME --output json -f ./main-cluster.bicep -p ./main-cluster.parameters.json" + . ../deploy.sh -# When initially creating the Azure Container App with SSL and a custom domain, we need to run the deployment three times (see https://github.com/microsoft/azure-container-apps/tree/main/docs/templates/bicep/managedCertificates): -# 1. On the initial run, the deployment will fail, providing instructions on how to manually create DNS TXT and CNAME records. After doing so, the workflow must be run again. -# 2. The second time, the DNS will be configured, and a certificate will be created. However, they will not be bound together, as this is a two-step process and they cannot be created in a single deployment. -# 3. The third deployment will bind the SSL Certificate to the Domain. This step will be triggered automatically. +# When initially creating the Azure Container App with SSL and a custom domain, the deployment may fail if DNS records are not configured. +# With bindingType: 'Auto' (API version 2025-07-01), certificates are created and bound in a single deployment. +# If the deployment fails, ensure DNS records are properly configured: +# - A TXT record: asuid. with the customDomainVerificationId value +# - A CNAME record: pointing to the container app's default domain if [[ "$*" == *"--apply"* ]] then RED='\033[0;31m' @@ -65,10 +73,10 @@ then cleaned_output=$(echo "$output" | sed '/^WARNING/d' | sed '/^\/home\/runner\/work\//d') # Check for the specific error message indicating that DNS Records are missing - if [[ $cleaned_output == *"InvalidCustomHostNameValidation"* ]] || [[ $cleaned_output == *"FailedCnameValidation"* ]] || [[ $cleaned_output == *"-certificate' under resource group '$RESOURCE_GROUP_NAME' was not found"* ]]; then - # Get details about the container apps environment. Although the creation of the container app fails, the verification ID on the container apps environment is consistent across all container apps. - env_details=$(az containerapp env show --name $RESOURCE_GROUP_NAME --resource-group $RESOURCE_GROUP_NAME) - + if [[ $cleaned_output == *"InvalidCustomHostNameValidation"* ]] || [[ $cleaned_output == *"FailedCnameValidation"* ]]; then + # Get details about the container apps environment to provide DNS configuration instructions + env_details=$(az containerapp env show --name $CLUSTER_RESOURCE_GROUP_NAME --resource-group $CLUSTER_RESOURCE_GROUP_NAME) + # Extract the customDomainVerificationId and defaultDomain from the container apps environment custom_domain_verification_id=$(echo "$env_details" | jq -r '.properties.customDomainConfiguration.customDomainVerificationId') default_domain=$(echo "$env_details" | jq -r '.properties.defaultDomain') @@ -83,20 +91,6 @@ then exit 1 fi - # If the domain was not configured during the first run and we didn't receive any warnings about missing DNS entries, we trigger the deployment again to complete the binding of the SSL Certificate to the domain. - if [[ "$IS_DOMAIN_CONFIGURED" == "false" ]] && [[ "$DOMAIN_NAME" != "" ]]; then - echo "Running deployment again to finalize setting up SSL certificate for $DOMAIN_NAME" - IS_DOMAIN_CONFIGURED=$(is_domain_configured "app-gateway" $RESOURCE_GROUP_NAME) - DEPLOYMENT_PARAMETERS="-l $CLUSTER_LOCATION -n $CURRENT_DATE-$RESOURCE_GROUP_NAME --output json -f ./main-cluster.bicep -p resourceGroupName=$RESOURCE_GROUP_NAME environmentResourceGroupName=$ENVIRONMENT_RESOURCE_GROUP_NAME environment=$ENVIRONMENT containerRegistryName=$CONTAINER_REGISTRY_NAME domainName=$DOMAIN_NAME isDomainConfigured=$IS_DOMAIN_CONFIGURED sqlAdminObjectId=$SQL_ADMIN_OBJECT_ID appGatewayVersion=$APP_GATEWAY_VERSION accountManagementVersion=$ACTIVE_ACCOUNT_MANAGEMENT_VERSION backOfficeVersion=$ACTIVE_BACK_OFFICE_VERSION applicationInsightsConnectionString=$APPLICATIONINSIGHTS_CONNECTION_STRING" - . ../deploy.sh - - cleaned_output=$(echo "$output" | sed '/^WARNING/d' | sed '/^\/home\/runner\/work\//d') - if [[ $cleaned_output == "ERROR:"* ]]; then - echo -e "${RED}$output" - exit 1 - fi - fi - # Extract the ID of the Managed Identities, which can be used to grant access to SQL Database ACCOUNT_MANAGEMENT_IDENTITY_CLIENT_ID=$(echo "$cleaned_output" | jq -r '.properties.outputs.accountManagementIdentityClientId.value') BACK_OFFICE_IDENTITY_CLIENT_ID=$(echo "$cleaned_output" | jq -r '.properties.outputs.backOfficeIdentityClientId.value') diff --git a/cloud-infrastructure/cluster/firewall.sh b/cloud-infrastructure/cluster/firewall.sh index f06fe8c2e2..7f3c9d2c7e 100644 --- a/cloud-infrastructure/cluster/firewall.sh +++ b/cloud-infrastructure/cluster/firewall.sh @@ -4,8 +4,8 @@ FIREWALL_RULE_NAME="GitHub Action Workflows - ${SQL_DATABASE_NAME} - Only active if [[ "$1" == "open" ]] then echo "$(date +"%Y-%m-%dT%H:%M:%S") Add the IP $IP_ADDRESS to the SQL Server firewall on server $SQL_SERVER_NAME for database $SQL_DATABASE_NAME" - az sql server firewall-rule create --resource-group $RESOURCE_GROUP_NAME --server $SQL_SERVER_NAME --name "$FIREWALL_RULE_NAME" --start-ip-address $IP_ADDRESS --end-ip-address $IP_ADDRESS + az sql server firewall-rule create --resource-group $CLUSTER_RESOURCE_GROUP_NAME --server $SQL_SERVER_NAME --name "$FIREWALL_RULE_NAME" --start-ip-address $IP_ADDRESS --end-ip-address $IP_ADDRESS else echo "$(date +"%Y-%m-%dT%H:%M:%S") Delete the IP $IP_ADDRESS from the SQL Server firewall on server $SQL_SERVER_NAME for database $SQL_DATABASE_NAME" - az sql server firewall-rule delete --resource-group $RESOURCE_GROUP_NAME --server $SQL_SERVER_NAME --name "$FIREWALL_RULE_NAME" + az sql server firewall-rule delete --resource-group $CLUSTER_RESOURCE_GROUP_NAME --server $SQL_SERVER_NAME --name "$FIREWALL_RULE_NAME" fi diff --git a/cloud-infrastructure/cluster/grant-database-permissions.sh b/cloud-infrastructure/cluster/grant-database-permissions.sh index 2cb491f69a..3aeab581d6 100755 --- a/cloud-infrastructure/cluster/grant-database-permissions.sh +++ b/cloud-infrastructure/cluster/grant-database-permissions.sh @@ -4,9 +4,9 @@ CLUSTER_LOCATION_ACRONYM=$3 SQL_DATABASE_NAME=$4 MANAGEMENT_IDENTITY_CLIENT_ID=$5 -RESOURCE_GROUP_NAME=$UNIQUE_PREFIX-$ENVIRONMENT-$CLUSTER_LOCATION_ACRONYM -MANAGED_IDENTITY_NAME=$RESOURCE_GROUP_NAME-$4 -SQL_SERVER_NAME=$RESOURCE_GROUP_NAME +CLUSTER_RESOURCE_GROUP_NAME=$UNIQUE_PREFIX-$ENVIRONMENT-$CLUSTER_LOCATION_ACRONYM +MANAGED_IDENTITY_NAME=$CLUSTER_RESOURCE_GROUP_NAME-$4 +SQL_SERVER_NAME=$CLUSTER_RESOURCE_GROUP_NAME SQL_SERVER=$SQL_SERVER_NAME.database.windows.net cd "$(dirname "${BASH_SOURCE[0]}")" @@ -26,7 +26,7 @@ SID=$(awk -v id="$SID" 'BEGIN { substr(id,17) }') # Reverse the byte order for the first three sections of the GUID and concatenate -echo "$(date +"%Y-%m-%dT%H:%M:%S") Granting $MANAGED_IDENTITY_NAME (ID: $SID) in Recource group $RESOURCE_GROUP_NAME permissions on $SQL_SERVER/$SQL_DATABASE_NAME database" +echo "$(date +"%Y-%m-%dT%H:%M:%S") Granting $MANAGED_IDENTITY_NAME (ID: $SID) in Resource group $CLUSTER_RESOURCE_GROUP_NAME permissions on $SQL_SERVER/$SQL_DATABASE_NAME database" # Execute the SQL script using mssql-scripter. Pass the script as a heredoc to sqlcmd to allow for complex SQL. sqlcmd -S $SQL_SERVER -d $SQL_DATABASE_NAME --authentication-method=ActiveDirectoryDefault --exit-on-error << EOF diff --git a/cloud-infrastructure/cluster/main-cluster.bicep b/cloud-infrastructure/cluster/main-cluster.bicep index f71cd01c56..17425901b9 100644 --- a/cloud-infrastructure/cluster/main-cluster.bicep +++ b/cloud-infrastructure/cluster/main-cluster.bicep @@ -1,12 +1,11 @@ targetScope = 'subscription' param location string = deployment().location -param resourceGroupName string -param environmentResourceGroupName string +param clusterResourceGroupName string +param globalResourceGroupName string param environment string param containerRegistryName string param domainName string -param isDomainConfigured bool param sqlAdminObjectId string param appGatewayVersion string param accountManagementVersion string @@ -14,33 +13,38 @@ param backOfficeVersion string param applicationInsightsConnectionString string param communicationServicesDataLocation string = 'europe' param mailSenderDisplayName string = 'PlatformPlatform' +param revisionSuffix string -var storageAccountUniquePrefix = replace(resourceGroupName, '-', '') +var storageAccountUniquePrefix = replace(clusterResourceGroupName, '-', '') var tags = { environment: environment, 'managed-by': 'bicep' } -resource clusterResourceGroup 'Microsoft.Resources/resourceGroups@2024-11-01' = { - name: resourceGroupName +resource clusterResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = { + name: clusterResourceGroupName location: location tags: tags } +// Derive the resource name prefix by removing location suffix from cluster resource group name +// e.g., "pxp-stage-eu" -> "pxp-stage" +var resourceNamePrefix = substring(clusterResourceGroupName, 0, lastIndexOf(clusterResourceGroupName, '-')) + resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = { - scope: resourceGroup('${environmentResourceGroupName}') - name: environmentResourceGroupName + scope: resourceGroup('${globalResourceGroupName}') + name: resourceNamePrefix } var subnetId = resourceId( subscription().subscriptionId, - resourceGroupName, + clusterResourceGroupName, 'Microsoft.Network/virtualNetworks/subnets', - resourceGroupName, + clusterResourceGroupName, 'subnet' ) var diagnosticStorageAccountName = '${storageAccountUniquePrefix}diagnostic' module diagnosticStorageAccount '../modules/storage-account.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-diagnostic-storage-account' + name: '${clusterResourceGroupName}-diagnostic-storage-account' params: { location: location name: diagnosticStorageAccountName @@ -51,10 +55,10 @@ module diagnosticStorageAccount '../modules/storage-account.bicep' = { module virtualNetwork '../modules/virtual-network.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-virtual-network' + name: '${clusterResourceGroupName}-virtual-network' params: { location: location - name: resourceGroupName + name: clusterResourceGroupName tags: tags address: '10.0.0.0' } @@ -62,23 +66,24 @@ module virtualNetwork '../modules/virtual-network.bicep' = { module containerAppsEnvironment '../modules/container-apps-environment.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-container-apps-environment' + name: '${clusterResourceGroupName}-container-apps-environment' params: { location: location - name: resourceGroupName + name: clusterResourceGroupName tags: tags subnetId: subnetId - environmentResourceGroupName: environmentResourceGroupName + globalResourceGroupName: globalResourceGroupName + logAnalyticsWorkspaceName: resourceNamePrefix } dependsOn: [virtualNetwork] } module keyVault '../modules/key-vault.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-key-vault' + name: '${clusterResourceGroupName}-key-vault' params: { location: location - name: resourceGroupName + name: clusterResourceGroupName tags: tags tenantId: subscription().tenantId subnetId: subnetId @@ -90,9 +95,9 @@ module keyVault '../modules/key-vault.bicep' = { module communicationService '../modules/communication-services.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-communication-services' + name: '${clusterResourceGroupName}-communication-services' params: { - name: resourceGroupName + name: clusterResourceGroupName tags: tags dataLocation: communicationServicesDataLocation mailSenderDisplayName: mailSenderDisplayName @@ -102,10 +107,10 @@ module communicationService '../modules/communication-services.bicep' = { module microsoftSqlServer '../modules/microsoft-sql-server.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-microsoft-sql-server' + name: '${clusterResourceGroupName}-microsoft-sql-server' params: { location: location - name: resourceGroupName + name: clusterResourceGroupName tags: tags subnetId: subnetId tenantId: subscription().tenantId @@ -116,13 +121,14 @@ module microsoftSqlServer '../modules/microsoft-sql-server.bicep' = { module microsoftSqlDerverDiagnosticConfiguration '../modules/microsoft-sql-server-diagnostic.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-microsoft-sql-server-diagnostic' + name: '${clusterResourceGroupName}-microsoft-sql-server-diagnostic' params: { diagnosticStorageAccountName: diagnosticStorageAccountName - microsoftSqlServerName: resourceGroupName + microsoftSqlServerName: clusterResourceGroupName dianosticStorageAccountBlobEndpoint: diagnosticStorageAccount.outputs.blobEndpoint dianosticStorageAccountSubscriptionId: subscription().subscriptionId } + dependsOn: [microsoftSqlServer] } var isCustomDomainSet = domainName != '' @@ -133,26 +139,26 @@ var cdnUrl = publicUrl // Account Management -var accountManagementIdentityName = '${resourceGroupName}-account-management' +var accountManagementIdentityName = '${clusterResourceGroupName}-account-management' module accountManagementIdentity '../modules/user-assigned-managed-identity.bicep' = { - name: '${resourceGroupName}-account-management-managed-identity' + name: '${clusterResourceGroupName}-account-management-managed-identity' scope: clusterResourceGroup params: { name: accountManagementIdentityName location: location tags: tags containerRegistryName: containerRegistryName - environmentResourceGroupName: environmentResourceGroupName + globalResourceGroupName: globalResourceGroupName keyVaultName: keyVault.outputs.name grantKeyVaultWritePermissions: true } } module accountManagementDatabase '../modules/microsoft-sql-database.bicep' = { - name: '${resourceGroupName}-account-management-sql-database' + name: '${clusterResourceGroupName}-account-management-sql-database' scope: clusterResourceGroup params: { - sqlServerName: resourceGroupName + sqlServerName: clusterResourceGroupName databaseName: 'account-management' location: location tags: tags @@ -163,7 +169,7 @@ module accountManagementDatabase '../modules/microsoft-sql-database.bicep' = { var accountManagementStorageAccountName = '${storageAccountUniquePrefix}acctmgmt' module accountManagementStorageAccount '../modules/storage-account.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-account-management-storage-account' + name: '${clusterResourceGroupName}-account-management-storage-account' params: { location: location name: accountManagementStorageAccountName @@ -220,13 +226,13 @@ var accountManagementEnvironmentVariables = [ ] module accountManagementWorkers '../modules/container-app.bicep' = { - name: '${resourceGroupName}-account-management-workers-container-app' + name: '${clusterResourceGroupName}-account-management-workers-container-app' scope: clusterResourceGroup params: { name: 'account-management-workers' location: location tags: tags - resourceGroupName: resourceGroupName + clusterResourceGroupName: clusterResourceGroupName containerAppsEnvironmentId: containerAppsEnvironment.outputs.environmentId containerAppsEnvironmentName: containerAppsEnvironment.outputs.name containerRegistryName: containerRegistryName @@ -239,18 +245,19 @@ module accountManagementWorkers '../modules/container-app.bicep' = { userAssignedIdentityName: accountManagementIdentityName ingress: true hasProbesEndpoint: false + revisionSuffix: revisionSuffix environmentVariables: accountManagementEnvironmentVariables } } module accountManagementApi '../modules/container-app.bicep' = { - name: '${resourceGroupName}-account-management-api-container-app' + name: '${clusterResourceGroupName}-account-management-api-container-app' scope: clusterResourceGroup params: { name: 'account-management-api' location: location tags: tags - resourceGroupName: resourceGroupName + clusterResourceGroupName: clusterResourceGroupName containerAppsEnvironmentId: containerAppsEnvironment.outputs.environmentId containerAppsEnvironmentName: containerAppsEnvironment.outputs.name containerRegistryName: containerRegistryName @@ -263,6 +270,7 @@ module accountManagementApi '../modules/container-app.bicep' = { userAssignedIdentityName: accountManagementIdentityName ingress: true hasProbesEndpoint: true + revisionSuffix: revisionSuffix environmentVariables: accountManagementEnvironmentVariables } dependsOn: [accountManagementWorkers] @@ -270,25 +278,25 @@ module accountManagementApi '../modules/container-app.bicep' = { // Back Office -var backOfficeIdentityName = '${resourceGroupName}-back-office' +var backOfficeIdentityName = '${clusterResourceGroupName}-back-office' module backOfficeIdentity '../modules/user-assigned-managed-identity.bicep' = { - name: '${resourceGroupName}-back-office-managed-identity' + name: '${clusterResourceGroupName}-back-office-managed-identity' scope: clusterResourceGroup params: { name: backOfficeIdentityName location: location tags: tags containerRegistryName: containerRegistryName - environmentResourceGroupName: environmentResourceGroupName + globalResourceGroupName: globalResourceGroupName keyVaultName: keyVault.outputs.name } } module backOfficeDatabase '../modules/microsoft-sql-database.bicep' = { - name: '${resourceGroupName}-back-office-sql-database' + name: '${clusterResourceGroupName}-back-office-sql-database' scope: clusterResourceGroup params: { - sqlServerName: resourceGroupName + sqlServerName: clusterResourceGroupName databaseName: 'back-office' location: location tags: tags @@ -299,7 +307,7 @@ module backOfficeDatabase '../modules/microsoft-sql-database.bicep' = { var backOfficeStorageAccountName = '${storageAccountUniquePrefix}backoffice' module backOfficeStorageAccount '../modules/storage-account.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-back-office-storage-account' + name: '${clusterResourceGroupName}-back-office-storage-account' params: { location: location name: backOfficeStorageAccountName @@ -346,13 +354,13 @@ var backOfficeEnvironmentVariables = [ ] module backOfficeWorkers '../modules/container-app.bicep' = { - name: '${resourceGroupName}-back-office-workers-container-app' + name: '${clusterResourceGroupName}-back-office-workers-container-app' scope: clusterResourceGroup params: { name: 'back-office-workers' location: location tags: tags - resourceGroupName: resourceGroupName + clusterResourceGroupName: clusterResourceGroupName containerAppsEnvironmentId: containerAppsEnvironment.outputs.environmentId containerAppsEnvironmentName: containerAppsEnvironment.outputs.name containerRegistryName: containerRegistryName @@ -365,18 +373,19 @@ module backOfficeWorkers '../modules/container-app.bicep' = { userAssignedIdentityName: backOfficeIdentityName ingress: true hasProbesEndpoint: false + revisionSuffix: revisionSuffix environmentVariables: backOfficeEnvironmentVariables } } module backOfficeApi '../modules/container-app.bicep' = { - name: '${resourceGroupName}-back-office-api-container-app' + name: '${clusterResourceGroupName}-back-office-api-container-app' scope: clusterResourceGroup params: { name: 'back-office-api' location: location tags: tags - resourceGroupName: resourceGroupName + clusterResourceGroupName: clusterResourceGroupName containerAppsEnvironmentId: containerAppsEnvironment.outputs.environmentId containerAppsEnvironmentName: containerAppsEnvironment.outputs.name containerRegistryName: containerRegistryName @@ -389,6 +398,7 @@ module backOfficeApi '../modules/container-app.bicep' = { userAssignedIdentityName: backOfficeIdentityName ingress: true hasProbesEndpoint: true + revisionSuffix: revisionSuffix environmentVariables: backOfficeEnvironmentVariables } dependsOn: [backOfficeWorkers] @@ -396,29 +406,29 @@ module backOfficeApi '../modules/container-app.bicep' = { // App Gateway -var appGatewayIdentityName = '${resourceGroupName}-app-gateway' +var appGatewayIdentityName = '${clusterResourceGroupName}-app-gateway' module appGatewayIdentity '../modules/user-assigned-managed-identity.bicep' = { - name: '${resourceGroupName}-app-gateway-managed-identity' + name: '${clusterResourceGroupName}-app-gateway-managed-identity' scope: clusterResourceGroup params: { name: appGatewayIdentityName location: location tags: tags containerRegistryName: containerRegistryName - environmentResourceGroupName: environmentResourceGroupName + globalResourceGroupName: globalResourceGroupName keyVaultName: keyVault.outputs.name } } var appGatewayContainerAppName = 'app-gateway' module appGateway '../modules/container-app.bicep' = { - name: '${resourceGroupName}-app-gateway-container-app' + name: '${clusterResourceGroupName}-app-gateway-container-app' scope: clusterResourceGroup params: { name: appGatewayContainerAppName location: location tags: tags - resourceGroupName: resourceGroupName + clusterResourceGroupName: clusterResourceGroupName containerAppsEnvironmentId: containerAppsEnvironment.outputs.environmentId containerAppsEnvironmentName: containerAppsEnvironment.outputs.name containerRegistryName: containerRegistryName @@ -432,8 +442,8 @@ module appGateway '../modules/container-app.bicep' = { ingress: true hasProbesEndpoint: false domainName: domainName == '' ? '' : domainName - isDomainConfigured: domainName != '' && isDomainConfigured external: true + revisionSuffix: revisionSuffix environmentVariables: [ { name: 'AZURE_CLIENT_ID' @@ -463,9 +473,9 @@ module appGateway '../modules/container-app.bicep' = { } } -module appGatwayAccountManagementStorageBlobDataReaderRoleAssignment '../modules/role-assignments-storage-blob-data-reader.bicep' = { +module appGatewayAccountManagementStorageBlobDataReaderRoleAssignment '../modules/role-assignments-storage-blob-data-reader.bicep' = { scope: clusterResourceGroup - name: '${resourceGroupName}-app-gateway-account-management-blob-reader' + name: '${clusterResourceGroupName}-app-gateway-account-management-blob-reader' params: { storageAccountName: accountManagementStorageAccountName userAssignedIdentityName: appGatewayIdentityName diff --git a/cloud-infrastructure/cluster/main-cluster.bicepparam b/cloud-infrastructure/cluster/main-cluster.bicepparam new file mode 100644 index 0000000000..c727ffd2ae --- /dev/null +++ b/cloud-infrastructure/cluster/main-cluster.bicepparam @@ -0,0 +1,15 @@ +using './main-cluster.bicep' + +// Read all parameters from environment variables +param location = readEnvironmentVariable('LOCATION') +param clusterResourceGroupName = readEnvironmentVariable('CLUSTER_RESOURCE_GROUP_NAME') +param globalResourceGroupName = readEnvironmentVariable('GLOBAL_RESOURCE_GROUP_NAME') +param environment = readEnvironmentVariable('ENVIRONMENT') +param containerRegistryName = readEnvironmentVariable('CONTAINER_REGISTRY_NAME') +param domainName = readEnvironmentVariable('DOMAIN_NAME', '') +param sqlAdminObjectId = readEnvironmentVariable('SQL_ADMIN_OBJECT_ID') +param appGatewayVersion = readEnvironmentVariable('APP_GATEWAY_VERSION') +param accountManagementVersion = readEnvironmentVariable('ACCOUNT_MANAGEMENT_VERSION') +param backOfficeVersion = readEnvironmentVariable('BACK_OFFICE_VERSION') +param applicationInsightsConnectionString = readEnvironmentVariable('APPLICATIONINSIGHTS_CONNECTION_STRING') +param revisionSuffix = readEnvironmentVariable('REVISION_SUFFIX') diff --git a/cloud-infrastructure/environment/deploy-environment.sh b/cloud-infrastructure/environment/deploy-environment.sh index abd3b7ef51..a72d24424e 100755 --- a/cloud-infrastructure/environment/deploy-environment.sh +++ b/cloud-infrastructure/environment/deploy-environment.sh @@ -5,11 +5,14 @@ ENVIRONMENT=$2 LOCATION_SHARED=$3 PRODUCTION_SERVICE_PRINCIPAL_OBJECT_ID=$4 -RESOURCE_GROUP_NAME=$UNIQUE_PREFIX-$ENVIRONMENT +export UNIQUE_PREFIX +export ENVIRONMENT + +GLOBAL_RESOURCE_GROUP_NAME=$UNIQUE_PREFIX-$ENVIRONMENT-global CONTAINER_REGISTRY_NAME=$UNIQUE_PREFIX$ENVIRONMENT CURRENT_DATE=$(date +'%Y-%m-%dT%H-%M') DEPLOYMENT_COMMAND="az deployment sub create" -DEPLOYMENT_PARAMETERS="-l $LOCATION_SHARED -n $CURRENT_DATE-$UNIQUE_PREFIX-$ENVIRONMENT --output table -f ./main-environment.bicep -p resourceGroupName=$RESOURCE_GROUP_NAME environment=$ENVIRONMENT containerRegistryName=$CONTAINER_REGISTRY_NAME" +DEPLOYMENT_PARAMETERS="-l $LOCATION_SHARED -n $CURRENT_DATE-$UNIQUE_PREFIX-$ENVIRONMENT --output table -f ./main-environment.bicep -p globalResourceGroupName=$GLOBAL_RESOURCE_GROUP_NAME uniquePrefix=$UNIQUE_PREFIX environment=$ENVIRONMENT containerRegistryName=$CONTAINER_REGISTRY_NAME" # Add production service principal object ID parameter if provided for ACR Pull role assignment to staging Container Registry if [[ "$PRODUCTION_SERVICE_PRINCIPAL_OBJECT_ID" != "-" ]]; then diff --git a/cloud-infrastructure/environment/main-environment.bicep b/cloud-infrastructure/environment/main-environment.bicep index 0300e5a9aa..e707317755 100644 --- a/cloud-infrastructure/environment/main-environment.bicep +++ b/cloud-infrastructure/environment/main-environment.bicep @@ -1,22 +1,24 @@ targetScope = 'subscription' param location string = deployment().location -param resourceGroupName string +param globalResourceGroupName string +param uniquePrefix string param environment string param containerRegistryName string param productionServicePrincipalObjectId string = '' var tags = { environment: environment, 'managed-by': 'bicep' } +var resourceNamePrefix = '${uniquePrefix}-${environment}' -resource environmentResourceGroup 'Microsoft.Resources/resourceGroups@2024-11-01' = { - name: resourceGroupName +resource globalResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = { + name: globalResourceGroupName location: location tags: tags } module containerRegistry '../modules/container-registry.bicep' = { - name: '${resourceGroupName}-container-registry' - scope: resourceGroup(environmentResourceGroup.name) + name: '${globalResourceGroupName}-container-registry' + scope: resourceGroup(globalResourceGroup.name) params: { name: containerRegistryName location: location @@ -26,8 +28,8 @@ module containerRegistry '../modules/container-registry.bicep' = { // Grant production service principal Container Registry Data Importer access to registry if specified module productionServicePrincipalDataImporter '../modules/role-assignments-container-registry-data-importer.bicep' = if (!empty(productionServicePrincipalObjectId)) { - name: '${resourceGroupName}-production-sp-data-importer' - scope: resourceGroup(environmentResourceGroup.name) + name: '${globalResourceGroupName}-production-sp-data-importer' + scope: resourceGroup(globalResourceGroup.name) params: { containerRegistryName: containerRegistryName principalId: productionServicePrincipalObjectId @@ -36,20 +38,20 @@ module productionServicePrincipalDataImporter '../modules/role-assignments-conta } module logAnalyticsWorkspace '../modules/log-analytics-workspace.bicep' = { - name: '${resourceGroupName}-log-analytics-workspace' - scope: resourceGroup(environmentResourceGroup.name) + name: '${globalResourceGroupName}-log-analytics-workspace' + scope: resourceGroup(globalResourceGroup.name) params: { - name: resourceGroupName + name: resourceNamePrefix location: location tags: tags } } module applicationInsights '../modules/application-insights.bicep' = { - name: '${resourceGroupName}-application-insights' - scope: resourceGroup(environmentResourceGroup.name) + name: '${globalResourceGroupName}-application-insights' + scope: resourceGroup(globalResourceGroup.name) params: { - name: resourceGroupName + name: resourceNamePrefix location: location tags: tags logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.workspaceId diff --git a/cloud-infrastructure/modules/communication-services.bicep b/cloud-infrastructure/modules/communication-services.bicep index 63cd152a1c..f03f7a4f0d 100644 --- a/cloud-infrastructure/modules/communication-services.bicep +++ b/cloud-infrastructure/modules/communication-services.bicep @@ -43,11 +43,11 @@ resource communicationServices 'Microsoft.Communication/communicationServices@20 } } -resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { +resource keyVault 'Microsoft.KeyVault/vaults@2025-05-01' existing = { name: keyVaultName } -resource communicationServiceConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { +resource communicationServiceConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2025-05-01' = { parent: keyVault name: 'communication-services-connection-string' properties: { diff --git a/cloud-infrastructure/modules/container-app.bicep b/cloud-infrastructure/modules/container-app.bicep index 16bd44071c..6449c38b6b 100644 --- a/cloud-infrastructure/modules/container-app.bicep +++ b/cloud-infrastructure/modules/container-app.bicep @@ -1,7 +1,7 @@ param name string param location string param tags object -param resourceGroupName string +param clusterResourceGroupName string param containerAppsEnvironmentId string param containerAppsEnvironmentName string param containerRegistryName string @@ -15,26 +15,30 @@ param userAssignedIdentityName string param ingress bool param hasProbesEndpoint bool param domainName string = '' -param isDomainConfigured bool = false param external bool = false param environmentVariables object[] = [] -param uniqueSuffix string = substring(newGuid(), 0, 4) +param revisionSuffix string -resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { - scope: resourceGroup(resourceGroupName) +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = { + scope: resourceGroup(clusterResourceGroupName) name: userAssignedIdentityName } var certificateName = '${domainName}-certificate' // Note: The `-certificate` is used to detect if a certificate in deploy-cluster.sh var isCustomDomainSet = domainName != '' -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-02-preview' existing = if (isCustomDomainSet) { - name: containerAppsEnvironmentName -} +var customDomainConfiguration = isCustomDomainSet + ? [ + { + name: domainName + bindingType: 'Auto' + } + ] + : [] module newManagedCertificate './managed-certificate.bicep' = if (isCustomDomainSet) { - name: '${resourceGroupName}-${name}-managed-certificate' - scope: resourceGroup(resourceGroupName) + name: '${clusterResourceGroupName}-${name}-managed-certificate' + scope: resourceGroup(clusterResourceGroupName) dependsOn: [containerApp] params: { name: certificateName @@ -45,21 +49,6 @@ module newManagedCertificate './managed-certificate.bicep' = if (isCustomDomainS } } -resource existingManagedCertificate 'Microsoft.App/managedEnvironments/managedCertificates@2023-05-02-preview' existing = if (isDomainConfigured) { - name: certificateName - parent: containerAppsEnvironment -} - -var customDomainConfiguration = isCustomDomainSet - ? [ - { - bindingType: isDomainConfigured ? 'SniEnabled' : 'Disabled' - name: domainName - certificateId: isDomainConfigured ? existingManagedCertificate.id : null - } - ] - : [] - // For the initial revision, we use the container image hello world quickstart image. // This allows for the container app to be created before the container image is pushed to the registry. var useQuickStartImage = containerImageTag == 'initial' @@ -68,10 +57,10 @@ var image = useQuickStartImage ? 'ghcr.io/platformplatform/quickstart:latest' : '${containerRegistryServerUrl}/${containerImageName}:${containerImageTag}' -// Create a revisionSuffix that contains the version but is be unique for each deployment. E.g. "2024-4-24-1557-tzyb" -var revisionSuffix = '${replace(containerImageTag, '.', '-')}-${substring(uniqueSuffix, 0, 4)}' +// Create a revisionSuffix that contains the version and random suffix. E.g. "2025-11-19-756-a3f2" +var fullRevisionSuffix = '${replace(containerImageTag, '.', '-')}-${revisionSuffix}' -resource containerApp 'Microsoft.App/containerApps@2024-02-02-preview' = { +resource containerApp 'Microsoft.App/containerApps@2025-07-01' = { name: name location: location tags: tags @@ -147,7 +136,7 @@ resource containerApp 'Microsoft.App/containerApps@2024-02-02-preview' = { ] } ] - revisionSuffix: revisionSuffix + revisionSuffix: fullRevisionSuffix scale: { minReplicas: minReplicas maxReplicas: maxReplicas @@ -160,11 +149,6 @@ resource containerApp 'Microsoft.App/containerApps@2024-02-02-preview' = { identity: userAssignedIdentity.id } ] - runtime: { - dotnet: { - autoConfigureDataProtection: true - } - } ingress: ingress ? { external: external diff --git a/cloud-infrastructure/modules/container-apps-environment.bicep b/cloud-infrastructure/modules/container-apps-environment.bicep index 34543714fa..a89ace1134 100644 --- a/cloud-infrastructure/modules/container-apps-environment.bicep +++ b/cloud-infrastructure/modules/container-apps-environment.bicep @@ -2,17 +2,18 @@ param name string param location string param tags object param subnetId string -param environmentResourceGroupName string +param globalResourceGroupName string +param logAnalyticsWorkspaceName string -resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = { - scope: resourceGroup('${environmentResourceGroupName}') - name: environmentResourceGroupName +resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2025-07-01' existing = { + scope: resourceGroup('${globalResourceGroupName}') + name: logAnalyticsWorkspaceName } var logAnalyticsCustomerId = existingLogAnalyticsWorkspace.properties.customerId var logAnalyticsSharedKey = existingLogAnalyticsWorkspace.listKeys().primarySharedKey -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' = { +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2025-07-01' = { name: name location: location tags: tags diff --git a/cloud-infrastructure/modules/container-registry.bicep b/cloud-infrastructure/modules/container-registry.bicep index dacadf3ed4..aa7b532819 100644 --- a/cloud-infrastructure/modules/container-registry.bicep +++ b/cloud-infrastructure/modules/container-registry.bicep @@ -2,7 +2,7 @@ param name string param location string param tags object -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-08-01-preview' = { +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2025-11-01' = { name: name location: location tags: tags @@ -25,10 +25,6 @@ resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-08-01-pr azureADAuthenticationAsArmPolicy: { status: 'Enabled' } - softDeletePolicy: { - retentionDays: 7 - status: 'disabled' - } } } } diff --git a/cloud-infrastructure/modules/log-analytics-workspace.bicep b/cloud-infrastructure/modules/log-analytics-workspace.bicep index e7aa652c6e..ed1ac3a41c 100644 --- a/cloud-infrastructure/modules/log-analytics-workspace.bicep +++ b/cloud-infrastructure/modules/log-analytics-workspace.bicep @@ -2,7 +2,7 @@ param name string param location string param tags object -resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2025-07-01' = { name: name location: location tags: tags diff --git a/cloud-infrastructure/modules/managed-certificate.bicep b/cloud-infrastructure/modules/managed-certificate.bicep index 27fc6af2b2..a6509ed4d9 100644 --- a/cloud-infrastructure/modules/managed-certificate.bicep +++ b/cloud-infrastructure/modules/managed-certificate.bicep @@ -4,11 +4,11 @@ param tags object param containerAppsEnvironmentName string param domainName string -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' existing = { +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2025-07-01' existing = { name: containerAppsEnvironmentName } -resource managedCertificate 'Microsoft.App/managedEnvironments/managedCertificates@2024-03-01' = { +resource managedCertificate 'Microsoft.App/managedEnvironments/managedCertificates@2025-07-01' = { name: name parent: containerAppsEnvironment location: location diff --git a/cloud-infrastructure/modules/microsoft-sql-database.bicep b/cloud-infrastructure/modules/microsoft-sql-database.bicep index e3c2d66833..af14ed51c4 100644 --- a/cloud-infrastructure/modules/microsoft-sql-database.bicep +++ b/cloud-infrastructure/modules/microsoft-sql-database.bicep @@ -3,7 +3,7 @@ param databaseName string param location string param tags object -resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = { +resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-08-01' = { name: '${sqlServerName}/${databaseName}' location: location tags: tags diff --git a/cloud-infrastructure/modules/microsoft-sql-server-diagnostic.bicep b/cloud-infrastructure/modules/microsoft-sql-server-diagnostic.bicep index cc573c810f..385ecfe394 100644 --- a/cloud-infrastructure/modules/microsoft-sql-server-diagnostic.bicep +++ b/cloud-infrastructure/modules/microsoft-sql-server-diagnostic.bicep @@ -3,7 +3,7 @@ param microsoftSqlServerName string param dianosticStorageAccountSubscriptionId string param dianosticStorageAccountBlobEndpoint string -resource existingMicrosoftSqlServer 'Microsoft.Sql/servers@2023-05-01-preview' existing = { +resource existingMicrosoftSqlServer 'Microsoft.Sql/servers@2023-08-01' existing = { name: microsoftSqlServerName } @@ -17,13 +17,13 @@ module diagnosticStorageBlobDataContributorRoleAssignment './role-assignments-st } } -resource microsoftSqlServerOutboundFirewallRules 'Microsoft.Sql/servers/outboundFirewallRules@2023-05-01-preview' = { +resource microsoftSqlServerOutboundFirewallRules 'Microsoft.Sql/servers/outboundFirewallRules@2023-08-01' = { parent: existingMicrosoftSqlServer name: replace(replace(dianosticStorageAccountBlobEndpoint, 'https:', ''), '/', '') dependsOn: [diagnosticStorageBlobDataContributorRoleAssignment] } -resource microsoftSqlServerAuditingSettings 'Microsoft.Sql/servers/auditingSettings@2023-05-01-preview' = { +resource microsoftSqlServerAuditingSettings 'Microsoft.Sql/servers/auditingSettings@2023-08-01' = { parent: existingMicrosoftSqlServer name: 'default' properties: { @@ -42,7 +42,7 @@ resource microsoftSqlServerAuditingSettings 'Microsoft.Sql/servers/auditingSetti dependsOn: [microsoftSqlServerOutboundFirewallRules] } -resource microsoftSqlServerVulnerabilityAssessment 'Microsoft.Sql/servers/vulnerabilityAssessments@2023-05-01-preview' = { +resource microsoftSqlServerVulnerabilityAssessment 'Microsoft.Sql/servers/vulnerabilityAssessments@2023-08-01' = { name: 'default' parent: existingMicrosoftSqlServer properties: { diff --git a/cloud-infrastructure/modules/microsoft-sql-server.bicep b/cloud-infrastructure/modules/microsoft-sql-server.bicep index ffed094daa..27db6c328a 100644 --- a/cloud-infrastructure/modules/microsoft-sql-server.bicep +++ b/cloud-infrastructure/modules/microsoft-sql-server.bicep @@ -5,7 +5,7 @@ param subnetId string param tenantId string param sqlAdminObjectId string -resource microsoftSqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = { +resource microsoftSqlServer 'Microsoft.Sql/servers@2023-08-01' = { name: name location: location tags: tags @@ -27,7 +27,7 @@ resource microsoftSqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = { } } -resource sqlServerVirtualNetworkRule 'Microsoft.Sql/servers/virtualNetworkRules@2023-05-01-preview' = { +resource sqlServerVirtualNetworkRule 'Microsoft.Sql/servers/virtualNetworkRules@2023-08-01' = { name: 'sql-server-virtual-network-rule' parent: microsoftSqlServer properties: { @@ -36,7 +36,7 @@ resource sqlServerVirtualNetworkRule 'Microsoft.Sql/servers/virtualNetworkRules@ } } -resource microsoftSqlServerSecurityAlertPolicies 'Microsoft.Sql/servers/securityAlertPolicies@2023-05-01-preview' = { +resource microsoftSqlServerSecurityAlertPolicies 'Microsoft.Sql/servers/securityAlertPolicies@2023-08-01' = { parent: microsoftSqlServer name: 'Default' properties: { diff --git a/cloud-infrastructure/modules/role-assignments-container-registry-acr-pull.bicep b/cloud-infrastructure/modules/role-assignments-container-registry-acr-pull.bicep index a0ff961c9d..31e7e8a95a 100644 --- a/cloud-infrastructure/modules/role-assignments-container-registry-acr-pull.bicep +++ b/cloud-infrastructure/modules/role-assignments-container-registry-acr-pull.bicep @@ -1,7 +1,7 @@ param containerRegistryName string param principalId string -resource containerRegistryResource 'Microsoft.ContainerRegistry/registries@2023-08-01-preview' existing = { +resource containerRegistryResource 'Microsoft.ContainerRegistry/registries@2025-11-01' existing = { name: containerRegistryName } diff --git a/cloud-infrastructure/modules/role-assignments-container-registry-data-importer.bicep b/cloud-infrastructure/modules/role-assignments-container-registry-data-importer.bicep index 3870ea8d16..c8415fb2bd 100644 --- a/cloud-infrastructure/modules/role-assignments-container-registry-data-importer.bicep +++ b/cloud-infrastructure/modules/role-assignments-container-registry-data-importer.bicep @@ -1,7 +1,7 @@ param containerRegistryName string param principalId string -resource containerRegistryResource 'Microsoft.ContainerRegistry/registries@2023-08-01-preview' existing = { +resource containerRegistryResource 'Microsoft.ContainerRegistry/registries@2025-11-01' existing = { name: containerRegistryName } diff --git a/cloud-infrastructure/modules/role-assignments-storage-blob-data-contributor.bicep b/cloud-infrastructure/modules/role-assignments-storage-blob-data-contributor.bicep index 121915c64b..ac636a6ec8 100644 --- a/cloud-infrastructure/modules/role-assignments-storage-blob-data-contributor.bicep +++ b/cloud-infrastructure/modules/role-assignments-storage-blob-data-contributor.bicep @@ -2,12 +2,12 @@ param storageAccountName string param userAssignedIdentityName string = '' param principalId string = '' -resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (userAssignedIdentityName != '') { +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = if (userAssignedIdentityName != '') { scope: resourceGroup() name: userAssignedIdentityName ?? principalId } -resource existingStorageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { +resource existingStorageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' existing = { scope: resourceGroup() name: storageAccountName } @@ -16,6 +16,7 @@ var storageBlobDataContributorRoleDefinitionId = 'ba92f5b4-2d11-453d-a403-e96b00 resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(existingStorageAccount.id, userAssignedIdentityName, storageBlobDataContributorRoleDefinitionId) properties: { + #disable-next-line BCP318 principalId: userAssignedIdentityName == '' ? principalId : userAssignedIdentity.properties.principalId principalType: 'ServicePrincipal' roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataContributorRoleDefinitionId) diff --git a/cloud-infrastructure/modules/role-assignments-storage-blob-data-reader.bicep b/cloud-infrastructure/modules/role-assignments-storage-blob-data-reader.bicep index 604fc08c02..3f26fcb80f 100644 --- a/cloud-infrastructure/modules/role-assignments-storage-blob-data-reader.bicep +++ b/cloud-infrastructure/modules/role-assignments-storage-blob-data-reader.bicep @@ -2,12 +2,12 @@ param storageAccountName string param userAssignedIdentityName string = '' param principalId string = '' -resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (userAssignedIdentityName != '') { +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = if (userAssignedIdentityName != '') { scope: resourceGroup() name: userAssignedIdentityName ?? principalId } -resource existingStorageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { +resource existingStorageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' existing = { scope: resourceGroup() name: storageAccountName } @@ -16,6 +16,7 @@ var storageBlobDataReaderRoleDefinitionId = '2a2b9908-6ea1-4ae2-8e65-a410df84e7d resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(existingStorageAccount.id, userAssignedIdentityName, storageBlobDataReaderRoleDefinitionId) properties: { + #disable-next-line BCP318 principalId: userAssignedIdentityName == '' ? principalId : userAssignedIdentity.properties.principalId principalType: 'ServicePrincipal' roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataReaderRoleDefinitionId) diff --git a/cloud-infrastructure/modules/storage-account.bicep b/cloud-infrastructure/modules/storage-account.bicep index a55c50f161..06a08f3f2e 100644 --- a/cloud-infrastructure/modules/storage-account.bicep +++ b/cloud-infrastructure/modules/storage-account.bicep @@ -11,7 +11,7 @@ param containers array = [ } ] -resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { +resource storageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' = { name: name location: location tags: tags @@ -46,7 +46,7 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { } } -resource blobContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = [ +resource blobContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-06-01' = [ for container in containers: { name: '${name}/default/${container.name}' properties: { diff --git a/cloud-infrastructure/modules/user-assigned-managed-identity.bicep b/cloud-infrastructure/modules/user-assigned-managed-identity.bicep index 8708c2a020..116979fce4 100644 --- a/cloud-infrastructure/modules/user-assigned-managed-identity.bicep +++ b/cloud-infrastructure/modules/user-assigned-managed-identity.bicep @@ -2,11 +2,11 @@ param name string param location string param tags object param containerRegistryName string -param environmentResourceGroupName string +param globalResourceGroupName string param keyVaultName string param grantKeyVaultWritePermissions bool = false -resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: name location: location tags: tags @@ -14,7 +14,7 @@ resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@ module containerRegistryPermission './role-assignments-container-registry-acr-pull.bicep' = { name: '${name}-permission' - scope: resourceGroup(subscription().subscriptionId, environmentResourceGroupName) + scope: resourceGroup(subscription().subscriptionId, globalResourceGroupName) params: { containerRegistryName: containerRegistryName principalId: userAssignedIdentity.properties.principalId diff --git a/cloud-infrastructure/modules/virtual-network.bicep b/cloud-infrastructure/modules/virtual-network.bicep index 3630770dee..13740def21 100644 --- a/cloud-infrastructure/modules/virtual-network.bicep +++ b/cloud-infrastructure/modules/virtual-network.bicep @@ -3,7 +3,7 @@ param location string param tags object param address string -resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = { +resource virtualNetwork 'Microsoft.Network/virtualNetworks@2025-01-01' = { name: name location: location tags: tags