在 Docker 中运行自托管代理

Azure DevOps Services | Azure DevOps Server 2022 - Azure DevOps Server 2019

本文介绍如何在 Docker 中运行 Azure Pipelines 代理。 可以将 Azure Pipelines 中的自托管代理设置为通过 Docker 运行于 Windows Server Core(适用于 Windows 主机)或 Ubuntu 容器(适用于 Linux 主机)中。 如果要运行具有外部业务流程的代理(例如 Azure 容器实例),此功能非常有用。 在本文中,你将演练完整的容器示例,包括处理代理自我更新。

支持将 WindowsLinux 作为容器主机。 Windows 容器应在 Windows vmImage 上运行。 要在 Docker 中运行代理,需要将一些环境变量传递给 docker run,后者将代理配置为连接到 Azure Pipelines 或 Azure DevOps Server。 最后,自定义容器以满足需求。 任务和脚本可能取决于容器的 PATH 中提供的特定工具,请确保这些工具可用。

此功能需要代理版本 2.149 或更高版本。 Azure DevOps 2019 未提供兼容的代理版本。 但是,如果要运行 Docker 代理,可以将正确的代理包上传到应用程序层

Windows

启用 Hyper-V

Windows 上默认不启用 Hyper-V。 如果要在容器之间提供隔离,则必须启用 Hyper-V。 否则,用于 Windows 的 Docker 无法启动。

注意

必须在计算机上启用虚拟化。 通常默认已经启用。 但是,如果 Hyper-V 安装失败,请参阅系统文档,了解如何启用虚拟化。

安装 Docker for Windows

如果使用 Windows 10,可以安装 Docker Community Edition。 对于 Windows Server 2016,请安装 Docker Enterprise Edition

切换 Docker 以使用 Windows 容器

默认情况下,用于 Windows 的 Docker 配置为使用 Linux 容器。 要允许运行 Windows 容器,请确认用于 Windows 的 Docker 正在运行 Windows 守护程序

创建和生成 Dockerfile

