Using your VSCode dev container as a hosted Azure DevOps build agent

Devcontainers are awesome for keeping tooling consistent over the team, so what about when you need to run your build?

There is some great work already done talking about how to use these as part of a normal pipeline (shout out to Eliise!), what about if you need your build agent to be inside a virtual network in Azure?

The standard approach would be to create a VM, setup tools and join that as an Agent to Azure Devops.

As we’ve already got a definition of the tooling we need, our devcontainer, can we reuse that to simplify things?

Turns out we can, using an Azure Container Repository, Azure Container Instance and a few tweaks to our devcontainer we can spin up an agent for Devops based on the devcontainer and start using it.

To do this we need to:

  1. Add the AzureDevops Agent script to your devcontainer
  2. Build the image and push up to your Azure Container Repository following this guide
  3. Use Terraform to deploy the built container into an Azure Container Instance

The snippets below assume you already have your agent built and pushed up to your Azure Container Repository with the name your_repo_name_here.azurecr.io/devcontainer:buildagent.

It shows the .Dockerfile for the devcontainer the bash script to start a devcontainer (slight edit from doc here) and the terraform to deploy it into a VNET.

You’ll have to do some tweaks, best to treat this as a starting point. See this doc for more detailed docs on how this work.

# Very basic devcontainer, see line 15 copying in build agent start script
# https://github.com/Azure/azure-functions-docker/blob/master/host/3.0/buster/amd64/dotnet/dotnet-core-tools.Dockerfile
FROM mcr.microsoft.com/azure-functions/dotnet:3.0-dotnet3-core-tools
# 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
# Install system tools
RUN apt-get update \
&& apt-get -y install –no-install-recommends apt-utils nano unzip curl icu-devtools bash-completion jq
# Add AzureDevops build agent script
COPY ./buildagentstart.sh .
view raw .Dockerfile hosted with ❤ by GitHub
set -e
# This script comes from the following documentation
# See https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/docker?view=azure-devops
if [ -z "$AZP_URL" ]; then
echo 1>&2 "error: missing AZP_URL environment variable"
exit 1
if [ -z "$AZP_TOKEN_FILE" ]; then
if [ -z "$AZP_TOKEN" ]; then
echo 1>&2 "error: missing AZP_TOKEN environment variable"
exit 1
mkdir -p /azp/
if [ -n "$AZP_WORK" ]; then
mkdir -p "$AZP_WORK"
rm -rf /azp/agent
mkdir /azp/agent
cd /azp/agent
cleanup() {
if [ -e config.sh ]; then
print_header "Cleanup. Removing Azure Pipelines agent…"
./config.sh remove –unattended \
–auth PAT \
–token $(cat "$AZP_TOKEN_FILE")
print_header() {
echo -e "${lightcyan}$1${nocolor}"
# Let the agent ignore the token env variables
print_header "1. Determining matching Azure Pipelines agent…"
-u user:$(cat "$AZP_TOKEN_FILE") \
-H 'Accept:application/json;api-version=3.0-preview' \
if echo "$AZP_AGENT_RESPONSE" | jq . >/dev/null 2>&1; then
| jq -r '.value | map([.version.major,.version.minor,.version.patch,.downloadUrl]) | sort | .[length-1] | .[3]')
if [ -z "$AZP_AGENTPACKAGE_URL" -o "$AZP_AGENTPACKAGE_URL" == "null" ]; then
echo 1>&2 "error: 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
print_header "2. Downloading and installing Azure Pipelines agent…"
curl -LsS $AZP_AGENTPACKAGE_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 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 $!
view raw buildagentstart.sh hosted with ❤ by GitHub
variable azp_docker_image {
description = "The docker image to use when running the build agent. This defaults to a build of ./.devcontainer pushed to the ACR container"
type = string
default = "your_repo_name_here.azurecr.io/devcontainer:buildagent"
variable azp_token {
description = "The token used for the azure pipelines build agent to connect to Azure Devops"
type = string
default = ""
variable azp_url {
description = "The url of the Azure Devops instance for the agent to connect to eg: https://dev.azure.com/yourOrg"
type = string
default = "https://dev.azure.com/your_org_here"
variable "docker_registry_username" {
description = "Docker registry to be used for containers"
default = "your_repo_name_here"
variable "docker_registry_password" {
description = "Docker registry password"
variable "subnet_id" {
description = "Azure subnet ID the build agent should be deployed onto"
variable "docker_registry_url" {
description = "Docker registry url"
default = "your_repo_here.azurecr.io"
resource "azurerm_resource_group" "env" {
location = var.resource_group_location
name = var.resource_group_name
tags = var.tags
resource "azurerm_network_profile" "buildagent" {
name = "acg-profile"
location = azurerm_resource_group.env.location
resource_group_name = azurerm_resource_group.env.name
container_network_interface {
name = "acg-nic"
ip_configuration {
name = "aciipconfig"
subnet_id = var.subnet_id
resource "azurerm_container_group" "build_agent" {
name = "buildagent"
location = azurerm_resource_group.env.location
resource_group_name = azurerm_resource_group.env.name
tags = var.tags
network_profile_id = azurerm_network_profile.buildagent.id
ip_address_type = "Private"
os_type = "Linux"
image_registry_credential {
username = var.docker_registry_username
password = var.docker_registry_password
server = var.docker_registry_url
container {
name = "buildagent"
image = var.azp_docker_image
cpu = "1"
memory = "2"
commands = ["bash", "-f", "./buildagentstart.sh"]
ports {
port = 443
protocol = "TCP"
environment_variables = {
// The URL of the Azure DevOps or Azure DevOps Server instance.
AZP_URL = var.azp_url
// Personal Access Token (PAT) with Agent Pools (read, manage) scope, created by a user who has permission to configure agents, at AZP_URL.
AZP_TOKEN = var.azp_token
// Agent name (default value: the container hostname).
AZP_AGENT_NAME = local.shared_env.rg.name
// Agent pool name (default value: Default).
AZP_POOL = local.shared_env.rg.name
// Work directory (default value: _work).
AZP_WORK = "_work"
view raw main.tf hosted with ❤ by GitHub

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s