Desplegar Backstage en Azure Container Apps

Esta semana iba a contarte, en mi serie sobre Platform Engineering de mi canal de YouTube, cómo puedes desplegar Backstage en Azure Container Apps. Pero no he podido llegar a tiempo ⏱️ ¿Por qué? Pues porque me acabo de mudar a mi nuevo hogar y toda la casa hace eco eco eco…

Así que como no puedo grabar, pero si escribir 😃, en este artículo te voy a mostrar todo lo que necesitas para poder desplegar de forma sencilla Backstage, utilizando Azure Container Apps, del que ya te hablé en este otro artículo.

¿Qué necesito?

Para que puedas ver a alto nivel qué pinta va a tener nuestro Backstage en Azure lo he pintado de la siguiente manera:

Según la documentación oficial de Backstage, cuando llega el momento de poner este developer portal en producción lo que debemos hacer es uso de un Dockerfile que junta tanto la API como el frontal en una misma imagen, de tal forma que solamente te hace falta un contenedor que contenga todo. A mi personalmente no es la aproximación que más me gusta, ya que considero que el backend debería de estar desacoplado del frontend por temas de escalabilidad e incluso resilencia. Hay algunos colaboradores que si que han hecho esta separación y también se puede utilizar pero, por ahora, nos conformaremos con la opción 1 😄 ¡Así que empecemos!

Cambios en mi instancia de Backstage

Si has seguido mi serie, los únicos cambios que tienes que tener en cuenta son dos: el archivo app-config.yaml debe hacer referencia a variables de entorno en aquellos valores que vamos a recuperar dinámicamente:

Configurar variables

En este artículo voy a utilizar Azure CLI, para que entiendas bien todo lo que necesitas crear y configurar, dejando a un lado la infraestructura como código, luego tu eliges 😃.

Lo primero que hago siempre es definir las variables que voy a necesitar para crear los diferentes recursos que voy a necesitar en Azure:

RESOURCE_GROUP=backstage-on-aca
ACR_NAME=backstageimages
LOCATION=northeurope
AZURE_KEY_VAULT_NAME=backstage-key-vault
DB_PASSWORD=@P0stgres$RANDOM
DB_SERVER_NAME=backdb
CONTAINERAPPS_ENVIRONMENT=backstage-environment
AZURE_STORAGE_ACCOUNT=backstagestore
AZURE_STORAGE_CONTAINER=docs
IDENTITY_NAME=backstage-identity

De esta forma me garantizo que si tengo que modificar alguno y este está referenciado en varios sitios no tenga que ir reemplazándolo en todos ellos.

Registrar una aplicación en Microsoft Entra ID

Siguiendo el mismo ejemplo que en el vídeo anterior de la serie, donde te contaba cómo podías integrar Backstage con Azure DevOps, para este despliegue voy a usar el mismo ejemplo (aunque puedes modificarlo si lo necesitas para que se integre con GitHub en su lugar), por lo que lo primero que necesito es registrar una aplicación en mi tenant de Microsoft Entra ID:

source .env
az login --tenant $AZURE_TENANT_ID --allow-no-subscriptions --use-device-code
CLIENT_ID=$(az ad app create --display-name $RESOURCE_GROUP --query appId -o tsv)
#Generate a secret for the app
CLIENT_SECRET=$(az ad app credential reset --id $CLIENT_ID --query password -o tsv)
# Add the following API Permissions:
# Microsoft Graph:
# - User.Read
# - User.Read.All
# profile
# offline_access
# email
# openid
# GroupMember.Read.All
# https://learn.microsoft.com/en-us/graph/permissions-reference
az ad app permission add --id $CLIENT_ID --api 00000003-0000-0000-c000-000000000000 --api-permissions e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope # User.Read
az ad app permission add --id $CLIENT_ID --api 00000003-0000-0000-c000-000000000000 --api-permissions 37f7f235-527c-4136-accd-4a02d197296e=Scope # openid
az ad app permission add --id $CLIENT_ID --api 00000003-0000-0000-c000-000000000000 --api-permissions 64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0=Scope # email
az ad app permission add --id $CLIENT_ID --api 00000003-0000-0000-c000-000000000000 --api-permissions 7427e0e9-2fba-42fe-b0c0-848c9e6a8182=Scope # offline_access
az ad app permission add --id $CLIENT_ID --api 00000003-0000-0000-c000-000000000000 --api-permissions 14dad69e-099b-42c9-810b-d002981feec1=Scope # profile
az ad app permission add --id $CLIENT_ID --api 00000003-0000-0000-c000-000000000000 --api-permissions df021288-bdef-4463-88db-98f22de89214=Role # User.Read.All
az ad app permission add --id $CLIENT_ID --api 00000003-0000-0000-c000-000000000000 --api-permissions 98830695-27a2-44f7-8c18-0c3ebc9698f6=Role # GroupMember.Read.All
# Wait for the permissions to be granted
sleep 30
# Grant admin consent
az ad app permission admin-consent --id $CLIENT_ID

