diff --git a/.github/workflows/_deploy-container.yml b/.github/workflows/_deploy-container.yml index fe42a403c..5dcd08e9c 100644 --- a/.github/workflows/_deploy-container.yml +++ b/.github/workflows/_deploy-container.yml @@ -20,7 +20,7 @@ jobs: uses: azure/login@v1 with: tenant-id: ${{ secrets.AZURE_TENANT_ID }} - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Deploy Staging West Europe cluster @@ -39,7 +39,7 @@ jobs: uses: azure/login@v1 with: tenant-id: ${{ secrets.AZURE_TENANT_ID }} - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Deploy Production West Europe cluster diff --git a/.github/workflows/_publish-container.yml b/.github/workflows/_publish-container.yml index 4bbd50d4e..e6a5309dc 100644 --- a/.github/workflows/_publish-container.yml +++ b/.github/workflows/_publish-container.yml @@ -26,7 +26,7 @@ jobs: uses: azure/login@v1 with: tenant-id: ${{ secrets.AZURE_TENANT_ID }} - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_ACR }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Login to ACR diff --git a/.github/workflows/cloud-infrastructure.yml b/.github/workflows/cloud-infrastructure.yml index 3120e15a8..e5eff90f2 100644 --- a/.github/workflows/cloud-infrastructure.yml +++ b/.github/workflows/cloud-infrastructure.yml @@ -37,7 +37,7 @@ jobs: - name: Login to Azure subscription uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -66,7 +66,7 @@ jobs: - name: Login to Azure subscription uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -92,7 +92,7 @@ jobs: - name: Login to Azure subscription uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -126,7 +126,7 @@ jobs: - name: Login to Azure subscription uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -160,7 +160,7 @@ jobs: - name: Login to Azure subscription uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -175,7 +175,7 @@ jobs: - name: Refresh Azure tokens ## The previous step may take a while, so we refresh the token to avoid timeouts uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -205,7 +205,7 @@ jobs: - name: Login to Azure subscription uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -239,7 +239,7 @@ jobs: - name: Login to Azure subscription uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -273,7 +273,7 @@ jobs: - name: Login to Azure subscription uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -288,7 +288,7 @@ jobs: - name: Refresh Azure tokens ## The previous step may take a while, so we refresh the token to avoid timeouts uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE }} + client-id: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} diff --git a/cloud-infrastructure/cluster/deploy-cluster.sh b/cloud-infrastructure/cluster/deploy-cluster.sh index ed7aec9ca..8110767e3 100755 --- a/cloud-infrastructure/cluster/deploy-cluster.sh +++ b/cloud-infrastructure/cluster/deploy-cluster.sh @@ -20,26 +20,81 @@ if [[ $ENVIRONMENT_VARIABLES_MISSING == true ]]; then echo "Please follow the instructions in the README.md for setting up the required environment variables and try again." exit 1 else - echo "$(date +"%Y-%m-%dT%H:%M:%S") All environment variables are set." + echo "$(date +"%Y-%m-%dT%H:%M:%S") All environment variables are set." fi -RESOURCE_GROUP_NAME="$ENVIRONMENT-$LOCATION_PREFIX" -DEPLOYMENT_COMMAND="az deployment sub create" -CURRENT_DATE=$(date +'%Y-%m-%dT%H-%M') - get_active_version() { local image=$(az containerapp revision list --name $1 --resource-group $RESOURCE_GROUP_NAME --query "[0].properties.template.containers[0].image" --output tsv 2>/dev/null) [ -z "$image" ] && echo "latest" || echo ${image##*:} } +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"* ]]; then + echo "false" + else + local result=$(echo "$app_details" | jq -r '.properties.configuration.ingress.customDomains') + [[ "$result" != "null" ]] && echo "true" || echo "false" + fi +} + +RESOURCE_GROUP_NAME="$ENVIRONMENT-$LOCATION_PREFIX" ACTIVE_ACCOUNT_MANAGEMENT_API=$(get_active_version account-management-api) +ACCOUNT_MANAGEMENT_DOMAIN_CONFIGURED=$(is_domain_configured "account-management-api" "$RESOURCE_GROUP_NAME") -DEPLOYMENT_PARAMETERS="-l $LOCATION -n $CURRENT_DATE-$RESOURCE_GROUP_NAME --output json -f ./main-cluster.bicep -p environment=$ENVIRONMENT locationPrefix=$LOCATION_PREFIX resourceGroupName=$RESOURCE_GROUP_NAME clusterUniqueName=$CLUSTER_UNIQUE_NAME useMssqlElasticPool=$USE_MSSQL_ELASTIC_POOL containerRegistryName=$CONTAINER_REGISTRY_NAME sqlAdminObjectId=$ACTIVE_DIRECTORY_SQL_ADMIN_OBJECT_ID accountManagementApiVersion=$ACTIVE_ACCOUNT_MANAGEMENT_API" +DEPLOYMENT_COMMAND="az deployment sub create" +CURRENT_DATE=$(date +'%Y-%m-%dT%H-%M') +DEPLOYMENT_PARAMETERS="-l $LOCATION -n $CURRENT_DATE-$RESOURCE_GROUP_NAME --output json -f ./main-cluster.bicep -p environment=$ENVIRONMENT locationPrefix=$LOCATION_PREFIX resourceGroupName=$RESOURCE_GROUP_NAME clusterUniqueName=$CLUSTER_UNIQUE_NAME useMssqlElasticPool=$USE_MSSQL_ELASTIC_POOL containerRegistryName=$CONTAINER_REGISTRY_NAME domainName=$DOMAIN_NAME sqlAdminObjectId=$ACTIVE_DIRECTORY_SQL_ADMIN_OBJECT_ID accountManagementApiVersion=$ACTIVE_ACCOUNT_MANAGEMENT_API accountManagementDomainConfigured=$ACCOUNT_MANAGEMENT_DOMAIN_CONFIGURED" cd "$(dirname "${BASH_SOURCE[0]}")" . ../deploy.sh -ACCOUNT_MANAGEMENT_IDENTITY_CLIENT_ID=$(echo "$output" | jq -r '.properties.outputs.accountManagementIdentityClientId.value') -if [[ -n "$GITHUB_OUTPUT" ]]; then +# 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. +if [[ "$*" == *"--apply"* ]] +then + RED='\033[0;31m' + RESET='\033[0m' # Reset formatting + + # Check for the specific error message indicating that DNS Records are missing + if [[ $output == *"InvalidCustomHostNameValidation"* ]]; 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 "$LOCATION_PREFIX-container-apps-environment" --resource-group "$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') + + # Display instructions for setting up DNS entries + echo -e "${RED}$(date +"%Y-%m-%dT%H:%M:%S") Please add the following DNS entries to $DOMAIN_NAME, and then retry:${RESET}" + echo -e "${RED}- A TXT record with the name 'asuid.account-management-api' and the value '$custom_domain_verification_id'.${RESET}" + echo -e "${RED}- A CNAME record with the Host name 'account-management-api' that points to address 'account-management-api.$default_domain'.${RESET}" + exit 1 + elif [[ $output == "ERROR:"* ]]; then + echo -e "${RED}$output${RESET}" + 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 [[ "$ACCOUNT_MANAGEMENT_DOMAIN_CONFIGURED" == "false" ]] && [[ "$DOMAIN_NAME" != "" ]]; then + echo "Running deployment again to finalize setting up SSL certificate for account-management-api" + ACCOUNT_MANAGEMENT_DOMAIN_CONFIGURED=true + DEPLOYMENT_PARAMETERS="-l $LOCATION -n $CURRENT_DATE-$RESOURCE_GROUP_NAME --output json -f ./main-cluster.bicep -p environment=$ENVIRONMENT locationPrefix=$LOCATION_PREFIX resourceGroupName=$RESOURCE_GROUP_NAME clusterUniqueName=$CLUSTER_UNIQUE_NAME useMssqlElasticPool=$USE_MSSQL_ELASTIC_POOL containerRegistryName=$CONTAINER_REGISTRY_NAME domainName=$DOMAIN_NAME sqlAdminObjectId=$ACTIVE_DIRECTORY_SQL_ADMIN_OBJECT_ID accountManagementApiVersion=$ACTIVE_ACCOUNT_MANAGEMENT_API accountManagementDomainConfigured=$ACCOUNT_MANAGEMENT_DOMAIN_CONFIGURED" + + . ../deploy.sh + + if [[ $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 "$output" | jq -r '.properties.outputs.accountManagementIdentityClientId.value') + if [[ -n "$GITHUB_OUTPUT" ]]; then echo "ACCOUNT_MANAGEMENT_IDENTITY_CLIENT_ID=$ACCOUNT_MANAGEMENT_IDENTITY_CLIENT_ID" >> $GITHUB_OUTPUT -fi \ No newline at end of file + fi +fi diff --git a/cloud-infrastructure/cluster/main-cluster.bicep b/cloud-infrastructure/cluster/main-cluster.bicep index ad96d9c5f..f3da40c57 100644 --- a/cloud-infrastructure/cluster/main-cluster.bicep +++ b/cloud-infrastructure/cluster/main-cluster.bicep @@ -8,7 +8,9 @@ param useMssqlElasticPool bool param containerRegistryName string param location string = deployment().location param sqlAdminObjectId string +param domainName string param accountManagementApiVersion string +param accountManagementDomainConfigured bool var tags = { environment: environment, 'managed-by': 'bicep' } var diagnosticStorageAccountName = '${clusterUniqueName}diagnostic' @@ -167,6 +169,7 @@ module accountManagementApi '../modules/container-app.bicep' = { tags: tags resourceGroupName: resourceGroupName environmentId: contaionerAppsEnvironment.outputs.environmentId + environmentName: contaionerAppsEnvironment.outputs.name containerRegistryName: containerRegistryName containerImageName: 'account-management-api' containerImageTag: accountManagementApiVersion @@ -175,6 +178,8 @@ module accountManagementApi '../modules/container-app.bicep' = { sqlServerName: clusterUniqueName sqlDatabaseName: 'account-management' userAssignedIdentityName: 'account-management-${resourceGroupName}' + domainName: domainName == '' ? '' : 'account-management-api.${domainName}' + accountManagementDomainConfigured: domainName != '' && accountManagementDomainConfigured } dependsOn: [accountManagementDatabase] } diff --git a/cloud-infrastructure/deploy.sh b/cloud-infrastructure/deploy.sh index 691bdad37..481da3288 100755 --- a/cloud-infrastructure/deploy.sh +++ b/cloud-infrastructure/deploy.sh @@ -19,9 +19,5 @@ fi if [[ "$*" == *"--apply"* ]] then echo "$(date +"%Y-%m-%dT%H:%M:%S") Applying changes..." - export output=$($DEPLOYMENT_COMMAND $DEPLOYMENT_PARAMETERS) - if [[ $? -ne 0 ]]; then - echo "::error::Deployment failed." - exit 1 - fi + export output=$($DEPLOYMENT_COMMAND $DEPLOYMENT_PARAMETERS | tee /dev/tty) fi diff --git a/cloud-infrastructure/environment/deploy-environment.sh b/cloud-infrastructure/environment/deploy-environment.sh index c1566ad35..f2080e042 100755 --- a/cloud-infrastructure/environment/deploy-environment.sh +++ b/cloud-infrastructure/environment/deploy-environment.sh @@ -6,3 +6,8 @@ DEPLOYMENT_PARAMETERS="-l $LOCATION -n "$CURRENT_DATE-$ENVIRONMENT" --output tab cd "$(dirname "${BASH_SOURCE[0]}")" . ../deploy.sh + +if [[ $output == "ERROR:"* ]]; then + echo -e "${RED}$output" + exit 1 +fi diff --git a/cloud-infrastructure/initialize-azure.sh b/cloud-infrastructure/initialize-azure.sh index 3c066a849..70bf52527 100644 --- a/cloud-infrastructure/initialize-azure.sh +++ b/cloud-infrastructure/initialize-azure.sh @@ -2,30 +2,30 @@ RED='\033[0;31m' YELLOW='\033[0;33m' GREEN='\033[0;32m' -NC='\033[0m' # No Color +RESET='\033[0m' # Reset formatting BOLD='\033[1m' NO_BOLD='\033[22m' -SEPARATOR="${BOLD}---------------------------------------------------------------------------${NC}" +SEPARATOR="${BOLD}---------------------------------------------------------------------------${RESET}" -if [ -z "$BASH_VERSION" ]; then +if [[ -z "$BASH_VERSION" ]]; then echo "" - echo -e "${RED}This script must be run in Bash. Please run ${BOLD}'bash ./initialize-azure.sh'.${NC}" + echo -e "${RED}This script must be run in Bash. Please run ${BOLD}'bash ./initialize-azure.sh'.${RESET}" return fi echo -e "${SEPARATOR}" -echo -e "${BOLD}Logging in to Azure${NC}" +echo -e "${BOLD}Logging in to Azure${RESET}" echo -e "${SEPARATOR}" sleep 1 az login || exit 1 tenantId=$(az account show --query 'tenantId' -o tsv) || exit 1 -echo -e "${GREEN}Successfully logged in to Azure on Tenant ID: $tenantId.${NC}" +echo -e "${GREEN}Successfully logged in to Azure on Tenant ID: $tenantId.${RESET}" echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Prompting for Azure Subscription${NC}" +echo -e "${BOLD}Prompting for Azure Subscription${RESET}" echo -e "${SEPARATOR}" echo "Enter your Azure Subscription ID (find it in the list above, look for the 'id' property):" @@ -33,16 +33,16 @@ read subscriptionId az account set --subscription $subscriptionId || exit 1 az account show --query 'id' -o tsv | grep -q $subscriptionId || exit 1 -if [ "$tenantId" != $(az account show --query 'tenantId' -o tsv) ]; then - echo -e "${RED}The Azure Subscription ID: '$subscriptionId' is not on the Tenant with ID '$tenantId'. Did you select the 'id' property?.${NC}" +if [[ "$tenantId" != $(az account show --query 'tenantId' -o tsv) ]]; then + echo -e "${RED}The Azure Subscription ID: '$subscriptionId' is not on the Tenant with ID '$tenantId'. Did you select the 'id' property?.${RESET}" exit 1 fi -echo -e "${GREEN}Successfully set subscription to $subscriptionId on Tenant ID: $tenantId.${NC}" +echo -e "${GREEN}Successfully set subscription to $subscriptionId on Tenant ID: $tenantId.${RESET}" echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Prompting GitHub Repository${NC}" +echo -e "${BOLD}Prompting GitHub Repository${RESET}" echo -e "${SEPARATOR}" echo "Enter your GitHub repository URL (e.g., https://github.com//):" @@ -58,41 +58,41 @@ if [[ $gitHubRepositoryUrl =~ ^https://github\.com/([a-zA-Z0-9_-]+)/([a-zA-Z0-9_ fi gitHubRepositoryPath="$gitHubOrganization/$gitHubRepositoryName" else - echo -e "${RED}Invalid GitHub URL. Please use the format: https://github.com//.${NC}" + echo -e "${RED}Invalid GitHub URL. Please use the format: https://github.com//.${RESET}" exit 1 fi -echo -e "${GREEN}Successfully extracted GitHub Organization and Repository: $gitHubRepositoryPath.${NC}" +echo -e "${GREEN}Successfully extracted GitHub Organization and Repository: $gitHubRepositoryPath.${RESET}" echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Ensure 'Microsoft.ContainerService' service provider is registered on Azure Subscription${NC}" +echo -e "${BOLD}Ensure 'Microsoft.ContainerService' service provider is registered on Azure Subscription${RESET}" echo -e "${SEPARATOR}" az provider register --namespace Microsoft.ContainerService -echo -e "${GREEN}Successfully registered the 'Microsoft.ContainerService' on Subscription '$subscriptionId'.${NC}" +echo -e "${GREEN}Successfully registered the 'Microsoft.ContainerService' on Subscription '$subscriptionId'.${RESET}" echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Configuring Azure AD Service Principal for Infrastructure${NC}" +echo -e "${BOLD}Configuring Azure AD Service Principal for passwordless deployments using OpenID Connect and federated credentials${RESET}" echo -e "${SEPARATOR}" -infrastructureServicePrincipalDisplayName="GitHub Azure Infrastructure - $gitHubOrganization - $gitHubRepositoryName" -servicePrincipalAppIdInfrastructure=$(az ad sp list --display-name "$infrastructureServicePrincipalDisplayName" --query "[].appId" -o tsv) || exit 1 -if [ -n "$servicePrincipalAppIdInfrastructure" ]; then - echo -e "${YELLOW}The Service Principal (App registration) '$infrastructureServicePrincipalDisplayName' already exists with App ID: $servicePrincipalAppIdInfrastructure.${NC}" +servicePrincipalDisplayName="GitHub Azure - $gitHubOrganization - $gitHubRepositoryName" +servicePrincipalAppId=$(az ad sp list --display-name "$servicePrincipalDisplayName" --query "[].appId" -o tsv) || exit 1 +if [[ -n "$servicePrincipalAppId" ]]; then + echo -e "${YELLOW}The Service Principal (App registration) '$servicePrincipalDisplayName' already exists with App ID: $servicePrincipalAppId.${RESET}" echo "Would you like to continue using this Service Principal? (y/n)" - read userChoiceForReuseServicePrincipalfrastructure + read reuseServicePrincipal - if [ "$userChoiceForReuseServicePrincipalfrastructure" != "y" ]; then - echo -e "${RED}Please delete the existing Service Principal and run this script again.${NC}" + if [[ "$reuseServicePrincipal" != "y" ]]; then + echo -e "${RED}Please delete the existing Service Principal and run this script again.${RESET}" exit 1 fi else - servicePrincipalAppIdInfrastructure=$(az ad app create --display-name "$infrastructureServicePrincipalDisplayName" --query 'appId' -o tsv) || exit 1 - az ad sp create --id $servicePrincipalAppIdInfrastructure || exit 1 + servicePrincipalAppId=$(az ad app create --display-name "$servicePrincipalDisplayName" --query 'appId' -o tsv) || exit 1 + az ad sp create --id $servicePrincipalAppId || exit 1 fi mainCredential=$(echo -n "{ @@ -125,126 +125,76 @@ productionEnvironmentCredentials=$(echo -n "{ \"subject\": \"repo:$gitHubRepositoryPath:environment:production\", \"audiences\": [\"api://AzureADTokenExchange\"] }") -if [ "$userChoiceForReuseServicePrincipalfrastructure" == "y" ]; then - echo -e "${YELLOW}You are reusing the Service Principal. Please ignore the error: 'FederatedIdentityCredential with name xxxx already exists'.${NC}" + +echo $mainCredential | az ad app federated-credential create --id $servicePrincipalAppId --parameters @- +echo $pullRequestCredential | az ad app federated-credential create --id $servicePrincipalAppId --parameters @- +echo $sharedEnvironmentCredentials | az ad app federated-credential create --id $servicePrincipalAppId --parameters @- +echo $stagingEnvironmentCredentials | az ad app federated-credential create --id $servicePrincipalAppId --parameters @- +echo $productionEnvironmentCredentials | az ad app federated-credential create --id $servicePrincipalAppId --parameters @- +if [[ "$reuseServicePrincipal" == "y" ]]; then + echo -e "${YELLOW}Please ignore the errors: 'FederatedIdentityCredential with name xxxx already exists'.${RESET}" fi -echo $mainCredential | az ad app federated-credential create --id $servicePrincipalAppIdInfrastructure --parameters @- -echo $pullRequestCredential | az ad app federated-credential create --id $servicePrincipalAppIdInfrastructure --parameters @- -echo $sharedEnvironmentCredentials | az ad app federated-credential create --id $servicePrincipalAppIdInfrastructure --parameters @- -echo $stagingEnvironmentCredentials | az ad app federated-credential create --id $servicePrincipalAppIdInfrastructure --parameters @- -echo $productionEnvironmentCredentials | az ad app federated-credential create --id $servicePrincipalAppIdInfrastructure --parameters @- -echo -e "${GREEN}Successfully configured Service Principal with Federated Credentials.${NC}" +echo -e "${GREEN}Successfully configured Service Principal with Federated Credentials.${RESET}" echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Grant subscription level 'Contributor' and 'User Access Administrator' role to the Infrastructure Service Principal${NC}" +echo -e "${BOLD}Grant subscription level 'Contributor' and 'User Access Administrator' role to the Infrastructure Service Principal${RESET}" echo -e "${SEPARATOR}" -az role assignment create --assignee $servicePrincipalAppIdInfrastructure --role Contributor --scope "/subscriptions/$subscriptionId" || exit 1 -az role assignment create --assignee $servicePrincipalAppIdInfrastructure --role "User Access Administrator" --scope "/subscriptions/$subscriptionId" || exit 1 +az role assignment create --assignee $servicePrincipalAppId --role "Contributor" --scope "/subscriptions/$subscriptionId" || exit 1 +az role assignment create --assignee $servicePrincipalAppId --role "User Access Administrator" --scope "/subscriptions/$subscriptionId" || exit 1 +az role assignment create --assignee $servicePrincipalAppId --role "AcrPush" --scope "/subscriptions/$subscriptionId" || exit 1 -echo -e "${GREEN}Successfully granted the Service Principal '$infrastructureServicePrincipalDisplayName' 'Contributor' rights to the Azure Subscription $subscriptionId.${NC}" +echo -e "${GREEN}Successfully granted the Service Principal '$servicePrincipalDisplayName' 'Contributor' rights to the Azure Subscription $subscriptionId.${RESET}" echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Configuring Azure AD Security Group for Infrastructure operations${NC}" +echo -e "${BOLD}Configuring Azure AD 'Azure SQL Server Admins' Security Group${RESET}" echo -e "${SEPARATOR}" azureSqlServerAdmins="Azure SQL Server Admins" sqlServerAdminsGroupId=$(az ad group list --filter "displayname eq '$azureSqlServerAdmins'" --query "[].id" -o tsv) || exit 1 -if [ -n "$sqlServerAdminsGroupId" ]; then - echo -e "${YELLOW}The Azure AD Group '$azureSqlServerAdmins' already exists with Group ID: $sqlServerAdminsGroupId.${NC}" +if [[ -n "$sqlServerAdminsGroupId" ]]; then + echo -e "${YELLOW}The Azure AD Group '$azureSqlServerAdmins' already exists with Group ID: $sqlServerAdminsGroupId.${RESET}" echo "Would you like to continue using this group? (y/n)" - read userChoiceForReuseGroup + read reuseSQLServerAdminsSecurityGroup - if [ "$userChoiceForReuseGroup" != "y" ]; then - echo -e "${RED}Please delete the existing group and run this script again.${NC}" + if [[ "$reuseSQLServerAdminsSecurityGroup" != "y" ]]; then + echo -e "${RED}Please delete the existing group and run this script again.${RESET}" exit 1 fi else sqlServerAdminsGroupId=$(az ad group create --display-name "$azureSqlServerAdmins" --mail-nickname "AzureSQLServerAdmins" --query "id" -o tsv) || exit 1 fi -servicePrincipalObjectIdInfrastructure=$(az ad sp list --filter "appId eq '$servicePrincipalAppIdInfrastructure'" --query "[].id" -o tsv) || exit 1 -az ad group member add --group $sqlServerAdminsGroupId --member-id $servicePrincipalObjectIdInfrastructure || echo -e "${YELLOW}Please ignore member already exists error." +servicePrincipalObjectId=$(az ad sp list --filter "appId eq '$servicePrincipalAppId'" --query "[].id" -o tsv) || exit 1 +az ad group member add --group $sqlServerAdminsGroupId --member-id $servicePrincipalObjectId || echo -e "${YELLOW}Please ignore member already exists error." -echo -e "${GREEN}Successfully added '$infrastructureServicePrincipalDisplayName' to '$azureSqlServerAdmins' Security Group.${NC}" +echo -e "${GREEN}Successfully added '$servicePrincipalDisplayName' to '$azureSqlServerAdmins' Security Group.${RESET}" echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Configuring Azure AD Service Principal for Azure Container Registry (ACR)${NC}" +echo -e "${BOLD}Configure GitHub secrets and variables${RESET}" echo -e "${SEPARATOR}" -acrServicePrincipalDisplayName="GitHub Azure Container Registry - $gitHubOrganization - $gitHubRepositoryName" -servicePrincipalAppIdAcr=$(az ad sp list --display-name "$acrServicePrincipalDisplayName" --query "[].appId" -o tsv) || exit 1 -if [ -n "$servicePrincipalAppIdAcr" ]; then - echo -e "${YELLOW}The Service Principal (App registration) '$acrServicePrincipalDisplayName' already exists with App ID: $servicePrincipalAppIdAcr.${NC}" - - echo "Would you like to continue using this Service Principal? (y/n)" - read userChoiceForReuseServicePrincipalAcr - - if [ "$userChoiceForReuseServicePrincipalAcr" != "y" ]; then - echo -e "${RED}Please delete the existing Service Principal and run this script again.${NC}" - exit 1 - fi -else - servicePrincipalAppIdAcr=$(az ad app create --display-name "$acrServicePrincipalDisplayName" --query 'appId' -o tsv) || exit 1 - az ad sp create --id $servicePrincipalAppIdAcr || exit 1 -fi - -mainCredential=$(echo -n "{ - \"name\": \"MainBranch\", - \"issuer\": \"https://token.actions.githubusercontent.com\", - \"subject\": \"repo:$gitHubRepositoryPath:ref:refs/heads/main\", - \"audiences\": [\"api://AzureADTokenExchange\"] -}") -pullRequestCredential=$(echo -n "{ - \"name\": \"PullRequests\", - \"issuer\": \"https://token.actions.githubusercontent.com\", - \"subject\": \"repo:$gitHubRepositoryPath:pull_request\", - \"audiences\": [\"api://AzureADTokenExchange\"] -}") - -if [ "$userChoiceForReuseServicePrincipalAcr" == "y" ]; then - echo -e "${YELLOW}You are reusing the Service Principal. Please ignore the error: 'FederatedIdentityCredential with name MainBranch/PullRequests already exists'.${NC}" -fi -echo $mainCredential | az ad app federated-credential create --id $servicePrincipalAppIdAcr --parameters @- -echo $pullRequestCredential | az ad app federated-credential create --id $servicePrincipalAppIdAcr --parameters @- - -echo -e "${GREEN}Successfully configured Service Principal with Federated Credentials.${NC}" - -echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Assigning subscription level 'AcrPush' rights to the Service Principals${NC}" -echo -e "${SEPARATOR}" - -az role assignment create --assignee $servicePrincipalAppIdAcr --role AcrPush --scope "/subscriptions/$subscriptionId" || exit 1 - -echo -e "${GREEN}Successfully granted the Service Principal '$acrServicePrincipalDisplayName' 'AcrPush' rights to the Azure Subscription $subscriptionId.${NC}" - - -echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Configure GitHub secrets and variables${NC}" -echo -e "${SEPARATOR}" - -echo -e "The following GitHub repository ${BOLD}secrets${NO_BOLD} must be created here: $gitHubRepositoryUrl/settings/${BOLD}secrets${NO_BOLD}/actions" +echo -e "The following GitHub repository ${BOLD}secrets${NO_BOLD} must be created:" echo -e "- AZURE_TENANT_ID: $tenantId" echo -e "- AZURE_SUBSCRIPTION_ID: $subscriptionId" -echo -e "- AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE: $servicePrincipalAppIdInfrastructure" -echo -e "- AZURE_SERVICE_PRINCIPAL_ID_ACR: $servicePrincipalAppIdAcr" +echo -e "- AZURE_SERVICE_PRINCIPAL_ID: $servicePrincipalAppId" echo -e "- ACTIVE_DIRECTORY_SQL_ADMIN_OBJECT_ID: $sqlServerAdminsGroupId" echo -e "\n" -echo -e "The following GitHub repository ${BOLD}variables${NO_BOLD} must be created here: $gitHubRepositoryUrl/settings/${BOLD}variables${NO_BOLD}/actions" +echo -e "The following GitHub repository ${BOLD}variables${NO_BOLD} must be created:" echo -e "- CONTAINER_REGISTRY_NAME: " echo -e "- UNIQUE_CLUSTER_PREFIX: /dev/null 2>&1 && echo "true" || echo "false") -if [ "$isGitHubCLIInstalled" == "true" ]; then +if [[ "$isGitHubCLIInstalled" == "true" ]]; then echo "Would you like to do this using GitHub CLI? (y/n)" read userChoiceForSecretCreation - if [ "$userChoiceForSecretCreation" == "y" ]; then + if [[ "$userChoiceForSecretCreation" == "y" ]]; then echo "Enter your Azure Container Registry (ACR) name (leave blank to use exiting value):" read acrName @@ -254,30 +204,39 @@ if [ "$isGitHubCLIInstalled" == "true" ]; then gh auth login --git-protocol https --web || exit 1 gh secret set AZURE_TENANT_ID -b"$tenantId" --repo=$gitHubRepositoryPath || exit 1 gh secret set AZURE_SUBSCRIPTION_ID -b"$subscriptionId" --repo=$gitHubRepositoryPath || exit 1 - gh secret set AZURE_SERVICE_PRINCIPAL_ID_INFRASTRUCTURE -b"$servicePrincipalAppIdInfrastructure" --repo=$gitHubRepositoryPath || exit 1 - gh secret set AZURE_SERVICE_PRINCIPAL_ID_ACR -b"$servicePrincipalAppIdAcr" --repo=$gitHubRepositoryPath || exit 1 + gh secret set AZURE_SERVICE_PRINCIPAL_ID -b"$servicePrincipalAppId" --repo=$gitHubRepositoryPath || exit 1 gh secret set ACTIVE_DIRECTORY_SQL_ADMIN_OBJECT_ID -b"$sqlServerAdminsGroupId" --repo=$gitHubRepositoryPath || exit 1 - if [ -n "$acrName" ]; then + if [[ -n "$acrName" ]]; then gh variable set CONTAINER_REGISTRY_NAME -b"$acrName" --repo=$gitHubRepositoryPath || exit 1 fi - if [ -n "$clusterPrefix" ]; then + if [[ -n "$clusterPrefix" ]]; then gh variable set UNIQUE_CLUSTER_PREFIX -b"$clusterPrefix" --repo=$gitHubRepositoryPath || exit 1 fi - echo -e "${GREEN}Successfully created secrets in GitHub.${NC}" - else - echo -e "\n${YELLOW}Please manually create the secrets and variables.${NC}" + echo -e "${GREEN}Successfully created secrets in GitHub.${RESET}" fi -else - echo -e "\n${YELLOW}GitHub CLI is not installed. Please manually create the secrets and variables.${NC}" fi echo -e "\n${SEPARATOR}" -echo -e "${BOLD}Setup completed${NC}" +echo -e "${BOLD}Setup completed${RESET}" echo -e "${SEPARATOR}" -echo -e "\n${YELLOW}Please manually set up SonarCloud to enable static code analysis. Alternativly disable the test-with-code-coverage job in the application.yml workflow.${NC}" + +if [[ "$isGitHubCLIInstalled" != "true" ]] || [[ "$userChoiceForSecretCreation" != "y" ]]; then + echo -e "\n${YELLOW}Please manually create the GitHub repository secrets and varibles:${RESET}" + echo -e "- Create secrets here: $gitHubRepositoryUrl/settings/${BOLD}secrets${NO_BOLD}/actions" + echo -e "- Create varibles here: $gitHubRepositoryUrl/settings/${BOLD}variables${NO_BOLD}/actions" +fi + +echo -e "\n${YELLOW}To finalize setting configuration of GitHub, please follow these instructions to setup environments:${RESET}" +echo -e "- Navigate to: $gitHubRepositoryUrl/settings/environments" +echo -e "- Create three environments named: ${BOLD}production${NO_BOLD}, ${BOLD}staging${NO_BOLD}, and ${BOLD}shared${NO_BOLD}." +echo -e "- For the ${BOLD}production${NO_BOLD} and ${BOLD}staging${NO_BOLD} environments, optionally create an environment variable named ${BOLD}DOMAIN_NAME${NO_BOLD} to set up a Custom Domain name and SSL Certificate." +echo -e "- It's also recommended to set up 'Required reviewers' and 'Branch protection rules' for each environment to ensure only code from the main branch are deployed." + + +echo -e "\n${YELLOW}Please manually set up SonarCloud to enable static code analysis. Alternativly disable the test-with-code-coverage job in the application.yml workflow.${RESET}" echo -e "- Sign up for a SonarCloud account here: https://sonarcloud.io. Use your GitHub account for authentication." echo -e "- Set up the following GitHub repository variables here: $gitHubRepositoryUrl/settings/${BOLD}variables${NO_BOLD}/actions:" echo -e " - SONAR_ORGANIZATION" @@ -285,11 +244,11 @@ echo -e " - SONAR_PROJECT_KEY" echo -e "- Set up the following GitHub repository secret here: $gitHubRepositoryUrl/settings/${BOLD}secrets${NO_BOLD}/actions:" echo -e " - SONAR_TOKEN to the token generated in SonarCloud." -echo -e "\n${BOLD}${GREEN}You are now ready to run these GitHub Action workflows from the main branch:${NC}" -echo -e "${GREEN}- 'Cloud Infrastructure - Deployment': cloud-infrastructure.yml${NC}" -echo -e "${GREEN}- 'Application - Build and Deploy': application.yml${NC}" +echo -e "\n${BOLD}${GREEN}You are now ready to run these GitHub Action workflows from the main branch:${RESET}" +echo -e "${GREEN}- 'Cloud Infrastructure - Deployment': cloud-infrastructure.yml${RESET}" +echo -e "${GREEN}- 'Application - Build and Deploy': application.yml${RESET}" -echo -e "\n${YELLOW}${BOLD}TIP:${NO_BOLD} First run the 'Shared' step of the Infrastructure to deploy the Azure Container Registry (ACR).${NC}" -echo -e "${YELLOW}Then run the 'Build and Test' to push an image to it, before deploying the rest of the infrastructure.${NC}" +echo -e "\n${YELLOW}${BOLD}TIP:${NO_BOLD} First run the 'Shared' step of the Infrastructure to deploy the Azure Container Registry (ACR).${RESET}" +echo -e "${YELLOW}Then run the 'Build and Test' to push an image to it, before deploying the rest of the infrastructure.${RESET}" exit 0 diff --git a/cloud-infrastructure/modules/container-app.bicep b/cloud-infrastructure/modules/container-app.bicep index eb7394ff6..e613a2527 100644 --- a/cloud-infrastructure/modules/container-app.bicep +++ b/cloud-infrastructure/modules/container-app.bicep @@ -3,6 +3,7 @@ param location string param tags object param resourceGroupName string param environmentId string +param environmentName string param containerRegistryName string param containerImageName string param containerImageTag string @@ -11,6 +12,8 @@ param memory string = '0.5Gi' param sqlServerName string param sqlDatabaseName string param userAssignedIdentityName string +param domainName string +param accountManagementDomainConfigured bool resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { scope: resourceGroup(resourceGroupName) @@ -27,6 +30,44 @@ module containerRegistryPermission './container-registry-permission.bicep' = { } } +var certificateName = '${domainName}-certificate' +var isCustomDomainSet = domainName != '' + +module newManagedCertificate './managed-certificate.bicep' = + if (isCustomDomainSet) { + name: certificateName + scope: resourceGroup(resourceGroupName) + dependsOn: [containerApp] + params: { + name: certificateName + location: location + tags: tags + environmentName: environmentName + domainName: domainName + } + } + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = + if (isCustomDomainSet) { + name: environmentName + } + +resource existingManagedCertificate 'Microsoft.App/managedEnvironments/managedCertificates@2023-05-01' existing = + if (isCustomDomainSet) { + name: certificateName + parent: containerAppsEnvironment + } + +var customDomainConfiguration = isCustomDomainSet + ? [ + { + bindingType: accountManagementDomainConfigured ? 'SniEnabled' : 'Disabled' + name: domainName + certificateId: accountManagementDomainConfigured ? existingManagedCertificate.id : null + } + ] + : [] + var containerRegistryServerUrl = '${containerRegistryName}.azurecr.io' resource containerApp 'Microsoft.App/containerApps@2023-04-01-preview' = { name: name @@ -65,7 +106,7 @@ resource containerApp 'Microsoft.App/containerApps@2023-04-01-preview' = { ] } ] - revisionSuffix: replace(containerImageTag, '.', '-') + revisionSuffix: replace(containerImageTag, '.', '-') scale: { minReplicas: 0 } @@ -88,6 +129,7 @@ resource containerApp 'Microsoft.App/containerApps@2023-04-01-preview' = { weight: 100 } ] + customDomains: customDomainConfiguration stickySessions: null } } diff --git a/cloud-infrastructure/modules/container-apps-environment.bicep b/cloud-infrastructure/modules/container-apps-environment.bicep index cb0a73f37..9701a0b85 100644 --- a/cloud-infrastructure/modules/container-apps-environment.bicep +++ b/cloud-infrastructure/modules/container-apps-environment.bicep @@ -28,3 +28,4 @@ resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' } output environmentId string = containerAppsEnvironment.id +output name string = containerAppsEnvironment.name diff --git a/cloud-infrastructure/modules/managed-certificate.bicep b/cloud-infrastructure/modules/managed-certificate.bicep new file mode 100644 index 000000000..86ecd3598 --- /dev/null +++ b/cloud-infrastructure/modules/managed-certificate.bicep @@ -0,0 +1,22 @@ +param name string +param location string +param tags object +param environmentName string +param domainName string + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { + name: environmentName +} + +resource managedCertificate 'Microsoft.App/managedEnvironments/managedCertificates@2023-05-01' = { + name: name + parent: containerAppsEnvironment + location: location + tags: tags + properties: { + subjectName: domainName + domainControlValidation: 'CNAME' + } +} + +output certificateId string = managedCertificate.id diff --git a/cloud-infrastructure/shared/deploy-shared.sh b/cloud-infrastructure/shared/deploy-shared.sh index 8e92686f8..23270468a 100755 --- a/cloud-infrastructure/shared/deploy-shared.sh +++ b/cloud-infrastructure/shared/deploy-shared.sh @@ -19,3 +19,8 @@ DEPLOYMENT_PARAMETERS="-l $LOCATION -n "$CURRENT_DATE-$RESOURCE_GROUP_NAME" --ou cd "$(dirname "${BASH_SOURCE[0]}")" . ../deploy.sh + +if [[ $output == "ERROR:"* ]]; then + echo -e "${RED}$output" + exit 1 +fi