AZ-400: Designing and Implementing Microsoft DevOps Solutions

Design and Implement Pipelines

Exploring Containerized Agents

In this lesson, you will learn how to create a containerized self-hosted agent for Azure DevOps. Unlike previous lessons where self-hosted agents ran on physical machines, here you will deploy an agent inside a Windows container. Containerized agents offer enhanced scalability, consistency, isolation, and portability. This guide focuses on setting up a Windows container in a Windows environment.

Pre-requisite

Before you begin, ensure that Docker is running in Windows Containers mode. Right-click the Docker icon and choose “Switch to Windows Containers.”


Overview of Self-Hosted vs. Containerized Agents

Self-hosted agents are installed on local machines and offer a cost-effective, customizable build environment for Azure DevOps. Containerizing the agent provides a consistent and encapsulated environment while making it easier to scale resources as needed.


Setting Up the Containerized Agent

Assuming you already have an Azure DevOps project (for example, an ASP.NET web application in the KodeKloud GIFs repository) that builds on your local machine, this guide will show you how to integrate a containerized agent alongside your local agent (e.g., DigitalStorm).

Official Microsoft documentation offers additional context on setting up self-hosted agents in Docker. The process involves creating a Dockerfile and an accompanying PowerShell startup script.


Creating the Dockerfile

Start by creating a working directory and a Dockerfile. Execute the following commands in PowerShell:

mkdir "C:\azp-agent-in-docker\"
cd "C:\azp-agent-in-docker\"

Next, create a Dockerfile (for instance, named azpagentwindows.dockerfile) with the content below:

FROM mcr.microsoft.com/windows/servercore:ltsc2022
WORKDIR /azp/
COPY ./start.ps1 ./
CMD powershell .\start.ps1

This Dockerfile pulls the Windows Server Core LTSC 2022 image, sets the working directory, copies the startup script, and runs it.


The PowerShell Startup Script (start.ps1)

The PowerShell script automates the process of setting up, downloading, and configuring the Azure Pipelines agent within the container. It performs the following actions:

  • Validates required environment variables.
  • Downloads the agent package.
  • Configures and installs the agent.
  • Runs the agent.

Below is the full version of the startup script:

function Print-Header {
    param (
        [string]$header
    )
    Write-Host "`n$header" -ForegroundColor Cyan
}

# Validate required environment variables.
if (-not $env:AZP_URL) {
    Write-Error "Error: missing AZP_URL environment variable"
    exit 1
}

if (-not ($env:AZP_TOKEN_FILE -or $env:AZP_TOKEN)) {
    Write-Error "Error: missing AZP_TOKEN environment variable"
    exit 1
}

# If only AZP_TOKEN is provided, define a token file path and extract its content.
if (-not $env:AZP_TOKEN_FILE) {
    $env:AZP_TOKEN_FILE = "C:\azp.token"
    $env:AZP_TOKEN | Out-File -FilePath $env:AZP_TOKEN_FILE -Encoding ascii
}

# Prepare work directory.
if ($env:AZP_WORK -and -not (Test-Path $env:AZP_WORK)) {
    New-Item $env:AZP_WORK -ItemType Directory | Out-Null
}

# Let the agent ignore the token environment variables.
$env:VSO_AGENT_IGNORE = "AZP_TOKEN;AZP_TOKEN_FILE"

# Change directory to agent folder (create if not exists).
if (-not (Test-Path "azp\agent")) {
    New-Item -ItemType Directory -Path "azp\agent" | Out-Null
}
Set-Location "azp\agent"

Print-Header "1. Determining matching Azure Pipelines agent..."

# Prepare basic authentication info.
$tokenContent = Get-Content $env:AZP_TOKEN_FILE
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$tokenContent"))
$packageUrl = "$($env:AZP_URL)/_apis/distributedtask/packages/agent"

Write-Host "Agent package URL: $packageUrl"