Como puedes ver, lo primero que hago es cargar los valores que he almacenado en un .env para evitar que los mismos formen parte del repo donde te comparto todo estos pasos. En él solamente tengo un Personal Access Token de Azure DevOps, del que te hablaré después, y el ID del tenant de Microsoft Entra ID:

ADO_PAT=<YOUR_AZURE_DEVOPS_PAT>
AZURE_TENANT_ID=<YOUR_MICROSOFT_ENTRA_ID>

Por otro lado, si el tenant que quieres utilizar no es el mismo donde vas a desplegar los recursos, y además este no tiene asociada ninguna suscripción, puedes usar az login con los parámetros que te comparto.

Una vez que ya estás logado en el tenant correcto, puedes registrar la aplicación, y recuperar el client id, y generar un secreto y almacenarlo en la variable llamada CLIENT_SECRET. Por otro lado, este mismo registro de aplicación lo vamos a usar para recuperar los ususarios y grupos de nuestro tenant, por lo que debemos asignarle los permisos que ves, esperar unos treinta segundos para que le de tiempo a este proceso y por último consentir todo ellos para que los usuarios de forma individual no tengan que hacerlo.

Recursos de Azure

Como has podido ver en la imagen a alto nivel que te compartí al inicio de este artículo, además del propio servicio de Azure Container Apps, vamos a necesitar otros servicios satélite que van a acompañar al mismo, estos son:

  • Una base de datos Postgres en Azure Database for PostgreSQL
  • Una cuenta de almacenamiento en Azure Storage para guardar la documentación generada por TechDocs
  • Una cuenta de Azure Key Vault para almacenar los secretos
  • El contenedor en Azure Container Apps

Para poder almacenar todos estos lo primero que necesitas es crear un grupo de recursos:

### 1. Login to Azure
If you want to deploy your resources in a different subscription, you can use the following command:
```bash
az login --use-device-code
```
### 2.Create a resource group
```bash
az group create --name $RESOURCE_GROUP --location $LOCATION

Base de datos en Azure Database for PostgreSQL

En la configuración de partida de Backstage, este utiliza una base de datos en memoria para el entorno de desarrollo, pero como ya te conté en el primero vídeo sobre este IDP, no es lo ideal para un entorno productivo e incluso te mostré cómo configurarte un PostgreSQL dentro del Dev Container. En Azure voy a utilizar Azure Database for PostgreSQL, un servicio PaaS que nos va a permitir no solo lo obvio sino que también vamos a poder generar backups, nos da recomendaciones, podemos geo-replicar, etcétera. Para crearla con lo que necesitamos por ahora usaríamos el siguiente script:

### 3.Create PostgreSQL database
```bash
POSTGRES_SERVER_FQDN=$(az postgres flexible-server create \
--resource-group $RESOURCE_GROUP \
--name $DB_SERVER_NAME \
--location $LOCATION \
--admin-user postgres \
--admin-password $DB_PASSWORD \
--public-access 0.0.0.0)
POSTGRES_SERVER_FQDN=$(az postgres flexible-server show --resource-group $RESOURCE_GROUP --name $DB_SERVER_NAME --query fullyQualifiedDomainName -o tsv)
# Disable SSL transport
 az postgres flexible-server parameter set --server-name $DB_SERVER_NAME --resource-group $RESOURCE_GROUP --name require_secure_transport --value off 

Para este ejemplo he deshabilitado que sea obligatorio el transporte seguro para no tener que modificar el código de ejemplo 😇

Una cuenta de almacenamiento para la documentación

Lo siguiente que vamos a crear es una cuenta de almacenamiento en la cual, gracias a nuestra Azure Pipelines, vamos a poder almacenar la documentación generada por TechDocs.

az storage account create \
--name $AZURE_STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku Standard_LRS
az storage container create \
--name $AZURE_STORAGE_CONTAINER \
--account-name $AZURE_STORAGE_ACCOUNT

Azure Container Registry para almacenar las imágenes de Backstage

Tanto para la primera vez, como para cada vez que hagamos un cambio en nuestra instancia de Backstage (añadir plugins, actualizaciones de dependencias, etcétera), vamos a necesitar generar una nueva versión de la imagen que producirá el Dockerfile que viene como parte del proyecto. Para ello lo más sencillo es crear un servicio del tipo Azure Container Registry, el cuál no solo va a almacenar las imágenes y va a ser la fuente de la cual Azure Container Apps recupere la que necesita para crear el contenedor, sino que también nos va a ayudar a generarla, para que no tengamos que hacerlo en nuestro equipo.

Por lo que primero creamos el servicio:

az acr create --resource-group $RESOURCE_GROUP --name $ACR_NAME --sku Basic --admin-enabled true

y luego seguimos los pasos, tal y como se detalla en la documentación de Backstage, para compilar el proyecto en Typescript y añadir el archivo de configuración con todo lo que ya te conté en el vídeo anterior.

cd backstage
yarn install --frozen-lockfile
yarn tsc
yarn build:backend --config ../../app-config.yaml
cd ..

Una vez que ya se han ejecutado todos le podemos pedir a Azure Container Registry que genere la imagen:

az acr run -f acr-task.yaml --registry $ACR_NAME ./backstage

Si te fijas, este comando utiliza un archivo que le dice cómo tiene que ejecutar la tarea. El mismo tiene la siguiente pinta:

version: v1.1.0
steps:
  - build: -t $Registry/backstage:$ID -f packages/backend/Dockerfile .
    env: 
      - DOCKER_BUILDKIT=1
  - push: 
    - "$Registry/backstage:$ID"

Lo más importante de este archivo es que nos permite habilitar la opción de BuildKit en ACR, porque de lo contrario no se generará la imagen correctamente. Una vez que finalice el proceso, lo único que nos queda por hacer es recuperar el tag que nos haya generado para después:

LAST_IMAGE_TAG=$(az acr repository show-tags --name $ACR_NAME --repository backstage --orderby time_desc --query '[0]' -o tsv)

Azure Key Vault para guardar secretos

Para seguir las buenas prácticas desde el punto de vista de seguridad, es recomendable que los datos sensibles que nuestro contenedor va a necesitar. Para ello necesitaras un Azure Key Vault que nos permite guardar este tipo de información y que luego podremos referenciar desde Azure Container Apps.

az keyvault create \
--name $AZURE_KEY_VAULT_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION

Una vez creado el recurso, ya podremos añadir en él todos los secretos que necesitamos:

