🤖

Kubernetesで作ったGitHub self-hosted runner内でコンテナイメージをビルドする

2024/12/06に公開

この記事はjsys Advent Calendar 2024 6日目の記事です。
https://adventar.org/calendars/10646

Actions Runner Controller (ARC)

Actions Runner Controller (ARC)とは

GitHubが公式でself-hosted Runnerを利用するユーザー向けに、一時的なRunner(EphemeralRunner)を作成するソリューションです。

GitHub Actions Runnerそのものは、Kubernetesがなくても利用可能です。しかし、アプリケーションのCIなどでActionsを利用する場合、前回の環境が残っているとCI結果が変わってきてしまいます。かといって、VPSやEC2などのVMマシンの再生成には数分時間がかかってしまうため現実的ではありません。

こういった問題を解決すべく、Kubernetesのオートスケーリング機能などを駆使して利用することを前提に作られているのがActions Runner Controller (ARC)です。

ちなみに、標準で利用可能なGitHub-hosted runnersはEC2のVMマシンで実行されているため、コンテナは利用されていません。ARCで作られたRunnerはかなりGitHub-hosted runnersに近い挙動をしますが、どうしてもコンテナの制約の関係で挙動が異なる部分があり、様々な工夫が必要となります。

Docker-in-Docker と Kubernetes モード

ARCはコンテナ(POD)内で実行されるため、workflow内でコンテナを動かすために工夫が必要です。GitHubの公式ドキュメントで触れられているようにDocker-in-Docker と Kubernetes モードが用意されています。

前者はサイドカーでworkflowが実行されるコンテナの外でDockerのデーモンが動いているコンテナのソケットをバインドしてDockerコマンドを実行可能にする方法です。後者は、workflow内でコンテナが呼び出されるとARCがKubernetesに新しくコンテナを立てるという手法です。

後者のほうがスマートであり、GitHubからも後者を使用することが推奨されていますが、結構大きなデメリットとしてworkflow内でDockerコマンドが実行できません。これは、Actionsの醍醐味であるマーケットプレイスで公開されている有志のActionsを利用するときに、困るケースがあります。マーケットプレイスのActionsでは、実行環境の構築時間を短縮するためにDockerのイメージに依存していることがあり、Dockerコマンドが使えないと無駄な迂回をしたりする必要があり、不便です。

そこで、今回はDocker-in-Docker と Kubernetesモードの両方を利用できるARCを構築する方法を紹介します。

そのままではWorkflow内でコンテナイメージが作れない問題

これは今回とても悩まされた問題で、最終的に原因が判明し解決することができました。

前提として、BuildxでDockerのイメージをビルドする基本的なworkflowの書き方は同じですが、少し気をつける部分があります

それは、docker/setup-buildx-action@v3でwithでdriver: kubernetesと指定する必要があることです。これは大した問題ではないですが、これでBuildxがKubernetes モードのARCに対応します。

サンプルのworkflowとしては下記の通りです。

