Skip to content

Getting started with Azure Container Apps Jobs

Published: | 8 min read

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: diagram

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:

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:

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:

This is my default way to run scheduled tasks in Azure nowadays.

Reading list