So you are back for more Automagical goodness? You loved the Poor Mans vRA post but now want to scale out with container goodness and still be the poor man? This post will cover how to setup / build the containers running Ansible for the Automated SOE. (Powershell Host container post to follow soon)

By using containers we can reduce the number of Ubuntu servers we need to deploy but increase the number of parallel jobs + building servers wouldn’t be cool in 2022…. so now we containerize the Devops Agent using docker cli (free tier)

This solution will mount the volume /home/vman/ansible from the Ubuntu host within the containers to have access to the build dependencies like psexec, sysmon, bginfo so that we don’t need to have these dependencies within the container image, in my case I have the servers mount an NFS share so that multiple Ubuntu servers access the same data centrally and its easier to manage. (I don’t cover that in this post)

OK so lets get started!

1).

Start by SSHing into the Ubuntu server built in the previous post, configure the account to have password less sudo (my account is vman).

sudo -i

nano /etc/sudoers.d/vman

Insert the following into nano and save.

Defaults:vman !requiretty
vman ALL=(ALL) NOPASSWD: ALL

Then change the permissions of the file as follows.

chmod 440 /etc/sudoers.d/vman

2).

Install the dependencies

apt install -y software-properties-common, python3-pip, sshpass, docker.io

3).

Create daemon.json for log rotation

nano /etc/docker/daemon.json
add in the contents below

{
  "log-driver": "local",
  "log-opts": 
 {
   "max-size": "10m",
   "max-file": "3" 
  }
 }

4).

Create the AzureContainerUbuntu folder which will store some of the dependencies needed to create the docker image

mkdir /home/vman/AzureContainerUbuntu

5).

Create the start.sh which will launch when the container starts

nano /home/vman/AzureContainerUbuntu/start.sh

Copy the following into start.sh

#!/bin/bash
set -e

ARCHITECTURE="$(arch)"
PLATFORM=$ARCHITECTURE
if [[ $PLATFORM == x86_64 ]]; then
  PLATFORM="linux-x64"
elif [[ $PLATFORM == arm* ]]; then
  PLATFORM="linux-arm"
elif [[ $PLATFORM == aarch64 ]]; then
  PLATFORM="linux-arm64"
else 
  echo 1>&2 "Unsupported architecture"
  exit 1
fi

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

echo "Setup - Determining matching Azure Pipelines agent..."
AZP_AGENT_RESPONSE=$(curl -LsS \
  -u user:$(cat "$AZP_TOKEN_FILE") \
  -H 'Accept:application/json' \
  "$AZP_URL/_apis/distributedtask/packages/agent?platform=$PLATFORM")
if echo "$AZP_AGENT_RESPONSE" | jq . >/dev/null 2>&1; then
  AZP_AGENTPACKAGE_URL=$(echo "$AZP_AGENT_RESPONSE" \
    | jq -r '.value | map([.version.major,.version.minor,.version.patch,.downloadUrl]) | sort | .[length-1] | .[3]')
fi
if [ -z "$AZP_AGENTPACKAGE_URL" -o "$AZP_AGENTPACKAGE_URL" == "null" ]; then
  echo 1>&2 "Setup - Could not determine a matching Azure Pipelines agent. Check that account '$AZP_URL' is correct and the token is valid for that account"
  exit 1
fi
echo "Setup - Latest agent package will be downloaded from $AZP_AGENTPACKAGE_URL"
echo "Setup - Downloading and unpacking Azure Pipelines agent..."
curl -LsS $AZP_AGENTPACKAGE_URL | tar -xz & wait $!
echo "Setup - Completed download and unpack"

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

source ./env.sh

print_header "1. Configuring Azure Pipelines agent..."

cleanup;

./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 "2. Running Azure Pipelines agent..."

trap 'cleanup; exit 0' EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM

# 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 "$@" &

wait $!

6).

Create the dockerfile

nano /home/vman/AzureContainerUbuntu/dockerfile

Copy the following into dockerfile

FROM ubuntu:20.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 ln -fs /usr/share/zoneinfo/UTC /etc/localtime

RUN apt-get update && apt-get install -y \
    software-properties-common \
    tzdata \
    python3-pip \
    libkrb5-dev \
    ca-certificates \
    curl \
    jq \
    git \
    iputils-ping \
    libcurl4 \
    libunwind8 \
    netcat \
    libssl1.0 \
    zip \
    unzip \
    wget \
    apt-transport-https \
    sshpass

RUN pip install ansible
RUN pip install requests
RUN pip install pyVim
RUN pip install PyVmomi
RUN pip install pywinrm
RUN pip install pywinrm[credssp]
RUN pip install pywinrm[kerberos]
RUN pip install netaddr
RUN pip install jinja2

RUN rm -rf /var/lib/apt/lists/*

RUN curl -LsS https://aka.ms/InstallAzureCLIDeb | bash \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /azp

COPY ./start.sh .
RUN chmod +x start.sh

CMD ["./start.sh"]

7). Building the Docker Image

Change to the correct directory

cd /home/vman/AzureContainerUbuntu/

Now to build the image type the following command to build the image

docker build -t dockerazureagent:latest .

Once complete we can run the following command to check if the image is ready

sudo docker image list

8). Now that the image is ready, let’s spawn some containers

Run the following command, exchanging your details below, I append _0, _1, etc… for each container so I know which one runs where.

docker run -d –restart unless-stopped –name linsrv01_0 -e AZP_URL=https://dev.azure.com/YOURORGHERE -e AZP_TOKEN=YOURAZUREPATHERE -e AZP_POOL=YOURPOOLNAMEHERE -e AZP_AGENT_NAME=linsrv01_0 -v /home/vman/:/home/vman dockerazureagent:latest

And there you have it, multiple AzureDevops Agents running on the same host allowing for more parallel pipelines without having to build more and more servers.

My post above was heavily influenced by this Microsoft post (give credit where credit is due!)

Hope you found this helpful.

vMan