You may have the requirement of creating self-hosted agents in Azure DevOps, there are multiple ways of doing this including Virtual Machine, Virtual Machine Scaleset & various Container-type approaches. In this blog I am going to to be using Azure Container Apps and KEDA to create self-hosted scalable Azure DevOps Agents deployed using Azure DevOps and Azure CLI.
In a previous blog post; I blogged Deploy to Azure Container App from Azure Container Registry using a CI/CD Azure DevOps Pipeline and Azure CLI – the setup for this post will be quite similar.
Not heard of Container apps? Watch this awesome video to see them in action!
How to Build and Deliver Apps Fast and Scalable with Azure Container Apps
Why a container for the self-hosted DevOps Agent?
A question that appears quite often; a container has so many benefits including:
- No need to worry about the underlying host machine (patching/updating Windows/Linux – not required)
- Usually a lot more cost effective
- Deploy with ease; whether locally, Container Instance, Kubernetes cluster, Container App etc
- Ability to deploy anywhere pretty much
- Flexibility and scalability
Only a few advantages of why to use a container
Azure Container Apps and Scaling
As documented in the Azure documentation for Azure Container Apps; they can dynamically scale based on the following characteristics:
- HTTP traffic
- Event-driven processing
- CPU or memory load
- Any KEDA-supported scaler
KEDA
Lets look at the feature we will be using in this blog post to scale the Azure DevOps agents, KEDA!
Current available KEDA scalers are listed here . We Will be using Azure Pipelines – its awesome! How does it scale you may be asking?
It scales based on the amount of pipeline runs pending in a given agent pool.
KEDA stands for Kubernetes Event-driven Autoscaler. It is built to be able to activate a Kubernetes deployment (i.e. no pods to a single pod) and subsequently to more pods based on events from various event sources. KEDA is a single-purpose and lightweight component that can be added into any Kubernetes cluster. KEDA works alongside standard Kubernetes components like the Horizontal Pod Autoscaler and can extend functionality without overwriting or duplication. With KEDA you can explicitly map the apps you want to use event-driven scale, with other apps continuing to function. This makes KEDA a flexible and safe option to run alongside any number of any other Kubernetes applications or frameworks.
https://keda.sh/docs/2.6/concepts/
Please note, this is a Proof-Of-Concept
This concept is cool ; but after further reading its not supported as such Currently its a PoC concept and maybe never a complete reality. I was going with a Container Instance approach and then thought i’d try with Container Apps as they can use KEDA.
Due to the KEDA not having ScaledJobs – the agents can be removed at any time, I didn’t notice this during my testing/creation of running jobs (must have been lucky). Some discussion here if you are interested https://twitter.com/JorgeArteiro/status/1494324269371002882
Time to Deploy
Pre-reqs
Lets start with the pre-reqs:
- Variable group
azure-container-app
to be created
Check out this blog regarding Referencing Variable Groups in Azure DevOps Pipeline Templates and how to set one one
- PAT Token is required , this will be used by the container to access Azure DevOps pipelines etc.
Documented here & save PAT token as pattoken
in the created Variable Group azure-container-app
The Pipeline
Very similar to my last blog post “Deploy to Azure Container App from Azure Container Registry using a CI/CD Azure DevOps Pipeline and Azure CLI” the pipeline will be in 4 stages:
create_acr
will create resource group , container registry to where the sample application will be uploaded to and also log analytics workspace that the container environment will connect tobuild
will build the sample application and push to the Container Registry created increate_acr
stage (build will build Azure DevOps docker image, reference to docker files here)create_container_environment
Will create the container environment to host the container app that will be deployed in the final stagedeploy_container_app
will deploy the container app from image created inbuild
stage
Please follow that blog on the initial setup of the first 3 stages. The fourth stage deploy_container_app
is slightly different & I will document this below:
Prior to the final stage of the pipeline, we need to create a new agent pool. This is where the container app agents will be situated
Select Agent Pools within Organisation settings

Add pool similar to the below, I will be using containerapp-pool

Now that the pool is created, select the pool in browser and make note of URL as below:
https://dev.azure.com/TerraformTraining/TamOpsTerraform/_settings/agentqueues?queueId=91&view=jobs
Make note of queueId
, mine will be 91
Looking at the final stage
The final stage below deploys the Azure Container app as a group deployment with a template file.
The template file containerapp.json
below will be referenced:
{
"$schema": "https://schema.management.azure.com/schemas/2019-08-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"location": {
"defaultValue": "northeurope",
"type": "String"
},
"environment_name": {
"type": "String"
},
"azp_url": {
"type": "securestring"
},
"azp_token": {
"type": "securestring"
},
"azp_pool": {
"type": "String"
},
"azp_poolId": {
"type": "String"
},
"REGISTRY_LOGIN_SERVER": {
"type": "String"
},
"REGISTRY_USERNAME": {
"type": "String"
},
"REGISTRY_PASSWORD": {
"type": "securestring"
},
"IMAGE": {
"type": "String"
},
"MIN_REPLICAS": {
"type": "String"
},
"MAX_REPLICAS": {
"type": "String"
},
"CPU": {
"type": "String"
},
"MEMORY": {
"type": "String"
},
"CONTAINER_APP_NAME": {
"type": "String"
}
},
"variables": {},
"resources": [
{
"name": "azuredevops-agent",
"type": "Microsoft.Web/containerApps",
"apiVersion": "2021-03-01",
"kind": "containerapp",
"location": "[parameters('location')]",
"properties": {
"kubeEnvironmentId": "[resourceId('Microsoft.Web/kubeEnvironments', parameters('environment_name'))]",
"configuration": {
"secrets": [
{
"name": "azp-url",
"value": "[parameters('azp_url')]"
},
{
"name": "azp-token",
"value": "[parameters('azp_token')]"
},
{
"name": "azp-pool",
"value": "[parameters('azp_pool')]"
},
{
"name": "azp-poolid",
"value": "[parameters('azp_poolId')]"
},
{
"name": "registry-secret",
"value": "[parameters('REGISTRY_PASSWORD')]"
}
],
"registries": [
{
"server": "[parameters('REGISTRY_LOGIN_SERVER')]",
"username": "[parameters('REGISTRY_USERNAME')]",
"passwordSecretRef": "registry-secret"
}
]
},
"template": {
"containers": [
{
"image": "[parameters('IMAGE')]",
"name": "basedockeragent",
"resources": {
"cpu": 1.75,
"memory": "3.5Gi"
},
"env": [
{
"name": "AZP_URL",
"secretRef": "azp-url"
},
{
"name": "AZP_TOKEN",
"secretRef": "azp-token"
},
{
"name": "AZP_POOL",
"secretRef": "azp-pool"
},
{
"name": "AZP_POOLID",
"secretRef": "azp-poolid"
}
]
}
],
"scale": {
"minReplicas": "[parameters('MIN_REPLICAS')]",
"maxReplicas": "[parameters('MAX_REPLICAS')]",
"rules": [
{
"name": "[parameters('CONTAINER_APP_NAME')]",
"custom": {
"type": "azure-pipelines",
"metadata": {
"poolID": "[parameters('azp_poolId')]",
"targetPipelinesQueueLength": "[parameters('MIN_REPLICAS')]"
},
"auth": [
{
"secretRef": "azp-token",
"triggerParameter": "personalAccessToken"
},
{
"secretRef": "azp-url",
"triggerParameter": "organizationURL"
}
]
}
}
]
}
}
}
}
]
}
Lets look at the variables the script will be using
RESOURCE_GROUP
: will be resource group to where the container app will be deployedLOG_ANALYTICS_WORKSPACE
: Name of the log analytics workspace to which the container apps metrics will be sent toLOCATION
: Location of the container appAZP_URL
: Azure DevOps URLAZP_TOKEN
: PAT Token saved in variable group at start of this blog postAZP_POOL
: Azure DevOps Agent Pool to which the container apps will useAZP_POOL_ID
: queueID of the Pool mentioned aboveREGISTRY_USERNAME
: Azure Container Registry UsernameREGISTRY_PASSWORD
: Azure Container Registry Password (Stored in variable group)REGISTRY_LOGIN_SERVER
: Azure Container Registry Login ServerIMAGE
: Image name that has been built in above stageCONTAINER_APP_NAME
: Name of container appMIN_REPLICAS
: Minimum number of container apps to be deployedMAX_REPLICAS
: Maximum number of container apps that can be scaled with KEDACPU
: CPU of container appsMEMORY
: MEMORY of container apps
- stage: deploy_container_app
dependsOn: [create_container_environment]
jobs:
- job: "deploy_app"
steps:
- task: AzureCLI@2
displayName: 'Deploy app to Container App'
inputs:
azureSubscription: 'thomasthorntoncloud'
scriptType: bash
scriptLocation: inlineScript
addSpnToEnvironment: true
inlineScript: |
#!/bin/bash
az extension add \
--source https://workerappscliextension.blob.core.windows.net/azure-cli-extension/containerapp-0.2.2-py2.py3-none-any.whl --yes
az provider register --namespace Microsoft.Web
RESOURCE_GROUP="azure-container-rg"
LOG_ANALYTICS_WORKSPACE="tamopsacrcontainersla"
CONTAINERAPPS_ENVIRONMENT="tamops-environment"
LOCATION="westeurope"
AZP_URL="https://dev.azure.com/TerraformTraining"
AZP_TOKEN=$(pattoken)
AZP_AGENT_NAME=devopsagentnew
AZP_POOL=containerapp-pool
AZP_POOL_ID=91
REGISTRY_USERNAME=tamopsacrcontainers
REGISTRY_PASSWORD=$(acrpassword)
REGISTRY_LOGIN_SERVER=tamopsacrcontainers.azurecr.io
IMAGE=tamopsacrcontainers.azurecr.io/adoagent:'$(Build.BuildId)'
CONTAINER_APP_NAME="azuredevops-agent"
MIN_REPLICAS="1"
MAX_REPLICAS="3"
CPU="1.75"
MEMORY="3.5Gi"
az deployment group create \
--resource-group $RESOURCE_GROUP \
--template-file "containerapp/containerapp.json" \
--parameters \
environment_name="$CONTAINERAPPS_ENVIRONMENT" \
location="$LOCATION" \
azp_url="$AZP_URL" \
azp_token="$AZP_TOKEN" \
azp_pool="$AZP_POOL" \
azp_poolId="$AZP_POOL_ID" \
REGISTRY_LOGIN_SERVER=$REGISTRY_LOGIN_SERVER \
REGISTRY_USERNAME=$REGISTRY_USERNAME \
REGISTRY_PASSWORD=$REGISTRY_PASSWORD \
IMAGE=$IMAGE \
MIN_REPLICAS=$MIN_REPLICAS \
MAX_REPLICAS=$MAX_REPLICAS \
CPU=$CPU \
MEMORY=$MEMORY \
CONTAINER_APP_NAME=$CONTAINER_APP_NAME
Now lets run the pipeline for the full 4 stages!