BACKEND_SECRET_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name BACKEND-SECRET \
--value secret \
--query id -o tsv)
AZURE_PERSONAL_ACCESS_TOKEN_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name AZURE-PERSONAL-ACCESS-TOKEN \
--value $ADO_PAT \
--query id -o tsv)
TECHDOCS_AZURE_CONTAINER_NAME_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name TECHDOCS-AZURE-CONTAINER-NAME \
--value $AZURE_STORAGE_CONTAINER \
--query id -o tsv)
TECHDOCS_AZURE_ACCOUNT_NAME_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name TECHDOCS-AZURE-ACCOUNT-NAME \
--value $AZURE_STORAGE_ACCOUNT \
--query id -o tsv)
STORAGE_ACCOUNT_KEY=$(az storage account keys list --resource-group $RESOURCE_GROUP --account-name $AZURE_STORAGE_ACCOUNT --query "[0].value" -o tsv)
TECHDOCS_AZURE_ACCOUNT_KEY_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name TECHDOCS-AZURE-ACCOUNT-KEY \
--value $STORAGE_ACCOUNT_KEY \
--query id -o tsv)
AZURE_CLIENT_ID_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name AZURE-CLIENT-ID \
--value $CLIENT_ID \
--query id -o tsv)
AZURE_CLIENT_SECRET_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name AZURE-CLIENT-SECRET \
--value $CLIENT_SECRET \
--query id -o tsv)
AZURE_TENANT_ID_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name AZURE-TENANT-ID \
--value $AZURE_TENANT_ID \
--query id -o tsv)
POSTGRES_HOST_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name POSTGRES-HOST \
--value $POSTGRES_SERVER_FQDN \
--query id -o tsv)
POSTGRES_PORT_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name POSTGRES-PORT \
--value 5432 \
--query id -o tsv)
POSTGRES_USER_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name POSTGRES-USER \
--value postgres \
--query id -o tsv)
POSTGRES_PASSWORD_URI=$(az keyvault secret set \
--vault-name $AZURE_KEY_VAULT_NAME \
--name POSTGRES-PASSWORD \
--value $DB_PASSWORD \
--query id -o tsv)

Crear contenedor con Backstage en Azure Container Apps

Ahora que ya tenemos todas las dependencias en su sitio lo único que nos queda es crear el contenedor que ejecutará una instancia de nuestra imagen de Backstage que ya está esperando en nuestro ACR. Si todavía no la tienes, necesitas tener instalada la extensión de ACA para Azure CLI:

az extension add --name containerapp --upgrade

Creamos un entorno donde vivirá este contenedor:

# Create a container app environment
az containerapp env create \
--name $CONTAINERAPPS_ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION

Y una vez creado, vamos a generar una managed identity para poder darle permisos a la misma sobre el Key Vault que tiene los secretos que va a necesitar:

BACKSTAGE_IDENTITY_ID=$(az identity create --name $IDENTITY_NAME --resource-group $RESOURCE_GROUP --query id -o tsv)
```
Give permissions to the identity to access the Key Vault
```bash
az keyvault set-policy --name $AZURE_KEY_VAULT_NAME --object-id $(az identity show --name $IDENTITY_NAME --resource-group $RESOURCE_GROUP --query principalId -o tsv) --secret-permissions get list

Y ahora, no te asustes, viene el comando que hará que toda esta configuración que hemos generado, haga que nuestro portal de Backstage comience a funcionar sobre Azure:

