Table of contents
Open Table of contents
Intro
Azure has multiple reasonably priced ways to run tasks on a schedule. There is a common issue with many of those though. All of the long-standing options like Automation account and Azure Functions have limited runtime support, which can be a problem. I would like to control the runtime version and programming language which I use to develop these tasks. Sometimes an external binary is needed within the task, or the maximum timeout is not enough with long-running tasks and using Durable Functions would add too much complexity overhead. From the developer’s point of view I find that dependency management and testing could also be much easier than it is.
This is why I wanted to take Azure Container Apps Jobs for a spin. The example task interacts with an API on public internet, and stores results as a blob on Storage account. Since there was not requirement for private networking I wanted to keep things as simple as possible, and did not deploy the environment to my own virtual network.
In this case the app is written in TypeScript, with some features written in Go. The Go part of the app is compiled to binary and included in the same container, and called from the TS code.
An overview of the solution:
This article is not about the local development flow or the app itself, but focuses on how to get the app running on Container Apps as a Job.
NOTE: All the commands following are run on macOS and zsh shell so adjust for your operating system and shell environment as needed.
Az CLI setup
To make sure the latest and greatest is within my disposal, I ugraded the az cli
, which is at version 2.66.0
and the containerapp
extension version is 1.0.0b3
at the time of writing.
az upgrade
az extension add --name containerapp --upgrade
Before doing anything else make sure you have Owner role in Azure. Login to Azure CLI with az login
and set subscription with az account set --subscription "ID or name"
to make sure the deployment will be in desired Subscription.
Define environment variables
Let’s start by defining variables for naming things:
ACA_RG_NAME="rg-tasks"
ACA_LOCATION="swedencentral"
ACA_ACR_NAME="acrtasks"
ACA_LAW_NAME="law-tasks"
ACA_CAPPS_ENV="cae-tasks"
ACA_UAMI="mi-tasks"
ACA_STORAGE_NAME="sttasks"
ACA_STORAGE_CONTAINER_NAME="data"
ACA_IMAGE_NAME_AND_TAG="my-task:1.0.0"
Scaffold Azure infra
First thing was to scaffold the required Azure infra. This time I chose to do it with simple shell scripts and az cli
commands instead of “real” IaC like Terraform.
As can be seen in the diagram above, I needed to create:
- Resource Group
- Container Registry
- Log Analytics Workspace
- Storage account
- User-assigned Managed identity
- Container Apps Environment
First make sure the required Resource providers have been registered:
echo "Registering resource providers"
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights
az provider register --namespace Microsoft.ContainerRegistry
echo "Registration successfully triggered, wait for completion before proceeding."
The command will not wait until the registration is completed, so make sure it is before moving forward.
Let’s start creating resources with the Resource group:
# Create Resource group
echo "Creating Resource Group $ACA_RG_NAME in $ACA_LOCATION"
az group create --name $ACA_RG_NAME --location $ACA_LOCATION --tags owner=janik6n environment=prod
When that is ready, next up is Container Registry which will store all the container images used in tasks, and is required by Container Apps:
# Create Container Registry
echo "Creating Container Registry $ACA_ACR_NAME in $ACA_LOCATION"
az acr create --resource-group $ACA_RG_NAME \
--name $ACA_ACR_NAME --sku Basic \
--location $ACA_LOCATION
Logging is often useful, so let’s create Log Analytics Workspace for that, with 90 day log retention:
# Create Log Analytics Workspace
echo "Creating Log Analytics Workspace $ACA_LAW_NAME in $ACA_LOCATION"
az monitor log-analytics workspace create -g $ACA_RG_NAME -n $ACA_LAW_NAME \
--location $ACA_LOCATION \
--retention-time 90
When the Container Apps Environment will be created later, it will be connected to this Log Analytics Workspace with id and keys so let’s get those next:
# Query LAW id
echo "Querying Log Analytics Workspace ID"
ACA_LAW_ID=$(az monitor log-analytics workspace show -g $ACA_RG_NAME -n $ACA_LAW_NAME --query customerId --output tsv)
# Query LAW keys
echo "Querying Log Analytics Workspace keys"
ACA_LAW_KEY=$(az monitor log-analytics workspace get-shared-keys -g $ACA_RG_NAME -n $ACA_LAW_NAME --query primarySharedKey --output tsv)
The results of my task will be stored as blobs in Storage account, so let’s create that too:
# Create Storage account
echo "Creating Storage Account $ACA_STORAGE_NAME in $ACA_LOCATION"
az storage account create -n $ACA_STORAGE_NAME -g $ACA_RG_NAME -l $ACA_LOCATION --sku Standard_ZRS
Creating a Blob container is a data plane operation, and needs few extra steps:
echo "Creating Blob Container $ACA_STORAGE_CONTAINER_NAME in $ACA_STORAGE_NAME"
# Get Storage account ID
ACA_STORAGE_ID=$(az storage account show -n $ACA_STORAGE_NAME -g $ACA_RG_NAME --query id --output tsv)
# Get my logged in identity and assing myself required role
az ad signed-in-user show --query id -o tsv | az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee @- \
--scope $ACA_STORAGE_ID
# Create Blob container
az storage container create -n $ACA_STORAGE_CONTAINER_NAME \
--account-name $ACA_STORAGE_NAME \
--auth-mode login
Next up is User-assigned Managed identity, which will be used by the Container Apps Job to authenticate against Container Registry and Storage account:
# Create user-assigned managed identity
echo "Creating User-Assigned Managed Identity $ACA_UAMI in $ACA_LOCATION"
az identity create --name $ACA_UAMI --resource-group $ACA_RG_NAME --location $ACA_LOCATION
And let’s also give it the right to write to the Blob container created earlier:
# Create role assignment to write to storage from app
echo "Creating role assignment to write to storage from app"
# Get the principal ID for the identity
ACA_UAMI_PRINCIPAL_ID=$(az identity show --name $ACA_UAMI --resource-group $ACA_RG_NAME --query principalId --output tsv)
# Create role assignment
az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee $ACA_UAMI_PRINCIPAL_ID \
--scope $ACA_STORAGE_ID
And finally, let’s create the Container Apps Environment, which is a logical container (no pun intended) and security boundary for the Jobs:
# Create Container Apps Environment
echo "Creating Container Apps Environment $ACA_CAPPS_ENV in $ACA_LOCATION"
az containerapp env create --name $ACA_CAPPS_ENV \
--resource-group $ACA_RG_NAME --location $ACA_LOCATION \
--logs-destination log-analytics \
--logs-workspace-id $ACA_LAW_ID \
--logs-workspace-key $ACA_LAW_KEY
When I don’t bring in my own virtual network, Azure will create a fully-managed virtual network for the environment. This network is invisible to me, and I cannot interact with it in any way.
Build the container image and push it to Container Registry
Now that I had the app developed and tested locally, and the required Azure infra was ready it was time to build the container image.
At the time of writing, Azure Container Apps supports only linux/amd64
container architecture:
# For Azure Container Apps, we need linux/amd64
echo "Building image $ACA_IMAGE_NAME_AND_TAG"
docker build --platform linux/amd64 -t $ACA_IMAGE_NAME_AND_TAG .
Login to the Registry:
# Login to registry
echo "Logging in to Azure Container Registry $ACA_ACR_NAME"
az acr login --name $ACA_ACR_NAME
Get the Server url:
# Get the login server url
echo "Querying Azure Container Registry login server"
ACA_ACR_LOGIN_SERVER=$(az acr show -n $ACA_ACR_NAME --query loginServer --output tsv)
Tag the built image:
# Tag the built image
echo "Tagging image $ACA_IMAGE_NAME_AND_TAG as $ACA_ACR_LOGIN_SERVER/$ACA_IMAGE_NAME_AND_TAG"
docker tag $ACA_IMAGE_NAME_AND_TAG $ACA_ACR_LOGIN_SERVER/$ACA_IMAGE_NAME_AND_TAG
And finally push the image to the Container Registry:
# Push the image to registry
echo "Pushing image $ACA_IMAGE_NAME_AND_TAG to Azure Container Registry $ACA_ACR_NAME"
docker push $ACA_ACR_LOGIN_SERVER/$ACA_IMAGE_NAME_AND_TAG
Create a scheduled Container Apps Job
The job creation will need yet another ID for the Managed identity, so let’s get that first:
# Get the Managed ID ID
echo "Querying User-Assigned Managed Identity ID"
ACA_UAMI_ID=$(az identity show --name $ACA_UAMI --resource-group $ACA_RG_NAME --query id --output tsv)
# Get the Managed Identity ClientID
echo "Querying User-Assigned Managed Identity ClientID"
MANAGED_IDENTITY_CLIENT_ID=$(az identity show --name $ACA_UAMI --resource-group $ACA_RG_NAME --query clientId --output tsv)
And as a final step, let’s create the scheduled Container Apps Job itself:
# Create Scheduled (1st day of each month at 09.00 UTC) Container App Job
echo "Creating Scheduled Container App Job $ACA_JOB_NAME to run on the 1st day of each month at 09.00 UTC"
az containerapp job create --name "$ACA_JOB_NAME" \
--resource-group "$ACA_RG_NAME" \
--environment "$ACA_CAPPS_ENV" \
--trigger-type "Schedule" \
--cron-expression "0 9 1 * *" \
--replica-timeout 900 \
--mi-user-assigned $ACA_UAMI_ID \
--registry-identity $ACA_UAMI_ID \
--registry-server $ACA_ACR_LOGIN_SERVER \
--image "$ACA_ACR_LOGIN_SERVER/$ACA_IMAGE_NAME_AND_TAG" \
--cpu "0.25" --memory "0.5Gi" \
--secrets "managed-identity-client-id=$MANAGED_IDENTITY_CLIENT_ID" \
--env-vars "PLATFORM=aca" "STORAGE_ACCOUNT_NAME=$ACA_STORAGE_NAME" "OFFSET=1" "MANAGED_IDENTITY_CLIENT_ID=secretref:managed-identity-client-id"
For the Job creation, many of the previously created things are needed:
- Resource Group and Container Apps Environment where the Job will be “hosted”
- The Job will use the User-assigned Managed identity to authenticate against the Container Registry when it pulls the image. The required role is assigned automatically when the Job is created with
az cli
. - Same Managed identity will be used to interact with the Storage account. For this we created the role assignment earlier.
- The schedule is defined as cron expression
- —replica-timeout is the timeout in seconds after which the task will be terminated unless it has exited before.
cpu
andmemory
will define the allocated resources for the Job- With
secrets
it is possible to manage Job specific secrets, such as API keys etc. Referencing Key Vault secrets is also possible. - These secrets are then injected to running container as environment variables defined with
--env-vars
. Of course other “non-secret” environment variables can be used too,"PLATFORM=aca"
,"STORAGE_ACCOUNT_NAME=$ACA_STORAGE_NAME"
and"OFFSET=1"
in the above command.
And that’s it! After all this I could download the results from Storage account after the successful run.
Should there be any issues, I can dive into the logs from the Log Analytics Workspace. Example query in KQL:
ContainerAppConsoleLogs_CL
| where ContainerImage_s == "acrtasks.azurecr.io/my-task:1.0.0"
| order by _timestamp_d asc
Thoughts
My experience with Container Apps Jobs have been very positive so far. What I especially like:
- Easy to develop and test the task apps and deploy them as containers.
- Almost full freedom with programming languages and runtimes.
- Plenty of room with timeouts.
- Azure standard logging solution with Log Analytics. Enables the creation of dashboards, monitoring and alerting as per usual in Azure.
- Support for Managed identity for authentication and authorization against other Azure services.
- Simple Azure architecture and reasonable pricing.
This is my default way to run scheduled tasks in Azure nowadays.
Reading list
- https://learn.microsoft.com/en-us/azure/container-registry/container-registry-get-started-azure-cli
- https://azure.microsoft.com/en-us/pricing/details/container-registry/
- https://learn.microsoft.com/en-us/cli/azure/containerapp/env?view=azure-cli-latest#az-containerapp-env-create
- https://github.com/Azure/azure-cli/issues/27098
- https://learn.microsoft.com/en-us/cli/azure/identity?view=azure-cli-latest#az-identity-create
- https://learn.microsoft.com/en-us/azure/container-apps/jobs?tabs=azure-cli#scheduled-jobs
- https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-cli