workflow
      - name: Docker meta
        id: docker_meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            latest
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          driver: kubernetes
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: Dockerfile
          platforms: |
            linux/amd64
          push: true
          tags: ${{ steps.docker_meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

ただ、これでだけではだめで、下記のようなエラーが出てしまいます。

User "system:gha-runner-kube-mode" cannot create resource "deployments" in API group "apps" in the namespace "namespace"

このgha-runner-kube-modeというのは、HelmでARCをデプロイする際のKubernetes モードの設定時にtemplate.spec.serviceAccountNameとして指定されたものです。

values.yaml
template:
  spec:
    serviceAccountName: gha-runner-kube-mode

この記述でサービスアカウントを作成していて、roleも割当られているのですがdeploymentsが動きません。ARCのHelmのテンプレートを見に行ったところ、該当のマニフェストは下記ファイルのようです。

https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/templates/kube_mode_role.yaml

appsグループのdeploymentsの操作をする権限がなく、Buildxがコンテナを建てれなかったのが原因のようです。

> - apiGroups: ["apps"]
>   resources: ["deployments"]
>   verbs: ["get", "list", "create", "delete"]

上記のようにサービスアカウントに対して追加の権限を与えることで問題を解決することができました。

上記のことを踏まえてActions Runner Controller (ARC)を構築する

デプロイにArgoCDを用いています。

ArgoCDのKustomize実行時のオプションのみ変更していて、--enable-helmを有効化しています。このオプションなしではkustomization.yaml内のHelmChartが展開されないのでご注意ください。

本来であればHelmChartをデプロイするだけでARCの実行が可能となるのですが、前述の通りHelmから生成されるサービスアカウントでは権限が不足しています。そのため、Kustomize内でHelmを呼び出し、別途適切な権限を持ったサービスアカウントのマニフェストを適用することで対応しています。

sysctlの設定

まず、rootless dockerの動作に必要なフラグが無効化されている場合があるので、有効化します。これはKubernetesクラスターのホストOSがUbuntuであり、Ubuntu 23.10以降の場合に影響があります。

詳細:

また、Runnerの動きうるKubernetesノード全てで行う必要があります。roleがetcdなどのコントロールプレーンしか持っていないノードでは不要と思います。

/etc/sysctl.d/に適当なファイルを生成するなどして、永続的にフラグを有効化します。

/etc/sysctl.d/k8s.conf
kernel.apparmor_restrict_unprivileged_unconfined = 0
kernel.apparmor_restrict_unprivileged_userns = 0

ファイル書き込み後は下記のコマンドを実行して反映させます。

sysctl -p

Helmを使ってActions Runner Controller (ARC)をデプロイ

ディレクトリ構造を下記の通りにしてマニフェストファイルを作成します。

.
├── gha-runner-kube-mode-role-binding.yaml
├── gha-runner-kube-mode-role.yaml
├── gha-runner-kube-mode-sa.yaml
├── gha-runner-scale-set-controller-values.yaml
├── gha-runner-scale-set-values.yaml
└── kustomization.yaml

Kustomization

kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: actions-runner-system

helmCharts:
- name: gha-runner-scale-set-controller
  releaseName: gha-runner-scale-set-controller
  namespace: actions-runner-system
  version: 0.9.3
  repo: oci://ghcr.io/actions/actions-runner-controller-charts
  valuesFile: gha-runner-scale-set-controller-values.yaml

- name: gha-runner-scale-set
  releaseName: gha-runner-scale-set
  namespace: actions-runner-system
  version: 0.9.3
  repo: oci://ghcr.io/actions/actions-runner-controller-charts
  valuesFile: gha-runner-scale-set-values.yaml

resources:
  - gha-runner-kube-mode-sa.yaml
  - gha-runner-kube-mode-role.yaml
  - gha-runner-kube-mode-role-binding.yaml

runner-scale-set-controller

ArgoCDのラベルがついたリソースが増えまくってしまい、正常にARCコントローラーのリスナーが動かなくなるので、gha-runner-scale-set-controllerのvaluesとして下記の記述が必要です。

詳細:
https://github.com/actions/actions-runner-controller/issues/3533#issuecomment-2232996941

gha-runner-scale-set-controller-values.yaml
flags:
  excludeLabelPropagationPrefixes:
    - "argocd.argoproj.io/instance"

runner-scale-set

これがRunnerの本体です。

RunnerのコンテナモードはcontainerMode.type: "kubernetes"として設定しています。また、workflow内でDockerコマンドを利用できるようにDinDのコンテナを用いてサイドカーで実行するようにしています。

gha-runner-kube-mode-sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gha-runner-kube-mode
  namespace: actions-runner-system
gha-runner-kube-mode-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: gha-runner-kube-mode
  namespace: actions-runner-system
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "create", "delete"]
- apiGroups: [""]
  resources: ["pods/exec"]
  verbs: ["get", "create"]
- apiGroups: [""]
  resources: ["pods/log"]
  verbs: ["get", "list", "watch",]
- apiGroups: ["batch"]
  resources: ["jobs"]
  verbs: ["get", "list", "create", "delete"]
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list", "create", "delete"]
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "create", "delete"]
gha-runner-scale-set-values.yaml
githubConfigUrl: [レポジトリURLかGitHub OrgのURL]
githubConfigSecret:
  github_app_id: [GitHub AppID]
  github_app_installation_id: [GitHub Appのinstallation ID]
  github_app_private_key: [GitHub Appの秘密鍵ダブルクオーテーション("")で囲む必要があります]

maxRunners: 8
minRunners: 3

runnerScaleSetName: github-actions-runner

containerMode:
  type: "kubernetes"
  kubernetesModeWorkVolumeClaim:
    accessModes: ["ReadWriteOnce"]
    resources:
      requests:
        storage: 3Gi


controllerServiceAccount:
  namespace: actions-runner-system
  name: gha-runner-scale-set-controller-gha-rs-controller