接下来,创建 Dockerfile。

  1. 打开命令提示符。

  2. 新建目录:

    mkdir "C:\azp-agent-in-docker\"
    
  3. 转到此新目录:

    cd "C:\azp-agent-in-docker\"
    
  4. 将以下内容保存到名为 C:\azp-agent-in-docker\azp-agent-windows.dockerfile 的文件:

    FROM mcr.microsoft.com/windows/servercore:ltsc2022
    
    WORKDIR /azp/
    
    COPY ./start.ps1 ./
    
    CMD powershell .\start.ps1
    
  5. 将以下内容保存到 C:\azp-agent-in-docker\start.ps1

    function Print-Header ($header) {
      Write-Host "`n${header}`n" -ForegroundColor Cyan
    }
    
    if (-not (Test-Path Env:AZP_URL)) {
      Write-Error "error: missing AZP_URL environment variable"
      exit 1
    }
    
    if (-not (Test-Path Env:AZP_TOKEN_FILE)) {
      if (-not (Test-Path Env:AZP_TOKEN)) {
        Write-Error "error: missing AZP_TOKEN environment variable"
        exit 1
      }
    
      $Env:AZP_TOKEN_FILE = "\azp\.token"
      $Env:AZP_TOKEN | Out-File -FilePath $Env:AZP_TOKEN_FILE
    }
    
    Remove-Item Env:AZP_TOKEN
    
    if ((Test-Path Env:AZP_WORK) -and -not (Test-Path $Env:AZP_WORK)) {
      New-Item $Env:AZP_WORK -ItemType directory | Out-Null
    }
    
    New-Item "\azp\agent" -ItemType directory | Out-Null
    
    # Let the agent ignore the token env variables
    $Env:VSO_AGENT_IGNORE = "AZP_TOKEN,AZP_TOKEN_FILE"
    
    Set-Location agent
    
    Print-Header "1. Determining matching Azure Pipelines agent..."
    
    $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$(Get-Content ${Env:AZP_TOKEN_FILE})"))
    $package = Invoke-RestMethod -Headers @{Authorization=("Basic $base64AuthInfo")} "$(${Env:AZP_URL})/_apis/distributedtask/packages/agent?platform=win-x64&`$top=1"
    $packageUrl = $package[0].Value.downloadUrl
    
    Write-Host $packageUrl
    
    Print-Header "2. Downloading and installing Azure Pipelines agent..."
    
    $wc = New-Object System.Net.WebClient
    $wc.DownloadFile($packageUrl, "$(Get-Location)\agent.zip")
    
    Expand-Archive -Path "agent.zip" -DestinationPath "\azp\agent"
    
    try {
      Print-Header "3. Configuring Azure Pipelines agent..."
    
      .\config.cmd --unattended `
        --agent "$(if (Test-Path Env:AZP_AGENT_NAME) { ${Env:AZP_AGENT_NAME} } else { hostname })" `
        --url "$(${Env:AZP_URL})" `
        --auth PAT `
        --token "$(Get-Content ${Env:AZP_TOKEN_FILE})" `
        --pool "$(if (Test-Path Env:AZP_POOL) { ${Env:AZP_POOL} } else { 'Default' })" `
        --work "$(if (Test-Path Env:AZP_WORK) { ${Env:AZP_WORK} } else { '_work' })" `
        --replace
    
      Print-Header "4. Running Azure Pipelines agent..."
    
      .\run.cmd
    } finally {
      Print-Header "Cleanup. Removing Azure Pipelines agent..."
    
      .\config.cmd remove --unattended `
        --auth PAT `
        --token "$(Get-Content ${Env:AZP_TOKEN_FILE})"
    }
    
  6. 从该目录处运行以下命令:

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

    最终映像被标记为 azp-agent:windows

启动映像

现在已经创建了一个映像,因此可以运行容器了。 这会安装代理的最新版本、对其进行配置并运行代理。 它以指定的 Azure DevOps 或你选择的 Azure DevOps Server 实例的指定代理池(默认情况下为 Default 代理池)为目标:

docker run -e AZP_URL="<Azure DevOps instance>" -e AZP_TOKEN="<Personal Access Token>" -e AZP_POOL="<Agent Pool Name>" -e AZP_AGENT_NAME="Docker Agent - Windows" --name "azp-agent-windows" azp-agent:windows

如果遇到网络问题,可能需要指定 --network 参数。

docker run --network "Default Switch" < . . . >

如果想要使用 Ctrl + C 停止容器并删除代理,则可能需要指定 --interactive--tty 标志(或只指定 -it)。

docker run --interactive --tty < . . . >

如果希望为每个管道作业使用一个新的代理容器,请将 --once 标志传递给 run 命令。

docker run < . . . > --once

使用 --once 标志,可能需要使用容器编排系统(如 Kubernetes 或 Azure 容器实例)在工作完成时启动容器的新副本。

可以使用可选环境变量来控制代理名称,代理池和代理工作目录。

Linux

安装 Docker

根据 Linux 分发版,可以安装 Docker Community EditionDocker Enterprise Edition

创建和生成 Dockerfile

接下来,创建 Dockerfile。

  1. 打开终端。

  2. 创建新目录(推荐):

    mkdir ~/azp-agent-in-docker/
    
  3. 转到此新目录:

    cd ~/azp-agent-in-docker/
    
  4. 将以下内容保存到 ~/azp-agent-in-docker/azp-agent-linux.dockerfile

    • 对于 Alpine:

      FROM alpine
      
      RUN apk update
      RUN apk upgrade
      RUN apk add bash curl git icu-libs jq
      
      ENV TARGETARCH="linux-musl-x64"
      
      WORKDIR /azp/
      
      COPY ./start.sh ./
      RUN chmod +x ./start.sh
      
      RUN adduser -D agent
      RUN chown agent ./
      USER agent
      # Another option is to run the agent as root.
      # ENV AGENT_ALLOW_RUNASROOT="true"
      
      ENTRYPOINT [ "./start.sh" ]
      
    • 对于 Ubuntu 22.04:

      FROM ubuntu:22.04
      
      RUN apt update -y && apt upgrade -y && apt install curl git jq libicu70 -y
      
      # Also can be "linux-arm", "linux-arm64".
      ENV TARGETARCH="linux-x64"
      
      WORKDIR /azp/
      
      COPY ./start.sh ./
      RUN chmod +x ./start.sh
      
      # Create agent user and set up home directory
      RUN useradd -m -d /home/agent agent
      RUN chown -R agent:agent /azp /home/agent
      
      USER agent
      # Another option is to run the agent as root.
      # ENV AGENT_ALLOW_RUNASROOT="true"
      
      ENTRYPOINT [ "./start.sh" ]
      

    如果要作为根运行代理,请取消注释 ENV AGENT_ALLOW_RUNASROOT="true" 该行,并删除在这一行之前添加的 agent 用户。

    注意

    任务可能依赖于容器应提供的可执行文件。 例如,必须将 zipunzip 包添加到 RUN apt install -y 命令才能运行 ArchiveFilesExtractFiles 任务。 此外,由于这是代理要使用的 Linux Ubuntu 映像,因此可以根据需要自定义映像。 例如:如果需要生成 .NET 应用程序,可以按照在 Ubuntu 上安装 .NET SDK 或 .NET 运行时文档进行操作并将 .NET 应用程序添加到映像中。

  5. 将以下内容保存到 ~/azp-agent-in-docker/start.sh,确保使用 Unix 样式 (LF) 行尾:

    #!/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
    
    cleanup() {
      trap "" EXIT
    
      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 "\n${lightcyan}$1${nocolor}\n"
    }
    
    # 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
    
    trap "cleanup; exit 0" EXIT
    trap "cleanup; exit 130" INT
    trap "cleanup; exit 143" TERM
    
    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..."
    
    chmod +x ./run.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.sh "$@" & wait $!
    

    注意

    还必须使用容器编排系统(如 Kubernetes 或 Azure 容器实例)在工作完成时启动容器的新副本。

  6. 从该目录处运行以下命令:

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

    最终映像被标记为 azp-agent:linux

启动映像

现在已经创建了一个映像,因此可以运行容器了。 这会安装代理的最新版本、对其进行配置并运行代理。 它以指定的 Azure DevOps 或你选择的 Azure DevOps Server 实例的指定代理池(默认情况下为 Default 代理池)为目标:

docker run -e AZP_URL="<Azure DevOps instance>" -e AZP_TOKEN="<Personal Access Token>" -e AZP_POOL="<Agent Pool Name>" -e AZP_AGENT_NAME="Docker Agent - Linux" --name "azp-agent-linux" azp-agent:linux

如果想要使用 Ctrl + C 停止容器并删除代理,则可能需要指定 --interactive--tty 标志(或只指定 -it)。

docker run --interactive --tty < . . . >

如果希望为每个管道作业使用一个新的代理容器,请将 --once 标志传递给 run 命令。

docker run < . . . > --once

使用 --once 标志,可能需要使用容器编排系统(如 Kubernetes 或 Azure 容器实例)在工作完成时启动容器的新副本。

可以使用可选环境变量来控制代理名称,代理池和代理工作目录。

环境变量

环境变量 说明
AZP_URL Azure DevOps 或 Azure DevOps Server 实例的 URL。
AZP_TOKEN 具有“代理池(读取、管理)”范围的个人访问令牌 (PAT),由有权配置代理的用户在 AZP_URL 中创建。
AZP_AGENT_NAME 代理名称(默认值:容器主机名)。
AZP_POOL 代理池名称(默认值:Default)。
AZP_WORK 工作目录(默认值:_work)。

添加工具和自定义容器

你已创建基本生成代理。 可以扩展 Dockerfile 以包含其他工具及其依赖项,或者使用此容器作为基本层来生成自己的容器。 只需确保以下各项保持不变:

  • 脚本 start.sh 由 Dockerfile 调用。
  • 脚本 start.sh 是 Dockerfile 中的最后一个命令。
  • 确保派生容器不会移除 Dockerfile 声明的任何依赖项。

在 Docker 容器中使用 Docker

要从 Docker 容器中使用 Docker,需要绑定装载 Docker 套接字。

注意

这样做会带来严重的安全隐患。 容器中的代码现在可以在 Docker 主机上以 root 运行。

如果确定要执行此操作,请参阅 Docker.com 上的 bind mount 文档。

使用 Azure Kubernetes 服务群集

注意

请注意,由于 Docker-in-Docker 的限制,任何基于 Docker 的任务将无法在 AKS 1.19 或更高版本上运行。 在 Kubernetes 1.19 中,Docker 被替换为容器,Docker-in-Docker 变得不可用。

部署和配置 Azure Kubernetes 服务

按照快速入门:使用 Azure 门户部署 Azure Kubernetes 服务 (AKS) 群集中的步骤操作。 在此之后,PowerShell 或 Shell 控制台便可以使用 kubectl 命令行。

部署和配置 Azure 容器注册表

按照快速入门:使用 Azure 门户创建 Azure 容器注册表中的步骤操作。 之后,可以从 Azure 容器注册表推送和拉取容器。

配置机密并部署副本集

  1. 在 AKS 群集上创建机密。

    kubectl create secret generic azdevops \
      --from-literal=AZP_URL=https://dev.azure.com/yourOrg \
      --from-literal=AZP_TOKEN=YourPAT \
      --from-literal=AZP_POOL=NameOfYourPool
    
  2. 运行以下命令以将容器推送到容器注册表:

    docker push "<acr-server>/azp-agent:<tag>"
    
  3. 为现有 AKS 群集配置容器注册表集成。

    注意

    如果在 Azure 门户中有多个订阅,请先使用此命令选择一个订阅

    az account set --subscription "<subscription id or subscription name>"
    
    az aks update -n "<myAKSCluster>" -g "<myResourceGroup>" --attach-acr "<acr-name>"
    
  4. 将以下内容保存到 ~/AKS/ReplicationController.yml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: azdevops-deployment
      labels:
        app: azdevops-agent
    spec:
      replicas: 1 # here is the configuration for the actual agent always running
      selector:
        matchLabels:
          app: azdevops-agent
      template:
        metadata:
          labels:
            app: azdevops-agent
        spec:
          containers:
          - name: kubepodcreation
            image: <acr-server>/azp-agent:<tag>
            env:
              - name: AZP_URL
                valueFrom:
                  secretKeyRef:
                    name: azdevops
                    key: AZP_URL
              - name: AZP_TOKEN
                valueFrom:
                  secretKeyRef:
                    name: azdevops
                    key: AZP_TOKEN
              - name: AZP_POOL
                valueFrom:
                  secretKeyRef:
                    name: azdevops
                    key: AZP_POOL
            volumeMounts:
            - mountPath: /var/run/docker.sock
              name: docker-volume
          volumes:
          - name: docker-volume
            hostPath:
              path: /var/run/docker.sock
    

    此 Kubernetes YAML 将创建副本集和部署,其中 replicas: 1 指示群集上运行的代理数。

  5. 运行以下命令:

    kubectl apply -f ReplicationController.yml
    

现在,代理将运行 AKS 群集。

设置自定义 MTU 参数

允许为容器作业使用的网络指定 MTU 值(对于 k8s 集群中的 Docker-in-Docker 场景很有用)。

需要通过设置环境变量 AGENT_DOCKER_MTU_VALUE 来设置 MTU 值,然后重启自托管代理。 有关代理重启的详情,请参阅此处,有关为每个代理设置不同环境变量详情,请参阅此处

这允许你为作业容器设置网络参数,使用此命令类似于在配置容器网络时使用下一个命令:

-o com.docker.network.driver.mtu=AGENT_DOCKER_MTU_VALUE

在 Docker 容器中使用 Docker 装载卷

如果 Docker 容器在另一个 Docker 容器中运行,则这两个容器都使用主机的守护程序,因此所有装载路径都引用主机,而不是容器。

例如,如果要将路径从主机装载到外部 Docker 容器,可以使用以下命令:

docker run ... -v "<path-on-host>:<path-on-outer-container>" ...

如果要将路径从主机装载到内部 Docker 容器,可以使用以下命令:

docker run ... -v "<path-on-host>:<path-on-inner-container>" ...

但是,我们无法将路径从外部容器装载到内部容器中;要解决此问题,必须声明一个 ENV 变量:

docker run ... --env DIND_USER_HOME=$HOME ...

之后,可以使用以下命令从外部容器启动内部容器:

docker run ... -v "${DIND_USER_HOME}:<path-on-inner-container>" ...

常见错误

如果使用的是 Windows,并且收到以下错误:

standard_init_linux.go:178: exec user process caused "no such file or directory"

通过下载并安装 git-scm 来安装 Git Bash。

运行以下命令:

dos2unix ~/azp-agent-in-docker/Dockerfile
dos2unix ~/azp-agent-in-docker/start.sh
git add .
git commit -m "Fixed CR"
git push

重试。 应该不会再收到错误。