Manage Thousands of Cloud VDI by deploying Docker Container as the Azure DevOps Pipeline Agent

Nowadays, large enterprises need to manage thousands of VDI for their customers in the public cloud, In the cloud everything is software-defined, so it’s absolutely necessary to run the entire VDI infrastructure as software.
Also, DaaS (Desktop As a Service) is no longer a buzzword it’s became a reality, Citrix has recently renamed their CVAD services to DaaS, this is going to continue for years now as on-premises VDI deployment is gradually phasing out due to the following reasons.
- Higher Cost
- Lack of agility
- Difficult to maintain compliance and standards
- Prone to human errors.
The benefits of deploying the VDI workload as the software will have the following advantages.
- Speed
- Consistency
- Repeatability
- Reliability
- Lower Cost
But when your VDI resources number in cloud are very high, you need to do a lot of changes every month and you need to deploy the changes continuously, if you are using any DevOps platform for orchestration you need many private agents to run your code in thousands of VDI across the cloud.
This is never possible if you choose VM’s as a private agent to run your pipelines because you can’t spin up multiple VM’s when there are many pipelines running together. for this scenario when you need to run multiple pipelines together you need more private agents. We have faced similar situation and to overcome that we have chosen docker container as a private agent to run the pipelines.
We have tested docker containers as a private agent in Azure DevOps and private runner in Gitlab, and we generally spin up a new docker container every time a new pipeline will run.
Today I will show you how you can deploy docker container as a private agent for Azure DevOps.
I have chosen Ubuntu Linux as the docker platform for this exercise.
Here are the step-by-step configuration which you can follow to implement this.
Install Docker Engine on Ubuntu
Prerequisites
OS requirements
To install Docker Engine, you need the 64-bit version of one of these Ubuntu versions:
- Ubuntu Impish 21.10
- Ubuntu Hirsute 21.04
- Ubuntu Focal 20.04 (LTS)
- Ubuntu Bionic 18.04 (LTS)
Docker Engine is supported on x86_64 (or amd64), armhf, arm64, and s390x architectures.
Check your OS version
cat /etc/*release

Our version is Ubuntu Focal version 20.0.4.3 LTS
Uninstall old versions
Older versions of Docker were called docker, docker.io, or docker-engine. If these are installed, uninstall them:
sudo apt-get remove docker docker-engine docker.io containerd runc
Since there is no docker package installed so I can see that.

Install using the repository
Before you install Docker Engine for the first time on a new host machine, you need to set up the Docker repository. Afterward, you can install and update Docker from the repository.
Set up the repository
Update the apt package index and install packages to allow apt to use a repository over HTTPS:
sudo apt-get update

Run this
sudo apt-get install \ ca-certificates \ curl \ gnupg \ lsb-release

Add Docker’s official GPG key:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
![]()
Use the following command to set up the stable repository. To add the nightly or test repository, add the word nightly or test (or both) after the word stable in the commands below. Learn about nightly and test channels.
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker Engine
Update the apt package index, and install the latest version of Docker Engine and container id, or go to the next step to install a specific version:
sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io

Once you type Y it will show like this

Verify that Docker Engine is installed correctly by running the hello-world image.

Manage Docker as a non-root user
If you don’t want to preface the docker command with sudo, create a Unix group called docker and add users to it. When the Docker daemon starts, it creates a Unix socket accessible by members of the docker group.
Create the docker group.
sudo groupadd docker
![]()
Add your user to the docker group.
sudo usermod -aG docker $USER
![]()
Log out and log back in so that your group membership is re-evaluated.
Verify that you can run docker commands without sudo.
docker run hello-world

Create and build the Dockerfile
Create a new directory (recommended):
mkdir ~/dockeragent

Change directories to this new directory:
![]()
Save the following content to ~/dockeragent/Dockerfile, Your image file content should look like the below.
FROM ubuntu:18.04
# To make it easier for build and release pipelines to run apt-get,
# configure apt to not require confirmation (assume the -y argument by default)
ENV DEBIAN_FRONTEND=noninteractive
RUN echo "APT::Get::Assume-Yes \"true\";" > /etc/apt/apt.conf.d/90assumeyes
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
jq \
git \
iputils-ping \
libcurl4 \
libicu60 \
libunwind8 \
netcat \
libssl1.0 \
&& rm -rf /var/lib/apt/lists/*
RUN curl -LsS https://aka.ms/InstallAzureCLIDeb | bash \
&& rm -rf /var/lib/apt/lists/*
# Update the list of packages
RUN apt-get update
# Install pre-requisite packages.
RUN apt-get install -y wget apt-transport-https software-properties-common
# Download the Microsoft repository GPG keys
RUN wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb
# Register the Microsoft repository GPG keys
RUN dpkg -i packages-microsoft-prod.deb
# Update the list of packages after we added packages.microsoft.com
RUN apt-get update
# Install PowerShell
RUN apt-get install -y powershell
# Install Azure Module
RUN pwsh -c "&{Install-Module -Name Az -AllowClobber -Scope AllUsers -Force}"
# Can be 'linux-x64', 'linux-arm64', 'linux-arm', 'rhel.6-x64'.
ENV TARGETARCH=linux-x64
WORKDIR /azp
COPY ./start.sh .
RUN chmod +x start.sh
![]()
Open the file with VI editor, and you can view the changes


Save the following content to ~/dockeragent/start.sh, making sure to use Unix-style (LF) line endings, your start.sh should look like below.
#!/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 [ -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..."
curl -LsS $AZP_AGENT_PACKAGE_LATEST_URL | tar -xz & wait $!
source ./env.sh
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-docker.sh
# 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-docker.sh "$@" & wait $!
vi start.sh
![]()
Open vi editor and make the changes and cross check the file with cat command 


Run the following command within that directory:
docker build -t dockeragent:latest .
This command builds the Dockerfile in the current directory.
The final image is tagged dockeragent:latest. You can easily run it in a container as dockeragent, because the latest tag is the default if no tag is specified.
Check the image

Start the image
Now that you have created an image, you can run a container.
- Open a terminal.
- Run the container. This installs the latest version of the agent, configures it, and runs the agent. It targets the Default pool of a specified Azure DevOps or Azure DevOps Server instance of your choice:
docker run -e AZP_URL=https://dev.azure.com/myOrg/ -e AZP_TOKEN=2746sb7lxqurq2igb7mvxj -e AZP_AGENT_NAME=mydockeragent dockeragent:latest


Go to Azure DevOps

As you see agent is running now. The agent name is mydockeragent which I have given, however you can give any name of your choice.
If you want to stop the agent press Ctrl + C

The agent will stop immediately.
If you want a fresh agent container for every pipeline job, pass the --once flag to the run command.
Once you stop an agent you can go back to azure devops to see that no agent is now running against your default pool.
You can see that no agent is running as below.

That’s it, I hope you have understood how to configure docker agents to run the Azure DevOps pipelines. You can run docker agent every time a new pipeline is triggered. I think you have also noticed that I have installed PowerShell core and Azure PowerShell Modules in the docker container which make your jobs easy if you are using PowerShell core. If you are using other extensions like terraform kindly add that in your container based on your pipeline code. For Azure Virtual Desktop you can use a Linux container but for Citrix SDK please use a windows container. However concept is same. If you are not using Azure DevOps as your CI/CD platform and if you are using Gitlab, there are more options, Gitlab users can run GitLab Runner inside a docker container as well, I will show that later. For Gitlab users you can run the runner into a Kubernetes cluster. The official way of deploying a GitLab Runner instance into your Kubernetes cluster is by using the gitlab-runner Helm chart.
This chart configures GitLab Runner to:
- Run using the Kubernetes executor for GitLab Runner.
- For each new job it receives from GitLab CI/CD, it will provision a new pod within the specified namespace to run it.
I will show this above configuration in a different post.
That’s all about today. That’s for your time.
Stay tuned and you have a great day ahead.