# Download agent package.
$wc = New-Object System.Net.WebClient
$wc.Headers["Authorization"] = "Basic $base64AuthInfo"
$wc.DownloadFile($packageUrl, "$(Get-Location)\agent.zip")

Print-Header "2. Downloading and Installing Azure Pipelines agent..."
$wc.DownloadFile($packageUrl, "$(Get-Location)\agent.zip")
Expand-Archive -Path "agent.zip" -DestinationPath "."

try {
    Print-Header "3. Configuring Azure Pipelines agent..."
    $agentName = if ($env:AZP_AGENT_NAME) { $env:AZP_AGENT_NAME } else { hostname }
    & .\config.cmd --unattended --agent "$agentName" --url "$env:AZP_URL" --auth pat --token (Get-Content $env:AZP_TOKEN_FILE)
    
    Print-Header "4. Running Azure Pipelines agent..."
    & .\run.cmd
} catch {
    Write-Error "An error occurred during agent configuration: $_"
}

Building the Container Image

With your Dockerfile and PowerShell startup script in place, build your container image by running the following command from your project folder:

docker build --tag "azp-agent:windows" --file "./azp-agent-windows.dockerfile" .

During the build process, you will observe steps such as downloading the base image, copying the start.ps1 file, and setting the working directory.


Running the Containerized Agent

After successfully building the image, run your container by passing the required environment variables:

docker run -e AZP_URL="%AZP_URL%" -e AZP_TOKEN="%AZP_TOKEN%" -e AZP_POOL="Default" -e AZP_AGENT_NAME="Docker Agent - Windows" --name "azp-agent-windows" azp-agent:windows

This command sends your Azure DevOps URL, personal access token, pool name, and agent name into the container. The agent then connects to Azure DevOps, downloads its necessary package, configures itself, and starts listening for jobs.

A sample output might appear as:

1. Determining matching Azure Pipelines agent...
https://vstsagentpackage.azureedge.net/agent/3.244.1/vsts-agent-win-x64-3.244.1.zip
2. Downloading and installing Azure Pipelines agent...
...
Successfully added the agent
Testing agent connection.
2024-09-25 17:55:29Z: Settings Saved.
4. Running Azure Pipelines agent...
Scanning for tool capabilities.

Once connected, navigate to your Agent Pools (e.g., Default pool) in Azure DevOps to see the new "Docker Agent - Windows" online alongside any other agents.

The image shows a web interface for Azure DevOps, specifically the "Agent pools" settings page, listing available agent pools such as "Azure Pipelines" and "Default."


Using the Containerized Agent in a Pipeline

A Basic Pipeline Example

To verify that your containerized agent works as expected, create a simple Azure Pipeline using this minimal YAML configuration:

# Starter pipeline
trigger:
- master

pool:
  name: Default
  vmImage: ubuntu-latest

steps:
- script: echo Hello, world!
  displayName: 'Run a one-line script'

- script: |
    echo Add other tasks to build, test, and deploy your project.
    echo See https://aka.ms/yaml
  displayName: 'Run a multi-line script'

When you run this pipeline for the first time on your containerized agent, you may be prompted to permit resource access. After approval, the agent checks out your code and executes the specified scripts. Verify in Azure DevOps that the "Docker Agent - Windows" appears online in the Default pool.

The image shows a web interface for Azure DevOps, displaying the "Agent pools" settings with two agents listed as online and idle. The sidebar includes various project settings and pipeline options.

Pipeline for an ASP.NET Core Application

For a more typical use case, set up a pipeline to build an ASP.NET Core application using this YAML configuration:

# ASP.NET Core (.NET Framework)
trigger:
- master

pool:
  name: Default
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    msbuildArguments: '/p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildOutputFolder="$(build.ArtifactStagingDirectory)"'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'

When this pipeline runs, the containerized agent will try to build your ASP.NET project. If build tools and SDKs are missing (as the Server Core image is minimal by default), the job may remain queued. This scenario leads to the next topic.


