コンテナ化したSelf-hosted Runnerからコンテナを起動する
やりたいこと
- 統合テストのために GitHub Actions でエフェメラルな DB (できればコンテナ)を構築したい
- Self-hosted Runner は諸般の事情によりコンテナで動いている
- コンテナからコンテナを起動しなければいけない!
環境
- Self-hosted Runner (Ubuntu 22.04 コンテナ)
アプローチ
コンテナからコンテナを起動する場合、大きく次の2つのアプローチがあります。
- Docker in Docker (dind)
- Docker outside of Docker (dood)
詳しくは次の記事をどうぞ。
Docker in Docker
起動元のコンテナに別の Docker デーモンを構築し、分離されたコンテナ環境を作る方法です。
完全に Self-hosted Runner 内でコンテナが分離されるのが魅力的ですが、以下の点を考慮して今回は採用を見送りました。
- 起動元コンテナを特権コンテナ、rootユーザとして起動する必要がある
-
/sbin/init
を PID 1 で起動して systemd で Docker デーモンを起動する方法がよく取られているようです-
sbin/init
は root 起動する必要がありその配下のアプリケーションプロセスをうまいこと(CMD
とかENTRYPOINT
などで)一般ユーザで起動する方法がよくわからなく、諦めました
-
-
以下の記事には --privileged
コンテナとして起動しないことが可能と言及されていますが、
Update (July 2020) : when I wrote this blog post in 2015, the only way to run Docker-in-Docker was to use the -privileged flag in Docker. Today, the landscape is very different. Container security and sandboxing advanced very significantly, with e.g. rootless containers and tools like sysbox. The latter lets you run Docker-in-Docker without the -privileged flag,
docker公式ドキュメントでもrootlessコンテナに対しても--privileged
オプションを付与している通り、執筆時時点では難しそうでした。
Docker outside of Docker
ホスト VM の Docker デーモンを共有して、「兄弟」のコンテナを操作する方法です。
環境が完全には分離されない点、ホスト VM の Docker デーモンに対する完全なコントロールをゲストコンテナに渡すことになる点が問題になる場合がありますが、特権が不要でコンテナ内は一般ユーザでもプロセスを実行できます。
今回は構築がdindよりもかなり簡単だったのでとりあえずこちらで試してみることにしました。
実際に採用する場合はセキュリティリスクを十分考慮してください。
ランナーのコンテナを構築
DooD を実現するためのランナーコンテナへの細工は非常にシンプルで、以下のことを行うのみです。
- docker CLI をインストール (docker engine は不要です)
なお、doodに関係しない部分は省略します。
Docker CLI をインストール
公式の手順にある通り apt リポジトリを追加してインストールするだけです(docker-ce-cli
のみでOKです)。
RUN apt-get update &&\
apt-get install -y ca-certificates curl &&\
install -m 0755 -d /etc/apt/keyrings &&\
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc &&\
chmod a+r /etc/apt/keyrings/docker.asc &&\
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null &&\
apt-get update &&\
apt-get install docker-ce-cli
ランナーコンテナを起動
ランナーコンテナをビルドしたら、以下の点に気をつけて起動します。
-
docker.sock
をマウントする - ホストの docker グループへコンテナユーザを追加する
docker run -d --rm --name <container_name>\
-v /var/run/docker.sock:/var/run/docker.sock \
--group-add $(awk -F: '$1 == "docker" {print $3}' /etc/group)
<image_name>
--group-add
でホストの docker グループへユーザを参加させる部分は次の記事を参考にしました。
このあとは configure.sh
と run.sh
を実行してリポジトリへの接続設定を行いますが、省略します。
(この時点でコンテナ内からdocker
コマンドが利用可能であることを確かめ、ホストVM上のコンテナが見えることを確認しておきます)
サービスコンテナとランナーコンテナの接続
ここまででランナーから docker
コマンドを利用できるようになりましたが、これだけでは本来やりたかったランナーから DB コンテナへの接続ができません。
これは、 DB コンテナのポートを公開してホストネットワークに晒したとしても、ランナーのlocalhost
とホストのlocalhost
は異なるため、ランナーからホストのlocalhost
へ接続することができないからです。
これを解決するために、ランナーのコンテナをDBコンテナが所属する docker network に所属させることにしました。
まずは DB コンテナを構築します。
コンテナのライフサイクルを自動で管理してくれる GitHub Actions の機能であるサービスコンテナを利用します。
jobs:
test:
runs-on: [self-hosted, linux, x64]
services:
postgres:
image: postgres:16-bookworm
env:
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
# (略)
サービスコンテナの挙動を調べてみると、最初に github_network_<ID>
という名前の docker network を作成したのちにその docker network 上でコンテナを起動しているようでした。
このネットワーク名は jobs.container.network
コンテキストから取得可能だったため、これを利用します。
steps:
- name: Connect self-hosted runner to docker network
run: |
docker network connect ${{ job.container.network }} $(uname -n)
if: ${{ runner.environment == 'self-hosted' }}
# (略)
- name: Detach self-hosted runner from docker network
run: |
docker network disconnect ${{ job.container.network }} $(uname -n)
if: ${{ always() }}
-
$(uname -n)
でホストネームを取得しています- ホストネームがコンテナIDと同一になることを仮定しています
- 最後にランナーコンテナを docker network から切断します
- サービスコンテナの破棄時に docker network の削除が自動で行われますが、1つでもアタッチされているコンテナが残っていると削除に失敗し、ホストの docker デーモンに不要な docker network が溜まっていくので、削除前にデタッチしておきます
-
if: ${{ always() }}
で前のステップが失敗した場合も必ず実行されるようにします
ネットワークからの切断部分はもう少しエレガントに書きたいところですが、まだ Composite Actions では Post step を定義できないようなので妥協しました。
まとめ
- コンテナ化した self-hosted runner からテスト用のコンテナをDooDで起動した
- self-hosted runner から別のコンテナへ接続するために docker network を制御する step を追加した
- VM 上や GitHub-managed なランナーでのコンテナ起動とほぼ変わらない使用感を実現できた
- テストが終わったら綺麗さっぱり消えてくれるので便利!
おまけ
Actions Runner Controller (ARC) を使えばもっと上手く解決できるのかもしれない