Reviewing Azure Portal, we can see the resources have been deployed successfully

Reviewing the agent pool, we can see an active Azure DevOps agent that has been created with the pipeline & Azure Container app

Running some pipelines, we can see it scaling, notice some offline? When they run the completed job they restart and a new container app appears – this is setup in the KEDA configuration. Further details on this below

Awesome! We are scaling up and down, how is this determined? Lets look at KEDA documentation. It scales using:
targetPipelinesQueueLength
– Target value for the amount of pending jobs in the queue to scale on. (Default: 1
, Optional)
Example – If one pod can handle 10 jobs, set the queue length target to 10. If the actual number of jobs in the queue is 30, the scaler scales to 3 pods.
Feel free to review the KEDA documentation further in terms of parameter list etc, its pretty awesome 🙂
Code used in the above, including full pipeline
A recommended Microsoft Learn module to further your knowledge with KEDA – Scale container applications in Azure Kubernetes Services using KEDA
I look forward to seeing Container Apps develop further – they are awesome!!
Had the sam great idea! Did you find any additional ideas except from mine? Would be interesting.
https://www.razorspoint.com/2021/11/19/scalable-container-based-azure-pipelines-pools-with-azure-container-apps/
Hi Sebastian,
Great blog! Just followed you 🙂
This concept is cool ; but after further reading its not supported as such 😦 Currently its a PoC concept and maybe never a complete reality. I was going with a Container Instance approach and then thought i’d try with Container Apps as they can use KEDA.
Due to the KEDA not having ScaledJobs – the agents can be removed at any time, I didn’t notice this during my testing/creation of running jobs (must have been lucky). Some discussion here if you are interested https://twitter.com/JorgeArteiro/status/1494324269371002882 – did you notice it much?
My next post will be similar but for Container Instance 🙂
Thanks
Thomas
Great thinking, Thomas.
I’ve used some of your ideas and the ones from Jorge in my implementation of ADO agents in Container Apps. https://github.com/antsok/ado-aca
The difference is I am using ACR to build images, and bicep to deploy.
Awesome Anton, great news!!
Hi Thomas, thanks for this it is really helpful.
By the way, where did you setup the KEDA part? I can’t see it and from my end it doesn’t restart the containers whether the job fails or succeeds.
Thank you, looking forward for your answers.
Hi Jude,
This was only ever a proof of concept and functionality of KEDA not fully supported! I recommend AKS with KEDA if you require
Thanks
Thomas