Enhancing the Container for .NET Builds

The base Windows Server Core image does not include the tools required for .NET builds. To resolve this, create an enhanced Docker image based on Windows Server Core with the .NET SDK pre-installed. In addition, install Visual Studio Build Tools and configure the necessary environment paths.

Below is the final version of the enhanced Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:8.0-windowsservercore-ltsc2022

WORKDIR /azp/

## Set up Environment for Build
ENV BUILD_PATH="C:\\BuildTools"
ENV DOTNET_PATH="C:\\dotnet"
ENV MSBuildSDKsPath="C:\\Program Files\\dotnet\\sdk\\8.0.402"

# Install Visual Studio Build Tools (similar to buildtools2019)
SHELL ["powershell", "-Command"]
RUN Invoke-WebRequest -UseBasicParsing -Uri "https://aka.ms/vs/17/release/vs_buildtools.exe" -OutFile vs_buildtools.exe; \
    .\vs_buildtools.exe --quiet --wait --norestart --nocache `
        --installPath C:\BuildTools `
        --add Microsoft.VisualStudio.Workload.VCTools `
        --add Microsoft.VisualStudio.Workload.MSBuildTools `
        --add Microsoft.VisualStudio.Workload.NetCoreBuildTools `
        --add Microsoft.VisualStudio.Workload.WebBuildTools `
        --add Microsoft.VisualStudio.Workload.NetWeb `
        --add Microsoft.VisualStudio.Web.BuildTools.ComponentGroup `
        --add Microsoft.NetCore.Component.SDK

RUN setx /M PATH "$env:PATH;$env:BUILD_PATH;"
RUN setx /M PATH "$env:PATH;$env:DOTNET_PATH;"
RUN setx /M PATH "$env:PATH;$env:MSBuildSDKsPath;"

# Revert shell back to CMD
SHELL ["cmd", "/S", "/C"]

COPY ./start.ps1 ./

CMD powershell ./start.ps1

This enhanced Dockerfile leverages the .NET SDK image, installs additional build tools, and configures the environment paths appropriately. With this image, your containerized agent will have the necessary dependencies to build .NET applications, including ASP.NET Core projects.

Once the image is built, run your container as before:

docker build --tag "azp-agent:windows" --file "./azp-agent-windows.dockerfile" .
docker run -e AZP_URL="%AZP_URL%" -e AZP_TOKEN="%AZP_TOKEN%" -e AZP_POOL="Default" -e AZP_AGENT_NAME="Docker Agent - Windows" --name "azp-agent-windows" azp-agent:windows

During execution, the agent downloads, configures, and begins processing jobs with tasks like NuGet restore, VSBuild, and VSTest. An excerpt from the build log might look like:

Starting: VSBuild
==============================================================================
Task          : Visual Studio build
Description   : Build with MSBuild and set the Visual Studio version property
...
Build started 9/25/2024 11:36:59 AM
Validating configuration...
Building solution configuration "Release|Any CPU".
...

By using this enhanced containerized agent, you can seamlessly run two types of agents in your Azure DevOps environment: your standard self-hosted agent (e.g., DigitalStorm) and your fully capable container-based agent.

The image shows an Azure DevOps Pipelines dashboard with a recently run pipeline named "KodeKloudGifts." The pipeline has a successful status and was run 2 minutes ago.


Final Thoughts

Containerized agents significantly enhance scalability and portability in your Azure DevOps pipelines. However, setting them up—especially on Windows—requires careful configuration to ensure all necessary build dependencies are included. Using a Windows Server Core image enriched with the .NET SDK and Visual Studio Build Tools provides a robust solution for building complex applications like ASP.NET Core projects.

For further details, you may check out Azure DevOps Documentation.

Happy building!

Watch Video

Watch video content

Previous
Using Build Trigger Rules