Skip to content

Commit

Permalink
Add infrastructure automation to configure custom domain and SSL cert…
Browse files Browse the repository at this point in the history
…ificates (#196)

### Summary & Motivation

Introduce Bicep code to automate the configuration of a Custom Domain
and the automatic creation of a valid SSL Certificate for the
`account-management-api`. This process unfolds in three distinct steps:

1. Deploy the container apps environment to retrieve the auto-generated
environment URL and the Custom Domain Verification Id.
2. Deploy the container app again to set up the Domain and SSL
Certificate, albeit without binding them (as this is not supported in
one step; see
https://github.com/microsoft/azure-container-apps/tree/main/docs/templates/bicep/managedCertificates).
3. Proceed to bind the SSL certificate to the domain.

The first step will fail when initially setting up custom domains. Bash
scripts have been crafted to extract the container app url and Domain
Verification ID, providing clear instructions on how to configure CNAME
and TXT records to validate domain ownership. Moreover, the procedures
for the second and third steps will be executed automatically if needed,
streamlining the overall process.

Bash scripts responsible for deploying Bicep code have been updated with
enhanced error management.

Update the `initialize-azure.sh` Bash script, incorporating guidelines
on establishing GitHub environments and setting up the `DOMAIN_NAME`
variables.

Simplify the overall deployment structure from GitHub to Azure by
adopting a single shared Service Principal. This unified approach caters
to the deployment of Bicep infrastructure, the push of container images
to ACR, and the deployment of these images across various environments.


### Checklist

- [x] I have added a Label to the pull-request
- [x] I have added tests, and done manual regression tests
- [x] I have updated the documentation, if necessary
  • Loading branch information
tjementum authored Nov 6, 2023
2 parents 5a73307 + 3f6f48a commit 2a21a67
Show file tree
Hide file tree
Showing 12 changed files with 238 additions and 148 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/_deploy-container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/_publish-container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/cloud-infrastructure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down Expand Up @@ -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 }}

Expand All @@ -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 }}

Expand Down Expand Up @@ -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 }}

Expand Down Expand Up @@ -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 }}

Expand All @@ -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 }}

Expand Down Expand Up @@ -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 }}

Expand Down Expand Up @@ -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 }}

Expand Down Expand Up @@ -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 }}

Expand All @@ -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 }}

Expand Down
73 changes: 64 additions & 9 deletions cloud-infrastructure/cluster/deploy-cluster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
fi
fi
5 changes: 5 additions & 0 deletions cloud-infrastructure/cluster/main-cluster.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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]
}
Expand Down
6 changes: 1 addition & 5 deletions cloud-infrastructure/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions cloud-infrastructure/environment/deploy-environment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 2a21a67

Please sign in to comment.