Skip to content

Instantly share code, notes, and snippets.

@macel94
Last active March 24, 2025 10:55
Show Gist options
  • Save macel94/827c1d7641c13df322d2a252abf22989 to your computer and use it in GitHub Desktop.
Save macel94/827c1d7641c13df322d2a252abf22989 to your computer and use it in GitHub Desktop.
Azure Container App Jobs: Why I think they're Great
FROM mcr.microsoft.com/dotnet/sdk:latest
RUN DEBIAN_FRONTEND=noninteractive apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get upgrade -y
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
apt-transport-https \
apt-utils \
ca-certificates \
curl \
git \
iputils-ping \
jq \
lsb-release \
software-properties-common
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash
RUN az extension add --name azure-devops
# Install powershell core
RUN dotnet tool install --global PowerShell
# Install PowerApps CLI
RUN dotnet tool install --global Microsoft.PowerApps.CLI.Tool
# nvm requirements
RUN apt-get update
RUN echo "y" | apt-get install curl
# nvm env vars
RUN mkdir -p /usr/local/nvm
ENV NVM_DIR=/usr/local/nvm
# IMPORTANT: set the exact version
ENV NODE_VERSION=v23.6.0
# Install latest nvm
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use --delete-prefix $NODE_VERSION"
# add node and npm to the PATH
ENV NODE_PATH=$NVM_DIR/versions/node/$NODE_VERSION/bin
ENV PATH=$NODE_PATH:$PATH
#Install cli-microsoft365
RUN npm i -g @pnp/cli-microsoft365
# Add .NET global tools to the PATH
ENV PATH="$PATH:/root/.dotnet/tools"
# Can be 'linux-x64', 'linux-arm64', 'linux-arm', 'rhel.6-x64'.
ENV TARGETARCH=linux-x64
WORKDIR /azp
COPY infrastructure/agents-images/azure-devops/azure-pipelines-agent/start.sh .
RUN chmod +x start.sh
ENTRYPOINT [ "./start.sh" ]
FROM mcr.microsoft.com/powershell:latest
# Note: The DEBIAN_FRONTEND export avoids warnings when you go on to work with your container.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install git
# nvm requirements
RUN apt-get update
RUN echo "y" | apt-get install curl
# nvm env vars
RUN mkdir -p /usr/local/nvm
ENV NVM_DIR=/usr/local/nvm
# IMPORTANT: set the exact version
ENV NODE_VERSION=v23.6.0
# Install latest nvm
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use --delete-prefix $NODE_VERSION"
# add node and npm to the PATH
ENV NODE_PATH=$NVM_DIR/versions/node/$NODE_VERSION/bin
ENV PATH=$NODE_PATH:$PATH
#Install cli-microsoft365
RUN npm i -g @pnp/cli-microsoft365
SHELL ["/usr/bin/pwsh", "-c"]
RUN $ErrorActionPreference='Stop';
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/powershell
{
"name": "PowerShell",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"build": { "dockerfile": "DevContainer-Dockerfile" },
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {},
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": "true",
"username": "vscode",
"upgradePackages": "false",
"nonFreePackages": "true"
},
"ghcr.io/devcontainers/features/powershell:1": {},
"ghcr.io/devcontainers/features/azure-cli:1": {},
"ghcr.io/devcontainers/features/docker-outside-of-docker": {}
},
"postCreateCommand": "sudo chsh vscode -s \"$(which pwsh)\"",
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.defaultProfile.linux": "pwsh"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-vscode.powershell",
"ms-azuretools.vscode-docker",
"ms-vscode-remote.remote-containers",
"GitHub.copilot",
"GitHub.copilot-chat",
"ms-dotnettools.csdevkit",
"microsoft-IsvExpTools.powerplatform-vscode",
"ms-vscode-remote.remote-wsl",
]
}
}
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
name: $(Build.DefinitionName)_$(Date:yyyyMMdd)$(Rev:.r)
appendCommitMessageToRunName: false
trigger: none
pool:
name: container-apps
stages:
- stage: DoWhateverWithYourAgent
jobs:
- job: SampleJob
timeoutInMinutes: 480 # Set it as you want, remember not to exceed what was set for the az container app job
displayName: 'Execute SampleJob'
steps:
- checkout: self
clean: true
- task: PowerShell@2
displayName: 'Run a script from your repo, having the dependencies already installed'
inputs:
filePath: './src/scripts/sample.ps1'
failOnStderr: true
showWarnings: true
pwsh: true
# To read secrets if the file SetLocals.ps1 exists, execute it
$filePath = "../../../../SetLocals.ps1"
if (Test-Path $filePath) {
Write-Host "Reading secrets from $filePath"
. $filePath
}
else{
Write-Host "No secrets file found"
}
########################### WARNING!!!!!!!!!
# Create this app pool manually, in the organization you specify, before running the script
$AZP_POOL="container-apps"
$RESOURCE_GROUP="azdevops-azcontapps-jobs-sample"
$LOCATION="westeurope"
$ENVIRONMENT="${env:ORGANIZATION}-jobs-sample"
$JOB_NAME="j-${env:ORGANIZATION}-azdo-agent-job"
$PLACEHOLDER_JOB_NAME="pj-${env:ORGANIZATION}-placeholder-job"
$SUBSCRIPTION_ID="your-sub-id"
$CONTAINER_IMAGE_NAME="azure-pipelines-agent:$(Get-Date -Format 'ddMMyyyy')"
$CONTAINER_REGISTRY_NAME="your-reg"
$ORGANIZATION_URL = "https://dev.azure.com/${env:ORGANIZATION}"
docker build --pull --rm -f "path-to-your-ag-dockerfile/Agent-DockerFile" -t automations-agent:latest .
if (-not $env:AZP_TOKEN -or -not $env:ORGANIZATION) {
throw "AZP_TOKEN or ORGANIZATION environment variables are not set. Please set them in the SetLocals.ps1 file."
}
# az login
az account set --subscription $SUBSCRIPTION_ID
az account show
az extension add --name containerapp --upgrade --allow-preview true
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights
az group create `
--name "$RESOURCE_GROUP" `
--location "$LOCATION"
az containerapp env create `
--name "$ENVIRONMENT" `
--resource-group "$RESOURCE_GROUP" `
--location "$LOCATION"
az acr create --name "$CONTAINER_REGISTRY_NAME" --resource-group "$RESOURCE_GROUP" --location "$LOCATION" --sku Basic --admin-enabled true
# The default would be to build from a remote repository, but we are building from a local Dockerfile and pushing manually
az acr login --name "$CONTAINER_REGISTRY_NAME"
docker tag automations-agent:latest "$CONTAINER_REGISTRY_NAME.azurecr.io/$CONTAINER_IMAGE_NAME"
docker push "$CONTAINER_REGISTRY_NAME.azurecr.io/$CONTAINER_IMAGE_NAME"
az containerapp env create -n "$ENVIRONMENT" -g "$RESOURCE_GROUP" --location "$LOCATION"
az containerapp job create -n "$PLACEHOLDER_JOB_NAME" -g "$RESOURCE_GROUP" --environment "$ENVIRONMENT" --trigger-type Manual --replica-timeout 300 --replica-retry-limit 0 --replica-completion-count 1 --parallelism 1 --image "$CONTAINER_REGISTRY_NAME.azurecr.io/$CONTAINER_IMAGE_NAME" --cpu "2.0" --memory "4Gi" --secrets "personal-access-token=$env:AZP_TOKEN" "organization-url=$ORGANIZATION_URL" --env-vars "AZP_TOKEN=secretref:personal-access-token" "AZP_URL=secretref:organization-url" "AZP_POOL=$AZP_POOL" "AZP_PLACEHOLDER=1" "AZP_AGENT_NAME=placeholder-agent" --registry-server "$CONTAINER_REGISTRY_NAME.azurecr.io"
az containerapp job start -n "$PLACEHOLDER_JOB_NAME" -g "$RESOURCE_GROUP"
az containerapp job execution list --name "$PLACEHOLDER_JOB_NAME" --resource-group "$RESOURCE_GROUP" --output table --query '[].{Status: properties.status, Name: name, StartTime: properties.startTime}'
# wait for the placeholder to correctly be registered in your azdevops pool as described in the official guide before deleting it
# https://learn.microsoft.com/en-us/azure/container-apps/tutorial-ci-cd-runners-jobs?pivots=container-apps-jobs-self-hosted-ci-cd-azure-pipelines&tabs=powershell
az containerapp job delete -n "$PLACEHOLDER_JOB_NAME" -g "$RESOURCE_GROUP"
# Max execution time is 8 hours (28800 seconds)
az containerapp job create -n "$JOB_NAME" -g "$RESOURCE_GROUP" --environment "$ENVIRONMENT" --trigger-type Event --replica-timeout 28800 --replica-retry-limit 0 --replica-completion-count 1 --parallelism 1 --image "$CONTAINER_REGISTRY_NAME.azurecr.io/$CONTAINER_IMAGE_NAME" --min-executions 0 --max-executions 10 --polling-interval 30 --scale-rule-name "azure-pipelines" --scale-rule-type "azure-pipelines" --scale-rule-metadata "poolName=$AZP_POOL" "targetPipelinesQueueLength=1" --scale-rule-auth "personalAccessToken=personal-access-token" "organizationURL=organization-url" --cpu "2.0" --memory "4Gi" --secrets "personal-access-token=$env:AZP_TOKEN" "organization-url=$ORGANIZATION_URL" --env-vars "AZP_TOKEN=secretref:personal-access-token" "AZP_URL=secretref:organization-url" "AZP_POOL=$AZP_POOL" --registry-server "$CONTAINER_REGISTRY_NAME.azurecr.io"
#!/bin/bash
set -e
if [ -z "$AZP_URL" ]; then
echo 1>&2 "error: missing AZP_URL environment variable"
exit 1
fi
if [ -z "$AZP_TOKEN_FILE" ]; then
if [ -z "$AZP_TOKEN" ]; then
echo 1>&2 "error: missing AZP_TOKEN environment variable"
exit 1
fi
AZP_TOKEN_FILE=/azp/.token
echo -n $AZP_TOKEN > "$AZP_TOKEN_FILE"
fi
unset AZP_TOKEN
if [ -n "$AZP_WORK" ]; then
mkdir -p "$AZP_WORK"
fi
export AGENT_ALLOW_RUNASROOT="1"
cleanup() {
# If $AZP_PLACEHOLDER is set, skip cleanup
if [ -n "$AZP_PLACEHOLDER" ]; then
echo 'Running in placeholder mode, skipping cleanup'
return
fi
if [ -e config.sh ]; then
print_header "Cleanup. Removing Azure Pipelines agent..."
# If the agent has some running jobs, the configuration removal process will fail.
# So, give it some time to finish the job.
while true; do
./config.sh remove --unattended --auth PAT --token $(cat "$AZP_TOKEN_FILE") && break
echo "Retrying in 30 seconds..."
sleep 30
done
fi
}
print_header() {
lightcyan='\033[1;36m'
nocolor='\033[0m'
echo -e "${lightcyan}$1${nocolor}"
}
# Let the agent ignore the token env variables
export VSO_AGENT_IGNORE=AZP_TOKEN,AZP_TOKEN_FILE
print_header "1. Determining matching Azure Pipelines agent..."
AZP_AGENT_PACKAGES=$(curl -LsS \
-u user:$(cat "$AZP_TOKEN_FILE") \
-H 'Accept:application/json;' \
"$AZP_URL/_apis/distributedtask/packages/agent?platform=$TARGETARCH&top=1")
AZP_AGENT_PACKAGE_LATEST_URL=$(echo "$AZP_AGENT_PACKAGES" | jq -r '.value[0].downloadUrl')
if [ -z "$AZP_AGENT_PACKAGE_LATEST_URL" -o "$AZP_AGENT_PACKAGE_LATEST_URL" == "null" ]; then
echo 1>&2 "error: could not determine a matching Azure Pipelines agent"
echo 1>&2 "check that account '$AZP_URL' is correct and the token is valid for that account"
exit 1
fi
print_header "2. Downloading and extracting Azure Pipelines agent..."
echo "Agent package URL: $AZP_AGENT_PACKAGE_LATEST_URL"
curl -LsS $AZP_AGENT_PACKAGE_LATEST_URL | tar -xz & wait $!
source ./env.sh
trap 'cleanup; exit 0' EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
print_header "3. Configuring Azure Pipelines agent..."
./config.sh --unattended \
--agent "${AZP_AGENT_NAME:-$(hostname)}" \
--url "$AZP_URL" \
--auth PAT \
--token $(cat "$AZP_TOKEN_FILE") \
--pool "${AZP_POOL:-Default}" \
--work "${AZP_WORK:-_work}" \
--replace \
--acceptTeeEula & wait $!
print_header "4. Running Azure Pipelines agent..."
trap 'cleanup; exit 0' EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
chmod +x ./run.sh
# If $AZP_PLACEHOLDER is set, skipping running the agent
if [ -n "$AZP_PLACEHOLDER" ]; then
echo 'Running in placeholder mode, skipping running the agent'
else
# To be aware of TERM and INT signals call run.sh
# Running it with the --once flag at the end will shut down the agent after the build is executed
./run.sh --once & wait $!
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment