Azure DevOps のセルフホステッド Linux エージェントを Docker で起動してその中で Docker ビルドしてみた

## 背景と目的

<https://qiita.com/mnrst/items/e48d6c5b54385de5447a>

以前こちらの記事を書きました。今回はセルフホステッド Linux エージェントを Docker で起動して、その Docker の中で Docker ビルドが出来る環境を用意して試してみました。

## 前提条件

コマンドの実施環境は、Mac + Azure CLI です。

```bash:bash
$ sw_vers
ProductName:    macOS
ProductVersion: 12.1
BuildVersion:   21C52

$ az version
{
  "azure-cli": "2.32.0",
  "azure-cli-core": "2.32.0",
  "azure-cli-telemetry": "1.0.6",
  "extensions": {}
}
```

## Azure DevOps 組織にプロジェクトを作成

```bash:bash
# リソースのリージョンを設定します
region=japaneast

# Azure DevOps 組織名を設定します（サインアップした組織名です）
prefix=mnrdevopsagent

# Azure CLI に拡張機能を追加します
az extension add \
  --name azure-devops

# 拡張機能に Azure DevOps 組織をデフォルト設定します
az devops configure \
  --defaults organization=https://dev.azure.com/${prefix}/

# Azure DevOps のプロジェクトを作成します
az devops project create \
  --name ${prefix}

# 作成したプロジェクトに簡単な Dockerfile を登録します
mkdir ${prefix} && cd ${prefix}
echo "FROM nginx" > Dockerfile
git init
git add Dockerfile
git commit -m "first commit"
git remote add origin https://${prefix}@dev.azure.com/${prefix}/${prefix}/_git/${prefix}
git push -u origin --all
```

## プロジェクトに Azure Pipeline を作成

<https://docs.microsoft.com/ja-jp/azure/devops/pipelines/ecosystems/containers/build-image?view=azure-devops>

こちらを参考にパイプラインを作成します。

```bash:bash
# azure-pipelines.yml を作成
cat <<EOF > azure-pipelines.yml
trigger:
- master

pool:
  vmImage: ubuntu-latest

variables:
  imageName: ${prefix}

steps:
- task: Docker@2
  displayName: Build an image
  inputs:
    repository: \$(imageName)
    command: build
    Dockerfile: Dockerfile
EOF

# azure-pipelines.yml を Git リポジトリに登録します
git add azure-pipelines.yml
git commit -m update
git push

# パイプラインを作成します
az pipelines create \
  --name ${prefix}-pipeline \
  --yml-path azure-pipelines.yml

# パイプラインの実行状況を確認します
az pipelines runs list \
  --output table

# 最初のパイプラインの詳細を確認します
az pipelines runs show \
  --id 1 \
  --output table

# ブラウザーでビルド結果ページを開きます
az pipelines runs show \
  --id 1 \
  --open
```

## セルフホステッド検証用の Linux VM を作成

```bash:bash
# リソースグループを作成します
az group create \
  --name ${prefix}-rg \
  --location $region

# SSH キーペアをファイル名を指定して作成します
ssh-keygen -m PEM -t rsa -b 4096 \
  -f ${prefix}

# VM を作成します
az vm create \
  --resource-group ${prefix}-rg \
  --name ${prefix}-vm \
  --os-disk-name ${prefix}-vmOSDisk \
  --image UbuntuLTS \
  --admin-username azureuser \
  --ssh-key-value ${prefix}.pub \
  --size Standard_A2_v2 \
  --nsg-rule NONE \
  --public-ip-address-dns-name ${prefix} \
  --storage-sku Standard_LRS

# NSG に自分の IP アドレスから SSH 接続出来るようにします
az network nsg rule create \
  --resource-group ${prefix}-rg \
  --name Allow-SSH \
  --nsg-name ${prefix}-vmNSG \
  --priority 100 \
  --source-address-prefixes $(curl -s inet-ip.info) \
  --destination-port-ranges 22 \
  --access Allow \
  --protocol Tcp

# SSH 秘密キーを使用して SSH 接続します
ssh -i ${prefix} azureuser@${prefix}.$region.cloudapp.azure.com
```

## VM 内で Docker 環境を準備

```bash:bash
sudo apt-get install ca-certificates curl gnupg lsb-release

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update

sudo apt-get -y install docker-ce docker-ce-cli containerd.io

sudo usermod -a -G docker $USER

newgrp docker

docker run -it --rm nginx curl inet-ip.info
```

## セルフホステッド Linux エージェントを Docker で起動