# Create a container app
az containerapp create \
--name backstage \
--environment $CONTAINERAPPS_ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
--min-replicas 1 \
--image "$ACR_NAME.azurecr.io/backstage:$LAST_IMAGE_TAG" \
--secrets "backend-secret=keyvaultref:$BACKEND_SECRET_URI,identityref:$BACKSTAGE_IDENTITY_ID" "azure-personal-access-token=keyvaultref:$AZURE_PERSONAL_ACCESS_TOKEN_URI,identityref:$BACKSTAGE_IDENTITY_ID" "techdocs-azure-container-name=keyvaultref:$TECHDOCS_AZURE_CONTAINER_NAME_URI,identityref:$BACKSTAGE_IDENTITY_ID" "techdocs-azure-account-name=keyvaultref:$TECHDOCS_AZURE_ACCOUNT_NAME_URI,identityref:$BACKSTAGE_IDENTITY_ID" "techdocs-azure-account-key=keyvaultref:$TECHDOCS_AZURE_ACCOUNT_KEY_URI,identityref:$BACKSTAGE_IDENTITY_ID" "azure-client-id=keyvaultref:$AZURE_CLIENT_ID_URI,identityref:$BACKSTAGE_IDENTITY_ID" "azure-client-secret=keyvaultref:$AZURE_CLIENT_SECRET_URI,identityref:$BACKSTAGE_IDENTITY_ID" "azure-tenant-id=keyvaultref:$AZURE_TENANT_ID_URI,identityref:$BACKSTAGE_IDENTITY_ID" "postgres-host=keyvaultref:$POSTGRES_HOST_URI,identityref:$BACKSTAGE_IDENTITY_ID" "postgres-port=keyvaultref:$POSTGRES_PORT_URI,identityref:$BACKSTAGE_IDENTITY_ID" "postgres-user=keyvaultref:$POSTGRES_USER_URI,identityref:$BACKSTAGE_IDENTITY_ID" "postgres-password=keyvaultref:$POSTGRES_PASSWORD_URI,identityref:$BACKSTAGE_IDENTITY_ID" \
--env-vars "BACKEND_SECRET=secretref:backend-secret" "AZURE_PERSONAL_ACCESS_TOKEN=secretref:azure-personal-access-token" "TECHDOCS_AZURE_CONTAINER_NAME=secretref:techdocs-azure-container-name" "TECHDOCS_AZURE_ACCOUNT_NAME=secretref:techdocs-azure-account-name" "TECHDOCS_AZURE_ACCOUNT_KEY=secretref:techdocs-azure-account-key" "AZURE_CLIENT_ID=secretref:azure-client-id" "AZURE_CLIENT_SECRET=secretref:azure-client-secret" "AZURE_TENANT_ID=secretref:azure-tenant-id" "POSTGRES_HOST=secretref:postgres-host" "POSTGRES_PORT=secretref:postgres-port" "POSTGRES_USER=secretref:postgres-user" "POSTGRES_PASSWORD=secretref:postgres-password" \
--user-assigned $BACKSTAGE_IDENTITY_ID \
--ingress external \
--target-port 7007 \
--registry-username $ACR_NAME \
--registry-password $(az acr credential show --name $ACR_NAME --query passwords[0].value -o tsv) \
--registry-server $ACR_NAME.azurecr.io

Aunque parece que tiene mucha chicha, en realidad no es tanta: A parte de indicarle en qué entorno de Azure Container Apps lo vamos a desplegar, se le especifica el número mínimo de réplicas, para evitar que se quede a cero, la imagen y la versión (tag) que vamos a utilizar para este contenedor, y luego todos los secretos que tenemos en el Key Vault que vamos a cargar variables de entorno para que Backstage los pueda consumir. Le asignamos la identidad que acabamos de crear, que es la que tiene permisos para recuperar todos estos valores, y luego le configuramos el ingress para que podamos acceder al sitio a través de https y que realmente internamente se rediriga al puerto 7007, que es donde está escuchando nuestra instancia de Backstage. Para que este contendor pueda recuperar la imagen que hemos generado hay que pasarle por último las credenciales del ACR y ya casi estaría 🥳

Para comprobar que el contenedor se ha ejecutado correctamente (o no), puedes utilizar este otro comando que te permite ver los logs que está generando el mismo, sin necesidad de entrar al portal de Azure, donde también lo puedes revisar:

az containerapp logs show --name backstage --resource-group $RESOURCE_GROUP

Modificar la URL de callback de la aplicación registrada en Microsoft Entra ID

Para terminar con la configuración, lo último que queda por hacer es modificar la URL de callback que necesita la aplicación registrada en Microsoft Entra ID para que cuando iniciemos sesión sepa volver correctamente a nuestro sitio. Para ello necesitamos recuperar la URL que nos ha generado ACA (si tuvieras un dominio personalizado esta parte no sería necesaria):

CONTAINER_APP_URL=$(az containerapp show --name backstage --resource-group $RESOURCE_GROUP --query "properties.configuration.ingress.fqdn" -o tsv)

y modificar la misma en Entra ID:

az login --tenant $AZURE_TENANT_ID --allow-no-subscriptions --use-device-code
az ad app update --id ${CLIENT_ID} --web-redirect-uris "https://${CONTAINER_APP_URL}/api/auth/microsoft/handler/frame"

Y ahora ya, la prueba de fuego 🔥, puedes utiliza reste otro comando para que te de URL de acceso a tu nueva instancia de Backstage, ahora en Azure 💙:

echo https://$CONTAINER_APP_URL

El código de ejemplo lo tienes en mi repo en GitHub.

¡Saludos 👋🏻!