Skip to main content

Run an Azure Pipelines build agent in WSL2

At my current employer we are still using a local build server to host our Azure Pipeline agents and run our builds. Having multiple agents running on the same machine works most of the time as most frameworks and libraries we depend on allow multiple side-by-side installations. Unfortunately there is one framework that doesn't like this; node.js. So far, we have worked around this by using NVM(Node Version Manager) to switch between node.js version. Of course this only works as long as no 2 builds are running at the same time that use a different node.js version.

We did a previous attempt to fix this problem by running docker on our build server and host separate build agents in a container. But it introduced too much overhead on our build server and we never succeeded in getting it stable.

As we had to move our build environment to a new server, we thought it would be a good time to finally fix this problem; this time by running multiple Linux distributions using WSL2 instead of a docker image.

Remark: Having a docker image would still be beneficial as it would help us keep the environments in sync and automate the installation of all the dependencies.

Prerequisites

Before we begin, ensure you have the following prerequisites in place:

  • Windows Server 2022 or higher
  • WSL2 installed and configured
  • A Linux distribution installed in WSL2 (Ubuntu 20.04 or 22.04 recommended)
  • An Azure DevOps organization with appropriate permissions to create agent pools
  • Basic familiarity with Linux command line operations

Setting up WSL2 on the server

If you haven't already set up WSL2, here's a quick overview of the process. Open PowerShell as Administrator and run:

wsl --install

This command installs WSL2 with Ubuntu as the default distribution. After installation, restart the server.

On our build server, WSL was already installed and I got the following message when trying to run this command:

PS C:\Users\bawu> wsl --install
Windows Subsystem for Linux is already installed.
The following is a list of valid distributions that can be installed.
Install using 'wsl --install -d <Distro>'.

NAME                            FRIENDLY NAME
Ubuntu                          Ubuntu
Debian                          Debian GNU/Linux
kali-linux                      Kali Linux Rolling
Ubuntu-18.04                    Ubuntu 18.04 LTS
Ubuntu-20.04                    Ubuntu 20.04 LTS
Ubuntu-22.04                    Ubuntu 22.04 LTS
Ubuntu-24.04                    Ubuntu 24.04 LTS
OracleLinux_7_9                 Oracle Linux 7.9
OracleLinux_8_10                Oracle Linux 8.10
OracleLinux_9_5                 Oracle Linux 9.5
openSUSE-Leap-15.6              openSUSE Leap 15.6
SUSE-Linux-Enterprise-15-SP6    SUSE Linux Enterprise 15 SP6
openSUSE-Tumbleweed             openSUSE Tumbleweed

I tried to run the command with a specific distro. However this failed with the error message below: 
PS C:\Users\bawu> wsl --install -d Ubuntu
Installing: Ubuntu
An error occurred during installation. Distribution Name: 'Ubuntu' Error Code: 0x8000ffff

I was able to fix it by first running an update:

PS C:\Users\bawu> wsl --update
Downloading: Windows Subsystem for Linux
Installing: Windows Subsystem for Linux
Windows Subsystem for Linux has been installed.
PS C:\Users\bawu> wsl --install -d Ubuntu
WSL is finishing an upgrade...
Installing Windows optional component: VirtualMachinePlatform

Deployment Image Servicing and Management tool
Version: 10.0.20348.2849

Image Version: 10.0.20348.3932

Enabling feature(s)
[==========================100.0%==========================]
The operation completed successfully.
The requested operation is successful. Changes will not be effective until the system is rebooted.

Installing the Linux agent in WSL2

Now let's install and configure the Azure Pipelines agent within WSL2. Start by updating your WSL2 system packages:

sudo apt update && sudo apt upgrade -y

Create a dedicated directory for the agent and navigate to it:

mkdir ~/agent && cd ~/agent

Download the latest agent package. You can find the current download URL in your Azure DevOps organization under Project Settings > Agent pools > Download agent:

wget https://download.agent.dev.azure.com/agent/3.238.0/vsts-agent-linux-x64-3.238.0.tar.gz

Extract the agent package:

tar zxvf vsts-agent-linux-x64-3.238.0.tar.gz

Configuring the agent

Before configuring the agent, you'll need to create a Personal Access Token (PAT) in Azure DevOps. Navigate to your Azure DevOps organization, click on your profile picture, select "Personal access tokens," and create a new token with "Agent Pools (read, manage)" permissions.

Run the configuration script:

./config.sh