```bash:bash
# Azure DevOps の組織 URL を設定（${prefix} 部分は手動で置き換えてください）
azpurl=https://dev.azure.com/${prefix}
azptoke=arwttgjnbnbssyn3gtdqau6a6g6sgx52pufczx4nmxrfvw2tmj5a

# セルフホステッドエージェント用の PAT を設定します
azptoke=yourpathere

# セルフホステッド Linux エージェント用の Dockerfile を作成します
cat << "EOF" > Dockerfile
FROM ubuntu:18.04

# 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

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    curl \
    jq \
    git \
    iputils-ping \
    libcurl4 \
    libicu60 \
    libunwind8 \
    netcat \
    libssl1.0 \
  && rm -rf /var/lib/apt/lists/*

RUN curl -LsS https://aka.ms/InstallAzureCLIDeb | bash \
  && rm -rf /var/lib/apt/lists/*

ENV DOCKER_CHANNEL stable
ENV DOCKER_VERSION 18.06.1-ce

RUN set -ex \
  && curl -fL "https://download.docker.com/linux/static/${DOCKER_CHANNEL}/`uname -m`/docker-${DOCKER_VERSION}.tgz" -o docker.tgz \
  && tar --extract --file docker.tgz --strip-components 1 --directory /usr/local/bin \
  && rm docker.tgz \
  && docker -v

ARG TARGETARCH=amd64
ARG AGENT_VERSION=2.194.0

WORKDIR /azp
RUN if [ "$TARGETARCH" = "amd64" ]; then \
      AZP_AGENTPACKAGE_URL=https://vstsagentpackage.azureedge.net/agent/${AGENT_VERSION}/vsts-agent-linux-x64-${AGENT_VERSION}.tar.gz; \
    else \
      AZP_AGENTPACKAGE_URL=https://vstsagentpackage.azureedge.net/agent/${AGENT_VERSION}/vsts-agent-linux-${TARGETARCH}-${AGENT_VERSION}.tar.gz; \
    fi; \
    curl -LsS "$AZP_AGENTPACKAGE_URL" | tar -xz

COPY ./start.sh .
RUN chmod +x start.sh

ENTRYPOINT [ "./start.sh" ]
EOF

# Dockerfile の一番最後で呼ばれるシェルスクリプトを作成します
cat << "EOF" > 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

export AGENT_ALLOW_RUNASROOT="1"

cleanup() {
  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 "${lightcyan}$1${nocolor}"
}

# Let the agent ignore the token env variables
export VSO_AGENT_IGNORE=AZP_TOKEN,AZP_TOKEN_FILE

source ./env.sh

print_header "1. 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 "2. Running Azure Pipelines agent..."

trap 'cleanup; exit 0' EXIT
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 $!
EOF

# コンテナイメージを作成します
docker build -t devopsagent:latest .

# セルフホステッドエージェントを起動します
docker run -d \
  -e AZP_URL=$azpurl \
  -e AZP_TOKEN=$azptoke \
  -e AZP_AGENT_NAME=$(hostname)-devopsagent \
  --name $(hostname)-devopsagent \
  -v /var/run/docker.sock:/var/run/docker.sock \
  devopsagent:latest

# エージェントのログを確認します
docker logs -f $(hostname)-devopsagent
```

## 作成したセルフホステッドエージェントでパイプラインを実行

```bash:bash
# VM からログアウトします
exit

# azure-pipelines.yml を更新
cat <<EOF > azure-pipelines.yml
trigger:
- master

pool: default

variables:
  imageName: ${prefix}

steps:
- task: Docker@2
  displayName: Build an image
  inputs:
    repository: \$(imageName)
    command: build
    Dockerfile: Dockerfile
EOF

# azure-pipelines.yml を Git リポジトリに登録します
git add azure-pipelines.yml
git commit -m update
git push

# パイプラインの実行状況を確認します
az pipelines runs list \
  --output table

# 最初のパイプラインの詳細を確認します
az pipelines runs show \
  --id 2 \
  --output table

# ブラウザーでビルド結果ページを開きます
az pipelines runs show \
  --id 2 \
  --open
```

## 参考

```bash:bash
# リソースグループを削除します
az group delete \
  --name ${prefix}-rg

# Azure DevOps のプロジェクトを削除します
az devops project delete \
  --id $(az devops project show \
  --project ${prefix} \
  --query id \
  --output tsv)

# Azure CLI に追加した拡張機能を削除します
az extension remove \
  --name azure-devops
```

<https://docs.microsoft.com/ja-jp/azure/devops/pipelines/agents/docker?view=azure-devops>