template:
  spec:
    serviceAccountName: gha-runner-kube-mode
    initContainers:
    - name: kube-init
      image: ghcr.io/actions/actions-runner:latest
      command: ["sudo", "chown", "-R", "1001:1001", "/home/runner/_work"]
      volumeMounts:
      - name: work
        mountPath: /home/runner/_work
    - name: init-dind-externals
      image: ghcr.io/actions/actions-runner:latest
      command: ["cp", "-r", "-v", "/home/runner/externals/.", "/home/runner/tmpDir/"]
      volumeMounts:
        - name: dind-externals
          mountPath: /home/runner/tmpDir
    - name: init-dind-rootless
      image: docker:dind-rootless
      command:
        - sh
        - -c
        - |
          set -x
          cp -a /etc/. /dind-etc/
          echo 'runner:x:1001:1001:runner:/home/runner:/bin/ash' >> /dind-etc/passwd
          echo 'runner:x:1001:' >> /dind-etc/group
          echo 'runner:100000:65536' >> /dind-etc/subgid
          echo 'runner:100000:65536' >>  /dind-etc/subuid
          chmod 755 /dind-etc;
          chmod u=rwx,g=rx+s,o=rx /dind-home
          chown 1001:1001 /dind-home
      securityContext:
        runAsUser: 0
      volumeMounts:
        - mountPath: /dind-etc
          name: dind-etc
        - mountPath: /dind-home
          name: dind-home
    containers:
    - name: runner
      image: ghcr.io/actions/actions-runner:latest
      command: ["/home/runner/run.sh"]
      env:
        - name: ACTIONS_RUNNER_CONTAINER_HOOKS
          value: /home/runner/k8s/index.js
        - name: ACTIONS_RUNNER_POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER
          value: "false"
        - name: DOCKER_HOST
          value: unix:///home/runner/var/run/docker.sock
      volumeMounts:
        - name: work
          mountPath: /home/runner/_work
        - name: dind-sock
          mountPath: /home/runner/var/run
    - name: dind
      image: docker:dind-rootless
      args: ["dockerd", "--host=unix:///home/runner/var/run/docker.sock"]
      securityContext:
        privileged: true
        runAsUser: 1001
        runAsGroup: 1001
      volumeMounts:
        - name: work
          mountPath: /home/runner/_work
        - name: dind-sock
          mountPath: /home/runner/var/run
        - name: dind-externals
          mountPath: /home/runner/externals
        - name: dind-etc
          mountPath: /etc
        - name: dind-home
          mountPath: /home/runner
    volumes:
    - name: dind-sock
      emptyDir: {}
    - name: dind-externals
      emptyDir: {}
    - name: dind-etc
      emptyDir: {}
    - name: dind-home
      emptyDir: {}

workflowの定義ファイル

jobs.[job名].runs-on:に、runner-scale-setのHelm valuesのrunnerScaleSetNameで指定した名称(サンプルではgithub-actions-runner)を指定することで、今回デプロイしたRunnerが利用されるようになります。

name: CI
on:
    push:

jobs:
  deploy:
    runs-on: [github-actions-runner]
    steps:
      - uses: actions/checkout@v4
        with:
          ref: "main"
...(以下略)...

独自イメージを利用する(オプション)

この記事のサンプル内で紹介したRunenr用イメージghcr.io/actions/actions-runner:latestは最低限のパッケージしか入っていません。

情報メディアシステム局では、パッケージインストール時間の短縮のためにGitHub公式RunnerがHashiCorp Packerで導入しているパッケージと、GitHubが公開しているactions-runnerのDockerfileを参考にして、独自のイメージを用意して利用しました。

実際に利用したDockerfileです。長いので折りたたんでいます
Dockerfile
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0-jammy as build

ARG TARGETOS
ARG TARGETARCH
ARG RUNNER_VERSION
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.6.1
ARG DOCKER_VERSION=27.1.1
ARG BUILDX_VERSION=0.16.2

RUN apt update -y && apt install curl unzip -y

WORKDIR /actions-runner
RUN export RUNNER_ARCH=${TARGETARCH} \
    && if [ "$RUNNER_ARCH" = "amd64" ]; then export RUNNER_ARCH=x64 ; fi \
    && curl -f -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-${TARGETOS}-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz \
    && tar xzf ./runner.tar.gz \
    && rm runner.tar.gz

RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v${RUNNER_CONTAINER_HOOKS_VERSION}/actions-runner-hooks-k8s-${RUNNER_CONTAINER_HOOKS_VERSION}.zip \
    && unzip ./runner-container-hooks.zip -d ./k8s \
    && rm runner-container-hooks.zip

RUN export RUNNER_ARCH=${TARGETARCH} \
    && if [ "$RUNNER_ARCH" = "amd64" ]; then export DOCKER_ARCH=x86_64 ; fi \
    && if [ "$RUNNER_ARCH" = "arm64" ]; then export DOCKER_ARCH=aarch64 ; fi \
    && curl -fLo docker.tgz https://download.docker.com/${TARGETOS}/static/stable/${DOCKER_ARCH}/docker-${DOCKER_VERSION}.tgz \
    && tar zxvf docker.tgz \
    && rm -rf docker.tgz \
    && mkdir -p /usr/local/lib/docker/cli-plugins \
    && curl -fLo /usr/local/lib/docker/cli-plugins/docker-buildx \
        "https://github.com/docker/buildx/releases/download/v${BUILDX_VERSION}/buildx-v${BUILDX_VERSION}.linux-${TARGETARCH}" \
    && chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx

FROM ubuntu:noble

ENV DEBIAN_FRONTEND=noninteractive
ENV RUNNER_MANUALLY_TRAP_SIG=1
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
ENV ImageOS=ubuntu24

# 'gpg-agent' and 'software-properties-common' are needed for the 'add-apt-repository' command that follows
RUN apt update -y \
    && apt install -y --no-install-recommends sudo lsb-release gpg-agent software-properties-common curl jq unzip \
    && rm -rf /var/lib/apt/lists/*

# Configure git-core/ppa based on guidance here:  https://git-scm.com/download/linux
RUN add-apt-repository ppa:git-core/ppa \
    && apt update -y \
    && apt install -y --no-install-recommends git

# add extra packages
# Configure vital_packages(excluded: curl jq unzip) install
RUN apt update -y \
    && apt install -y --no-install-recommends bzip2 g++ gcc make tar wget

# Configure common_packages install
RUN apt update -y \
    && apt install -y --no-install-recommends autoconf automake dbus dnsutils dpkg dpkg-dev \
    fakeroot fonts-noto-color-emoji gnupg2 iproute2 iputils-ping libyaml-dev libtool libssl-dev \
    locales mercurial openssh-client p7zip-rar pkg-config python-is-python3 rpm texinfo tk tree \
    tzdata upx xvfb xz-utils zsync

# Configure cmd_packages install
RUN apt update -y \
    && apt install -y --no-install-recommends acl aria2 binutils bison brotli \
    coreutils file findutils flex ftp haveged lz4 m4 mediainfo netcat-traditional net-tools \
    p7zip-full parallel patchelf pigz pollinate rsync shellcheck sphinxsearch sqlite3 \
    ssh sshpass sudo swig telnet time zip

# Configure sohosai_dev_packages install
RUN apt update -y \
    && apt install -y --no-install-recommends npm

RUN adduser --disabled-password --gecos "" --uid 1001 runner \
    && groupadd docker --gid 123 \
    && usermod -aG sudo runner \
    && usermod -aG docker runner \
    && echo "%sudo   ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers \
    && echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers

WORKDIR /home/runner

COPY --chown=runner:docker --from=build /actions-runner .
COPY --from=build /usr/local/lib/docker/cli-plugins/docker-buildx /usr/local/lib/docker/cli-plugins/docker-buildx

RUN install -o root -g root -m 755 docker/* /usr/bin/ && rm -rf docker

USER runner

終わりに

筑波大学学園祭実行委員会情報メディアシステム局では、学園祭の実施に向けチームでWebサービスを開発しています。self-hosted Runnerの導入により、GitHub Actionsの制限に躊躇することなく開発できる環境を構築し、効率的な開発を実現しました。

筑波大学学園祭実行委員会情報メディアシステム局の紹介

筑波大学学園祭実行委員会は筑波大学の学園祭「雙峰祭(そうほうさい)」を開催しています。

https://sohosai.com

情報メディアシステム局では、学園祭の実現に向けて参加企画向けシステムの開発や、独自の生配信プラットフォームを通じた映像配信をしています。

情報系に限らず様々な学生が1~2年生を中心に活動しています。少しでも面白そうと感じた筑波大生の方は、info@sohosai.comよりお気軽にお問い合わせください。

筑波大学学園祭実行委員会

Discussion