Kubernetesで作ったGitHub self-hosted runner内でコンテナイメージをビルドする
この記事はjsys Advent Calendar 2024 6日目の記事です。
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としては下記の通りです。
- 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
として指定されたものです。
template:
spec:
serviceAccountName: gha-runner-kube-mode
この記述でサービスアカウントを作成していて、roleも割当られているのですがdeployments
が動きません。ARCのHelmのテンプレートを見に行ったところ、該当のマニフェストは下記ファイルのようです。
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/
に適当なファイルを生成するなどして、永続的にフラグを有効化します。
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
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として下記の記述が必要です。
詳細:
flags:
excludeLabelPropagationPrefixes:
- "argocd.argoproj.io/instance"
runner-scale-set
これがRunnerの本体です。
RunnerのコンテナモードはcontainerMode.type: "kubernetes"
として設定しています。また、workflow内でDockerコマンドを利用できるようにDinDのコンテナを用いてサイドカーで実行するようにしています。
apiVersion: v1
kind: ServiceAccount
metadata:
name: gha-runner-kube-mode
namespace: actions-runner-system
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"]
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です。長いので折りたたんでいます
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 /actions-runner .
COPY /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の制限に躊躇することなく開発できる環境を構築し、効率的な開発を実現しました。
筑波大学学園祭実行委員会情報メディアシステム局の紹介
筑波大学学園祭実行委員会は筑波大学の学園祭「雙峰祭(そうほうさい)」を開催しています。
情報メディアシステム局では、学園祭の実現に向けて参加企画向けシステムの開発や、独自の生配信プラットフォームを通じた映像配信をしています。
情報系に限らず様々な学生が1~2年生を中心に活動しています。少しでも面白そうと感じた筑波大生の方は、info@sohosai.com
よりお気軽にお問い合わせください。
Discussion