Docker でセルフホステッド エージェントを実行する

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

この記事では、Docker で Azure Pipelines エージェントを実行する手順について説明します。 Docker を使って Windows Server Core (Windows ホストの場合) または Ubuntu コンテナー (Linux ホストの場合) 内で実行するセルフホステッド エージェントを Azure Pipelines に設定できます。 これは、Azure Container Instances などの外部オーケストレーションを使ってエージェントを実行する場合に便利です。 この記事では、エージェントの自己更新の処理など、完全なコンテナーの例について説明します。

WindowsLinux の両方がコンテナー ホストとしてサポートされています。 Windows コンテナーは Windows vmImage で実行する必要があります。 Docker でエージェントを実行するには、いくつかの環境変数docker run に渡します。これにより、Azure Pipelines または Azure DevOps Server に接続するようにエージェントが構成されます。 最後に、ニーズに合わせてコンテナーをカスタマイズします。 タスクとスクリプトによっては、コンテナーの PATH で使用できる特定のツールに依存する場合があり、これらのツールが使用可能であることを確認するのはユーザーの責任です。

この機能には、エージェント バージョン 2.149 以降が必要です。 Azure DevOps 2019 には、互換性のあるエージェント バージョンが付属していませんでした。 ただし、Docker エージェントを実行する場合は、適切なエージェント パッケージをアプリケーション層にアップロードできます。

Windows

Hyper-V の有効化

Hyper-V は、Windows では既定では有効になっていません。 コンテナー間の分離を提供する場合は、Hyper-V を有効にする必要があります。 それ以外の場合、Docker for Windows は起動しません。

注意

マシンで仮想化を有効にする必要があります。 これは通常、既定で有効になっています。 ただし、Hyper-V のインストールが失敗する場合は、システムのドキュメントで仮想化を有効にする方法を参照してください。

Docker for Windows のインストール

Windows 10を使っている場合は、Docker Community Edition をインストールできます。 Windows Server 2016 の場合は、Docker Enterprise Edition をインストールします。

Windows コンテナーを使用するように Docker を切り替える

既定では、Docker for Windows は Linux コンテナーを使用するように構成されています。 Windows コンテナーの実行を許可するには、Docker for Windows が 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 Container Instances などのコンテナー オーケストレーション システムを使用して、ジョブの完了時にコンテナーの新しいコピーを開始できます。

オプションの環境変数を使用して、エージェント名、エージェント プール、およびエージェント作業ディレクトリを制御できます。

Linux

Docker のインストール

Linux ディストリビューションに応じて、Docker Community Edition または Docker 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 ユーザーを追加して削除します。

    Note

    タスクは、コンテナーが提供すると予想される実行可能ファイルに依存する場合があります。 たとえば、ArchiveFiles および ExtractFiles タスクを実行するために、zip および unzip パッケージを RUN apt install -y コマンドに追加する必要があります。 また、これはエージェントが使用する Linux Ubuntu イメージであるため、必要に応じてイメージをカスタマイズできます。 例: .NET アプリケーションをビルドする必要がある場合は、「Ubuntu に .NET SDK または .NET ランタイムをインストールする」のドキュメントに従って、これをイメージに追加できます。

  5. 必ず Unix スタイル (LF) の改行コードを使って、次の内容を ~/azp-agent-in-docker/start.sh に保存します。

    #!/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 Container Instances などのコンテナー オーケストレーション システムを使って、作業が完了したときにコンテナーの新しいコピーを開始する必要があります。

  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 Container Instances などのコンテナー オーケストレーション システムを使用して、ジョブの完了時にコンテナーの新しいコピーを開始できます。

オプションの環境変数を使用して、エージェント名、エージェント プール、およびエージェント作業ディレクトリを制御できます。

環境変数

環境変数 説明
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 ホストでルートとして実行できるようになりました。

これを行う必要があることが確実な場合は、Docker.com のバインド マウントに関するドキュメントを参照してください。

Azure Kubernetes Service クラスターを使用する

注意事項

docker in docker の制限により、Docker ベースのタスクは AKS 1.19 以降では機能しないことに注意してください。 Docker は Kubernetes 1.19 にコンテナー化されたものに置き換えられ、Docker-in-Docker は使用できなくなりました。

Azure Kubernetes Service をデプロイし構成する

クイック スタート: Azure portal を使用して Azure Kubernetes Service (AKS) クラスターをデプロイする」の手順に従います。 その後、PowerShell または Shell コンソールで kubectl コマンド ラインを使用できます。

Azure Container Registry をデプロイし構成する

クイック スタート: Azure portal を使用して Azure コンテナー レジストリを作成する」の手順に従います。 その後、Azure Container Registry からコンテナーをプッシュおよびプルできます。

シークレットを構成し、レプリカ セットをデプロイする

  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. このコマンドを実行して、コンテナーを Container Registry にプッシュします。

    docker push "<acr-server>/azp-agent:<tag>"
    
  3. 既存の AKS クラスターの Container Registry 統合を構成します。

    注意

    Azure Portal で複数のサブスクリプションを使っている場合は、最初にこのコマンドを使ってサブスクリプションを選んでください

    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

やり直してください。 エラーは表示されなくなります。