The configuration script will prompt you for several pieces of information:

  • Server URL: Your Azure DevOps organization URL (e.g., https://servername/tfs/defaultcollection)
    • Remark: Specify the server URL including the collection as the generated PAT will always be scoped at the collection level. Otherwise you will get an VS30063 error message.
  • Authentication type: Choose PAT (Personal Access Token)
  • Personal access token: Enter the PAT you created earlier
  • Agent pool: Choose an existing pool or create a new one (default is usually fine)
  • Agent name: Give your agent a descriptive name (e.g., "MrBlue", we are Reservoir Dogs fans)
  • Work folder: Accept the default or specify a custom path

The configuration process will register your agent with Azure DevOps and prepare it for running builds.



Now is also a good time to install any other dependencies you need on this WSL2 instance:

First add the Microsoft Package reference repository:

wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb

sudo dpkg -i packages-microsoft-prod.deb

rm packages-microsoft-prod.deb

Afterwards you can install the SDK on the agent:

sudo apt-get update && \ sudo apt-get install -y dotnet-sdk-9.0

Running the agent

Once the configuration process has completed, we can run the agent:

./run.sh

The agent will scan for available tool capabilities and after that it should appear in your configured pool:

Right now, our agent is running in interactive mode. This is good to test if everything works, but requires us to have an active user session.

To have the agent run unattended, we add a scheduled task to our Windows Server. This task will start a WSL2 distro on server startup and run the agent as a service.

Create a new Scheduled task

Set the Security options to Run whether user is logged in or not.

Go to Triggers and create a new Trigger. Set Begin the task to At startup.

On the Actions tab, create a new Action.

Set the Action to Start a program

Set the Program/Script to wsl>

Set the arguments to -d Ubuntu /home/<serviceaccount>/agent/run.sh

 

Now your build agent should start automatically when the server boots up.

Create a build pipeline that targets our Linux agent

If you want to create a pipeline that should run on the Linux agent, you can specify a demand with the Agent.OS set to Linux:

Optimizing performance

To get the best performance from your WSL2 agent, consider these optimizations:

Memory Configuration: Create or modify /etc/wsl.conf in your WSL2 distribution to allocate appropriate resources:

[wsl2]
memory=8GB
processors=4
swap=2GB

File System Performance: Store your source code and build artifacts on the Linux file system rather than accessing Windows drives through /mnt/c/. This provides significantly better I/O performance.

More information

WSL 2 distros are now supported on Windows Server - Windows Command Line

Install Linux Subsystem on Windows Server | Microsoft Learn

Switch between node versions on Windows

Advanced settings configuration in WSL | Microsoft Learn

Why am I getting error code 0x8000ffff when I try to install Ubuntu on my WSL distribution? - Microsoft Q&A

Popular posts from this blog

Azure DevOps/ GitHub emoji

I’m really bad at remembering emoji’s. So here is cheat sheet with all emoji’s that can be used in tools that support the github emoji markdown markup: All credits go to rcaviers who created this list.

Kubernetes–Limit your environmental impact

Reducing the carbon footprint and CO2 emission of our (cloud) workloads, is a responsibility of all of us. If you are running a Kubernetes cluster, have a look at Kube-Green . kube-green is a simple Kubernetes operator that automatically shuts down (some of) your pods when you don't need them. A single pod produces about 11 Kg CO2eq per year( here the calculation). Reason enough to give it a try! Installing kube-green in your cluster The easiest way to install the operator in your cluster is through kubectl. We first need to install a cert-manager: kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.5/cert-manager.yaml Remark: Wait a minute before you continue as it can take some time before the cert-manager is up & running inside your cluster. Now we can install the kube-green operator: kubectl apply -f https://github.com/kube-green/kube-green/releases/latest/download/kube-green.yaml Now in the namespace where we want t...

.NET 8–Keyed/Named Services

A feature that a lot of IoC container libraries support but that was missing in the default DI container provided by Microsoft is the support for Keyed or Named Services. This feature allows you to register the same type multiple times using different names, allowing you to resolve a specific instance based on the circumstances. Although there is some controversy if supporting this feature is a good idea or not, it certainly can be handy. To support this feature a new interface IKeyedServiceProvider got introduced in .NET 8 providing 2 new methods on our ServiceProvider instance: object? GetKeyedService(Type serviceType, object? serviceKey); object GetRequiredKeyedService(Type serviceType, object? serviceKey); To use it, we need to register our service using one of the new extension methods: Resolving the service can be done either through the FromKeyedServices attribute: or by injecting the IKeyedServiceProvider interface and calling the GetRequiredKeyedServic...