GitHub Actions で独自のランナーをホストする事ができる self-hosted runner
というソフトウェアがあります。
この記事では、self-hosted runner
をローカル環境(docker compose)に構築する方法を紹介します。
前提
-
self-hosted runner
は docker compose で起動します - GitHub Actions のコンテナ機能を使えるようにするため、
公式のコミュニティ版の rootless dind イメージを利用します
Windows 10
Docker Desktop 4.29.0
self-hosted runner v2.316.1
runner-container-hooks v0.6.0
self-hosted runner とは
冒頭に記載した通り、独自のランナーを作成してそのプロセス上でGitHub Actionsのワークフローを実行するソフトウェアです。
GitHub ホステッド ランナーでは提供されていない機能/OSを利用したい場合や、CI/CDの実行時間やコスト削減を目的として導入するケースが多いようです。
以下、公式ドキュメントより引用。
Self-hosted runners offer more control of hardware, operating system, and software tools than GitHub-hosted runners provide. With self-hosted runners, you can create custom hardware configurations that meet your needs with processing power or memory to run larger jobs, install software available on your local network, and choose an operating system not offered by GitHub-hosted runners. Self-hosted runners can be physical, virtual, in a container, on-premises, or in a cloud.
self-hosted runner をコンテナで起動するには
Actions Runner Controller という k8s オペレータを公式が提供しています。
上記レポジトリを確認すると dind 用のコンテナイメージがありましたので、それをベースイメージとして利用する事にします。
また、self-hosted runner
上でコンテナ機能を利用するためには runner-container-hooks を導入する必要があります。
詳細は公式ドキュメントを参照してください。
概要
ざっくり、以下の流れでCIを実行してみます。
ビルドしたコンテナイメージは self-hosted runner
と一緒に起動するローカルレジストリに格納します。
- GitHub に変更をPushする
-
self-hosted runner
はロングポーリング(TCP443)でGitHubの変更を検知する - 変更を検知したら、GitHub Actions ワークフローの実行をトリガーする
- UT, コンテナイメージのビルド/プッシュを実行する
実装
ディレクトリ構成は以下となります。
sample-app
は Fast API と MySQL で動く適当なサンプルアプリです。
.
|-- compose.yaml
|-- .env
|-- .env.setup
|-- .github
| `-- workflows
| |-- _build.yaml
| `-- publish-image.yaml
|-- sample-app
| |-- app.py
| |-- ...
| |-- requirements.txt
| |-- Dockerfile
| `-- compose.yaml
|-- auth
| `-- htpasswd
|-- runner
| |-- Dockerfile
| |-- cleanup.sh
| |-- .env
| `-- docker-daemon.json
`-- setup-runner
|-- Dockerfile
`-- set_registration_token.sh
runner/Dockerfile
公式の dind イメージに幾つか変更を加えています。
-
/etc/arc/hooks/job-completed.d
に Post 処理用スクリプトを追加- 詳細は公式ドキュメントを参照してください
-
/runnertmp/.env
に Env ファイルを追加- ここで定義した環境変数が
self-hosted runner
のプロセスに適用されます
- ここで定義した環境変数が
-
docker-daemon.json
を追加- Dockerデーモンに insecure-registry 等を設定するため
- rootlessモードなので、ユーザーのホームディレクトリ配下に設置する
-
runner-container-hooks
をインストール
FROM ghcr.io/actions-runner-controller/actions-runner-controller/actions-runner-dind-rootless:v2.316.0-ubuntu-22.04
COPY cleanup.sh /etc/arc/hooks/job-completed.d/cleanup.sh
COPY --chown=runner .env /runnertmp/.env
COPY --chown=runner docker-daemon.json /home/runner/.config/docker/daemon.json
USER root
RUN mkdir -p -m 755 /var/lib/docker /runner \
&& chown -R runner:runner /runner /var/lib/docker \
&& chmod -R +x /etc/arc/hooks
USER runner
ENV ACTIONS_RUNNER_CONTAINER_HOOKS=${HOME}/runner-container-hooks-docker/index.js
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.6.0
RUN cd ${HOME} \
&& curl -fL -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v${RUNNER_CONTAINER_HOOKS_VERSION}/actions-runner-hooks-docker-${RUNNER_CONTAINER_HOOKS_VERSION}.zip \
&& unzip ./runner-container-hooks.zip -d ./runner-container-hooks-docker \
&& rm -f runner-container-hooks.zip \
&& chown -R runner ./runner-container-hooks-docker
VOLUME [ "/runner", "/var/lib/docker" ]
runner/.env
self-hosted runner
のプロセスに適用する環境変数を定義します。
公式ドキュメントによると、起動スクリプトと同じ階層に .env
が配置されていると自動で読み込まれるようです。
TZ=Asia/Tokyo
runner/cleanup.sh
ワークフロー実行後の Post 処理で実行されるスクリプトです。
ログ出力とプライベートレジストリのログアウト処理を追加してみました。
#!/bin/bash
set -Eeo pipefail
set -x
trap catch ERR
# exit 0を返せないと後続のstepに進む前にFailedになってしまうので必ずexit 0で終了させる
catch() {
echo "Trap ERR! exit 0 for run job"
exit 0
}
DATE=$(date "+%Y/%m/%d %H:%M:%S")
echo "${DATE} Job completed: ${GITHUB_REPOSITORY} ${GITHUB_JOB}"
echo "${DATE} WORKFLOW: ${GITHUB_WORKFLOW}"
echo "${DATE} RUNNER_NAME: ${RUNNER_NAME}"
echo "${DATE} RUN_ID": "${GITHUB_RUN_ID}"
docker logout private-registry.local:5000
runner/docker-daemon.json
Docker デーモンに読み込ませるコンフィグファイルです。
ローカル環境に起動したプライベートレジストリを insecure-registries
に追加します。
- https://matsuand.github.io/docs.docker.jp.onthefly/registry/insecure/
- https://docs.docker.jp/config/daemon/daemon.html#configure-the-docker-daemon
{
"insecure-registries" : [
"private-registry.local:5000"
]
}
setup-runner/Dockerfile
self-hosted runner
をGitHubに登録するためには、アクセストークンが必要です。
self-hosted runner
を起動する前に、こちらのコンテナで GitHub の Rest API からアクセストークンを取得しておきます。
FROM alpine
WORKDIR /work
RUN apk --no-cache add curl bash jq
COPY set_registration_token.sh .
CMD [ "sleep", "infinity" ]
setup-runner/set_registration_token.sh
アクセストークンを取得して、.env
に追記するためのスクリプトです。
self-hosted runner
を起動する前に、このスクリプトを実行します。
#!/bin/sh
response=$(curl -sSL \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_PAT" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/<Organization名>/<Repository名>/actions/runners/registration-token)
token=$(echo "$response" | jq -r '.token')
sed -i "s|RUNNER_TOKEN=.*|RUNNER_TOKEN=$token|" /mount/.env
compose.yaml
docke compose で self-hosted runner
を起動します。
setup-runner
コンテナが正常終了すると、runner[1-2]
が起動されます。
また、プライベートレジストリとUIも一緒にデプロイしてしまいます。
x-templates: &runner
build:
context: ./runner
dockerfile: Dockerfile
privileged: true
restart: always
env_file:
- .env
depends_on:
setup-runner:
condition: service_completed_successfully
deploy:
resources:
limits:
memory: 2g
cpus: '2'
networks:
- runner-net
services:
runner1:
<<: *runner
container_name: self-hosted-runner-example-1
hostname: self-hosted-runner-example-1
environment:
RUNNER_NAME: self-hosted-runner-example-1
volumes:
- type: volume
source: runner-docker-volume1
target: /home/runner/.local/share/docker
runner2:
<<: *runner
container_name: self-hosted-runner-example-2
hostname: self-hosted-runner-example-2
environment:
RUNNER_NAME: self-hosted-runner-example-2
volumes:
- type: volume
source: runner-docker-volume2
target: /home/runner/.local/share/docker
setup-runner:
build:
context: setup-runner
dockerfile: Dockerfile
command: sh ./set_registration_token.sh
env_file:
- .env
- .env.setup
volumes:
- type: bind
source: ./
target: /mount
networks:
- runner-net
private-registry:
image: registry
container_name: private-registry.local
hostname: private-registry.local
ports:
- 5000:5000
environment:
REGISTRY_HTTP_SECRET: "<適当な文字列>"
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
REGISTRY_STORAGE_DELETE_ENABLED: "true"
volumes:
- type: bind
source: ./auth
target: /auth
- type: volume
source: registry-volume
target: /var/lib/registry
volume:
nocopy: true
networks:
- runner-net
registry-ui:
image: klausmeyer/docker-registry-browser:latest
ports:
- 8080:8080
environment:
DOCKER_REGISTRY_URL: "http://private-registry.local:5000"
ENABLE_DELETE_IMAGES: "true"
SECRET_KEY_BASE: "<適当な文字列>"
depends_on:
- private-registry
networks:
- runner-net
volumes:
runner-docker-volume1:
runner-docker-volume2:
registry-volume:
networks:
runner-net:
.env
runner[1-2]
コンテナに読み込ませる環境変数を .env
定義します。
ドキュメントが見つけられなかったので、この辺りの設定は元イメージの起動スクリプトから確認しました。
GITHUB_URL=https://github.com/
# RUNNER_ORG=<Organization名>
# RUNNER_REPO=<Repository名>
RUNNER_REPO=<Organization名>/<Repository名>
RUNNER_EPHEMERAL=false
RUNNER_TOKEN=xxxxx
RUNNER_HOME=/runner
RUNNER_ASSETS_DIR=/runnertmp
RUNNER_GRACEFUL_STOP_TIMEOUT=15
STARTUP_DELAY_IN_SECONDS=5
WAIT_FOR_DOCKER_SECONDS=120
DOCKER_ENABLED=true
# LOG_DEBUG_DISABLED=false
# LOG_NOTICE_DISABLED=false
# LOG_WARNING_DISABLED=false
# LOG_ERROR_DISABLED=false
# LOG_SUCCESS_DISABLED=false
.env.setup
.env.setup
は setup-runner
コンテナが Rest API を実行するためのアクセストークンを設定します。
GITHUB_PAT=ghp_xxx
今回のケースでは、アクセストークンに以下の権限が必要となります。
- administration:write
GitHub Actions ワークフロー
実際に動かす GitHub Actions のワークフローを実装します。
ワークフロー定義ファイルは .github/workflows
配下に置く必要があります。
.github/workflows/_build.yaml
コンテナイメージをビルドして任意のレジストリにプッシュするワークフローです。
name: Publish image to container Registry
on:
workflow_call:
inputs:
REGISTORY_HOST:
description: 'Container registry hostname'
type: string
required: true
REGISTORY_USER:
description: 'Container registry username'
type: string
required: true
IMAGE_NAME:
description: 'Container image name'
type: string
required: true
IMAGE_TAG:
description: 'Container tag name'
type: string
required: true
BUILD_CONTEXT:
description: 'Docker build context'
type: string
default: '.'
DOCKERFILE:
description: 'Dockerfile path'
type: string
default: './Dockerfile'
CURRENT_DIR:
description: 'Current directory path'
type: string
default: './'
secrets:
REGISTRY_PASSWD:
description: 'Password for container registry login'
required: true
env:
REGISTORY_HOST: ${{ inputs.REGISTORY_HOST }}
REGISTORY_USER: ${{ inputs.REGISTORY_USER }}
IMAGE_NAME: ${{ inputs.IMAGE_NAME }}
IMAGE_TAG: ${{ inputs.IMAGE_TAG }}
BUILD_CONTEXT: ${{ inputs.BUILD_CONTEXT }}
DOCKERFILE: ${{ inputs.DOCKERFILE }}
CURRENT_DIR: ${{ inputs.CURRENT_DIR }}
jobs:
publish:
name: Publish container image
runs-on: [self-hosted, linux, x64]
permissions:
contents: read
steps:
- name: Publish container image
run: |
cd $CURRENT_DIR
echo ${{ secrets.REGISTRY_PASSWD }} | docker login $REGISTORY_HOST -u $REGISTORY_USER --password-stdin
docker build -t "$REGISTORY_HOST/$IMAGE_NAME:$IMAGE_TAG" $BUILD_CONTEXT -f $DOCKERFILE
docker push "$REGISTORY_HOST/$IMAGE_NAME:$IMAGE_TAG"
.github/workflows/publish-image.yaml
所謂 CI を実行するためのワークフローです。
main ブランチにプッシュするとこのワークフローがトリガーされます。
Unit Test を実行するために self-hosted runner
上でアプリケーション用の docker compose
を起動しています。
※self-hosted runner
を起動したままにしておくこと、job間でcloneしたレポジトリが共有できるようになっています
name: Publish image to Private Registry
on:
push:
branches:
- main
jobs:
setup:
runs-on: [self-hosted, linux, x64]
permissions:
contents: read
steps:
- name: Check out source repository
uses: actions/checkout@v4
with:
fetch-depth: 1
test:
needs: setup
runs-on: [self-hosted, linux, x64]
permissions:
contents: read
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install -r sample-app/requirements.txt
python -m pip install pytest flake8
- name: Lint with flake8
run: flake8
- name: Start containers
run: |
cd sample-app
docker compose up -d
- name: Execute pytest
run: pytest
env:
MYSQL_HOST: 127.0.0.1
- name: Stop containers
if: always()
run: |
cd sample-app
docker compose down
publish:
name: Publish container image
needs: test
uses: ./.github/workflows/_build.yaml
with:
REGISTORY_HOST: private-registry.local:5000
IMAGE_NAME: sample-app
IMAGE_TAG: ${{ github.sha }}
REGISTORY_USER: test-user
CURRENT_DIR: ./sample-app
secrets:
REGISTRY_PASSWD: ${{ secrets.PRIVATE_REGISTRY_TOKEN }}
起動準備
プライベートレジストリのベーシック認証用に htpasswd
を作成します。
$ REG_USER=test-user
$ REG_PASSWORD=xxxxx
$ docker run -it --rm httpd htpasswd -nb -B ${REG_USER} ${REG_PASSWORD} > ./auth/htpasswd
ここで定義したパスワードは GitHub Actions Secret に登録しておきます。
ワークフロー実行
ワークフローを実行する前に self-hosted runner
を起動します。
$ docker compose up -d
...
[+] Running 6/6
✔ Network self-hosted-runner-dind-rootless_runner-net Created 0.0s
✔ Container private-registry.local Started 0.2s
✔ Container self-hosted-runner-dind-rootless-setup-runner-1 Exited 0.2s
✔ Container self-hosted-runner-example-2 Started 0.1s
✔ Container self-hosted-runner-example-1 Started 0.2s
✔ Container self-hosted-runner-dind-rootless-registry-ui-1 Started 0.1s
self-hosted-runner-example-[1-2]
の Status が「Idle」になっていればOKです。
もし登録がうまくいかない場合は、コンテナの実行ログを確認してみましょう。
最後にワークフロー実行します。
$ git commit --allow-empty -m "empty commit" && git push origin main
GitHub の Actions タブから、ワークフローの実行状況を確認できます。
サイドメニューから任意のジョブ名を選択すると、GitHub ホステッドランナーと同様に実行ログが確認できました。
最後に runner/cleanup.sh
もちゃんと実行されていました。
補足
使い始める前に、まずは公式ドキュメントを読みましょう。
コンテナイメージ
コンテナイメージは他にも幾つかありましたので、必要に応じて差し替えると良いでしょう。
- https://github.com/actions-runner-controller/actions-runner-controller/pkgs/container/actions-runner-controller%2Factions-runner
- https://github.com/actions-runner-controller/actions-runner-controller/pkgs/container/actions-runner-controller%2Factions-runner-dind
- https://github.com/actions-runner-controller/actions-runner-controller/pkgs/container/actions-runner-controller%2Factions-runner-dind-rootless
※ 2024/6/15 追記:
上記イメージはコミュニティ版(?)のようで、公式の提供するイメージは以下となります。
self-hosted runner の管理レベル
以下の管理レベル毎に self-hosted runner
を登録することが可能です。
- repository
- organization
- enterprise
管理レベルによって GitHub Rest API のエンドポイントが若干異なりますので、必要に応じて変更する必要があります。
また、self-hosted runner
を誰でも登録できてしまうとセキュリティの観点でよろしくない(任意コードの実行 等々)ため、運用方法の検討が必要です。
- https://docs.github.com/ja/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization
- https://docs.github.com/ja/enterprise-cloud@latest/admin/policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-github-actions-in-your-enterprise
エフェメラル ランナー
一度ジョブを実行するとプロセス(コンテナ)が停止するエフェメラル ランナーとして起動することができます。
都度クリーンな環境でジョブを実行したい場合や自動スケーリングを作りこむ場合は、エフェメラル ランナーを選択しましょう。
コンテナで起動する場合は RUNNER_EPHEMERAL: true
で指定可能です。
actions-runner-controller
公式の k8s オペレータがありますので、kubernetes環境で実行する場合はこちらの導入を検討しましょう。
- https://github.com/actions/actions-runner-controller
- https://docs.github.com/ja/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller
ちなみに actions-runner-controller
の dind モード(のデフォルト設定)は、dind コンテナをサイドカー方式でデプロイして docker.sock
を self-hosted runner
コンテナに共有(マウント)しています。
runner-container-hooks
コンテナ上で起動した self-hosted runner
で、Actions のコンテナ機能が利用できるようになります。
詳細は以下ドキュメントを参照してください。
- https://github.com/actions/runner-container-hooks/tree/main/docs/adrs
- https://zenn.dev/kesin11/articles/20230514_container_hooks
まとめ
ソースコード管理システムに付随するCI/CDツールは、一定数の需要があると思います。
また、self-hosted runner
は実行環境を選ばないので、利用者側でのカスタマイズもしやすいと思いました。
何かしらの理由でGitHub ホステッドランナーが使えない場合は、次の選択肢として self-hosted runner
の導入を検討されてみてはいかがでしょうか。