コンテナで動かしたGithub Actionsセルフホストランナーでrootless dockerを利用する検証
先日の自分の趣味全開で主催させてもらったCI/CD Test Night #6で高まったテンションが冷めやらぬうちにs4ichiさんの発表にあったrootless dockerを自分のところでも再現できるかを検証している
具体的にこのスライドのp25の構成のように、ランナーを動かすコンテナとは別にsidecardでrootelss dockerの公式イメージを動かして接続させるというもの。
s4ichiさんの構成はECS on EC2なので最終的には同じようにECS上で再現させたいが、まずは仕組みを理解するためにdocker composeによる再現を目指す。
さらにその前段として、セルフホストランナーは関係無しにコンテナの間同士でdocker(クライアント) -> rootless docker(サーバー)の接続をする方法を検証した。
結論、このようなcompose.ymlで可能であることが確認できた。
services:
docker: # クライアントから接続する際にtcp://docker:2376にする必要があるので `docker` である必要がある
image: docker:24.0-dind-rootless
privileged: true
ports:
- "2376:2376" # デフォルトで2376ポートがlistenされている
networks:
- dind-rootless
volumes:
# dind-rootlessが作成してくれる証明書をクライアント側に渡すためにボリュームをマウントする
# https://github.com/docker-library/docker/blob/679ff1a932a1bdf4341fb0d5cb06f088c77d44bf/24/dind/dockerd-entrypoint.sh#LL130C1-L130C1
- type: volume
source: rootless-certs
target: /certs
client:
image: docker:24.0-dind-rootless
# docker exec でコンテナ内に入って実験するのでentrypointは何でもよいがとりあえずsh
tty: true
entrypoint: sh
environment:
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: /rootless-certs/client/
DOCKER_HOST: tcp://docker:2376 # ホスト名がdockerでないと証明書の検証が通らない。例えばtcp://dind:2376にするとこのようなエラーになる
# ERROR: error during connect: Get "https://dind:2376/v1.24/info": tls: failed to verify certificate: x509: certificate is valid for docker, f54ac70a5c66, localhost, not dind
networks:
- dind-rootless
volumes:
# 同じボリュームをマウントすることで証明書を持ち込む
- type: volume
source: rootless-certs
target: /rootless-certs
depends_on:
- docker
networks:
dind-rootless:
volumes:
rootless-certs:
サーバーとクライアントのコンテナを起動させる
docker compose up
クライアントのコンテナに入ってdockerコマンドを実行できるか試す
$ docker exec -it client-1 sh
/ $ docker version
Client:
Version: 24.0.2
API version: 1.43
Go version: go1.20.4
Git commit: cb74dfc
Built: Thu May 25 21:50:49 2023
OS/Arch: linux/arm64
Context: default
Server: Docker Engine - Community
Engine:
Version: 24.0.2
API version: 1.43 (minimum version 1.12)
Go version: go1.20.4
Git commit: 659604f
Built: Thu May 25 21:34:53 2023
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: v1.7.1
GitCommit: 1677a17964311325ed1c31e2c0a3589ce6d5c30d
runc:
Version: 1.1.7
GitCommit: v1.1.7-0-g860f061
docker-init:
Version: 0.19.0
GitCommit: de40ad0
rootlesskit:
Version: 1.1.0
ApiVersion: 1.1.1
NetworkDriver: vpnkit
PortDriver: builtin
StateDir: /tmp/rootlesskit1117617536
vpnkit:
Version: 7f0eff0dd99b576c5474de53b4454a157c642834
/ $ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
Digest: sha256:dfd64a3b4296d8c9b62aa3309984f8620b98d87e47492599ee20739e8eb54fbf
Status: Image is up to date for ubuntu:latest
docker.io/library/ubuntu:latest
/ $ docker run ubuntu echo "hello dind"
hello dind
どちらも同じ docker:24.0-dind-rootless
のイメージを使っているので、うっかりクライアント側でもdockerが動いてしまっていてそれを使っているだけなのでは?というわけではないことも確認しておく。
試しに DOCKER_HOST
の環境変数を消してみるとちゃんとdockerに接続できない、というエラーがちゃんと出た。
/ $ unset DOCKER_HOST
/ $ docker version
Client:
Version: 24.0.2
API version: 1.43
Go version: go1.20.4
Git commit: cb74dfc
Built: Thu May 25 21:50:49 2023
OS/Arch: linux/arm64
Context: default
Cannot connect to the Docker daemon at tcp://localhost:2376. Is the docker daemon running?
このcompose.ymlでなぜ動くのかを解説する。
まずサーバーとクライアントのコンテナ同士が通信できるように同じ dind-rootless
ネットワークに入れておく。
サーバーへの接続をTCPで行う方法だが、rootless dockerのドキュメントを見るとそれが書いてあるが、正直全く役に立たなかった。
docker:24.0-dind-rootless
イメージを使う場合、何らかのentrypointが既に決まっているのでコンテナを起動すればrootless dockerが起動するのでどうやってオプションを渡せるのかが不明だし、自分でentrypointを書き換えようと思ってもそもそも dockerd-rootless.sh
なるものがどこにあるのかが不明。
このイメージのDockerfile周りのコードを読んでみたところ、そもそもデフォルトでTCPに公開するための証明書の生成とかをやっていることがわかった。さらに DOCKER_TLS_CERTDIR=''
にしておけばTLS無しでTCPの公開も可能らしい。(ちなみに試してみると、この方法は危険だしdeprecatedだから!というメッセージがログに出てきて、かなりの温度感で非推奨であることを伝えてくれる)
TCPを公開するポートが2376であることもこのコードが分かるため、compose.ymlでも2376を公開しておく。
次はこの立ち上げたrootless dockerのサーバーに、dockerクライアント側のコンテナからTCPで接続させる。
サーバー側のdockerdはTLSの設定がされているため、クライアントから接続するときにはサーバー側で生成された証明書の情報が必要になる。
このあたりは先ほどのrootless dockerのドキュメントを見ても何も書いていなかったので、こちらの記事が大変参考になりました。
サーバー側のコンテナで生成された証明書をクライアント側のコンテナに持っていくため、両方のコンテナで共通のボリュームをマウントしてそこ経由で証明書を受け渡す。compose.ymlでは rootless-certs
というボリュームをマウントした。
サーバー側では /certs
に証明書が生成されるのでこのパスに対してマウントする。クライアント側では自由なパスを指定できるのでどこでもいい。今回はわかりやすく /rootless-certs
というパスにマウントした。
これでサーバー側では /certs/client
に生成されたファイルがクライアント側では /rootless-certs/client
として見えるようになる。
あとは docker -H tcp://docker:2376
でサーバー側のrootless dockerに接続できるのだが、どうやらこの docker
というホスト名であることが重要らしい。最初はサーバー側のコンテナは dind
という別の名前にしていたのだがこういうエラーになった
# ERROR: error during connect: Get "https://dind:2376/v1.24/info": tls: failed to verify certificate: x509: certificate is valid for docker, f54ac70a5c66, localhost, not dind
エラーによると証明書が有効な名前は docker
, f54ac70a5c66
(ちなみにこれはコンテナを再起動するたびに変わっていた) , localhost
らしい。
自分は恥ずかしながらこのあたりの認証について詳しくないのでよく分かっていないのだが、サーバー側のdockerで証明書を作っているこのあたりのコードが関係してそう?
とりあえず tcp://docker:2376
であればTLSも突破してサーバー側のdockerに接続できることはわかったので、compose.ymlの方でサーバーのコンテナは docker
という名前にしておく。
あとはこのあたりの接続先ホスト、TLS周りの設定をいちいちdockerのオプションとして設定しなくても済むようにクライアント側のコンテナの環境変数に登録して完成。
environment:
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: /rootless-certs/client/
DOCKER_HOST: tcp://docker:2376
これでクライアント側のコンテナでは docker
だけでサーバー側のrootless dockerにTCPで接続してくれる。
全てが上手く噛み合うと docker pull ubuntu
をしたときにサーバー側のrootelss dockerのログが変化するのでちゃんと接続できている様子が確認できるはず。
次はこの単なるクライアント -> サーバーのdockerの接続から発展させて、クライアントのコンテナをgithub actionsのセルフホストランナーが動くコンテナに置き換える。
クライアント側を https://github.com/myoung34/docker-github-actions-runner に置き換えた。
追加した環境変数は myoung34/docker-github-actions-runner に必要なものなので詳しくは向こうのREADMEを参照。
追加のvolumeで/_workをマウントしているのはランナー(dockerクライアント)でのジョブのディレクトリをdockerサーバー側にマウントさせるため。
myoung34/docker-github-actions-runner は RUNNER_WORKDIR: /_work
の環境変数によってジョブ中のディレクトリを /_work
に指定できる。dockerのクライアントとサーバーが別のマシンにある場合、bind mountをするにはディレクトリのパスが一致していないと意図した挙動にならないため、ジョブの中で docker run
でbind mountを行ったり uses
などでコンテナ型のactionを使えるようにするために /_work
のパスでデータを共有している。
(これはかなり分かりにくい挙動だが、macOSのマシンでDocker DesktopではなくてlimaやRancher Desktopを使っているとmacOS <-> limaのVM間でディレクトリをマウントしている理由と同じはず)
services:
docker: # クライアントから接続する際にtcp://docker:2376にする必要があるので `docker` である必要がある
image: docker:24.0-dind-rootless
privileged: true
ports:
- "2376:2376" # デフォルトで2376ポートがlistenされている
networks:
- dind-rootless
volumes:
# dind-rootlessが作成してくれる証明書をクライアント側に渡すためにボリュームをマウントする
# https://github.com/docker-library/docker/blob/679ff1a932a1bdf4341fb0d5cb06f088c77d44bf/24/dind/dockerd-entrypoint.sh#LL130C1-L130C1
- type: volume
source: rootless-certs
target: /certs
# ジョブ内でbind mountを可能にするためにactions用のワークスペースをvolumeで共有して同じパスにマウントする
- type: volume
source: workspace
target: /_work
client:
image: ghcr.io/myoung34/docker-github-actions-runner:2.304.0
environment:
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: /rootless-certs/client/
DOCKER_HOST: tcp://docker:2376 # ホスト名がdockerでないと証明書の検証が通らない。例えばtcp://dind:2376にするとこのようなエラーになる
RUNNER_SCOPE: org
ORG_NAME: ${ORG_NAME}
LABELS: dind-rootless
RUNNER_NAME_PREFIX: container
DISABLE_AUTO_UPDATE: 1
RUN_AS_ROOT: false
ACCESS_TOKEN: ${GITHUB_PAT}
RUNNER_WORKDIR: /_work
networks:
- dind-rootless
volumes:
# 同じボリュームをマウントすることで証明書を持ち込む
- type: volume
source: rootless-certs
target: /rootless-certs
# ジョブ内でbind mountを可能にするためにactions用のワークスペースをvolumeで共有して同じパスにマウントする
- type: volume
source: workspace
target: /_work
depends_on:
- docker
networks:
dind-rootless:
volumes:
rootless-certs:
workspace:
このcompose.ymlの設定で立ち上げたランナーは以下のdockerやコンテナを使うワークフローのうち、 run_container
を除いて完走できる。
つまり以下のdockerの使い方までは動くことを確認できた。
- 単純にdocker info
- bind mount込みのdocker run
- 読み込みはできたが書き込みはpermission deniedで不可能だった。要調査
- mysql, goのサーバー, nginxの組み合わせのdocker compose
- ランナーを立ち上げたcompose側のネットワークの設定上、localhost:80ではアクセスできずdocker:80である必要がある
-
docker/setup-buildx-action
とdocker/build-push-action
が使える- setup-buildx-actionの前に
docker context create
によるワークアラウンドが必要 - 原因がよく分かっていないがおそらくCircleCIでbuildxを使う場合に必要なワークアラウンドと同じっぽい https://support.circleci.com/hc/ja/articles/360058095471-Remote-Docker-で-Buildx-を利用したい
- setup-buildx-actionの前に
唯一完走できない run_container
は以下のエラーになる。
Container feature is not supported when runner is already running inside container.
これはdindやrootless dockerが原因ではなく、ランナーの actions/runnner の方でチェックされて制限されているもの。この問題を突破するにはdindだけでは不可能で https://github.com/actions/runner-container-hooks を利用する必要がある。
name: "Docker and container hooks test"
on:
workflow_dispatch:
jobs:
docker:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
runs-on:
- self-hosted
- ${{ matrix.docker }}
timeout-minutes: 10
steps:
- run: env
- name: Show docker info
run: |
docker -v
docker info
docker ps
- run: ls
- run: pwd
docker-bind-mount:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
runs-on:
- self-hosted
- ${{ matrix.docker }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
# Bind mount to read
- run: docker run --rm -v $(pwd):/workdir:ro -w /workdir busybox ls README.md
# Bind mount to write
# NOTE: Permission deniedになってしまう。rootlessだとrootユーザーが使えないからだと思われるが解決方法が分からないので保留
# - run: docker run --rm -v $(pwd):/workdir -w /workdir busybox touch foo
# - run: ls -l foo
run_container:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
runs-on:
- self-hosted
- ${{ matrix.docker }}
services:
redis:
image: redis
container:
image: ubuntu
options: --cpus 1
timeout-minutes: 10
steps:
- run: env
- run: ls
- run: pwd
docker-compose:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
concurrency: docker-compose
runs-on:
- self-hosted
- ${{ matrix.docker }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
repository: docker/awesome-compose
- name: Show docker compose
run: docker compose version
# dindでdocker自体へtcp://docker:2376で接続しているため、同様にlocalhostではなくdocker:8080にする必要がある
# compose.yml側でブリッジのネットワークにすればlocalhsotで接続できるようになる?
- name: docker compose
run: |
cd nginx-golang-mysql
docker compose up --wait
curl http://docker:80
docker-build-and-push:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
runs-on:
- self-hosted
- ${{ matrix.docker }}
timeout-minutes: 10
steps:
# NOTE: rootlessだと動かないので諦める
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v2
# TCPで接続しているdockerの場合だと以下のエラーが出るっぽい?のでその対策
# CircleCIであれば docker context create circleci && docker buildx create --use circleci で対応したワークアラウンド
# ERROR: could not create a builder instance with TLS data loaded from environment.
# Please use `docker context create <context-name>` to create a context for current environment and then create a builder instance with `docker buildx create <context-name>`
- name: Setup docker context
run: docker context create actions_context
continue-on-error: true # 2回目以降ではエラーになるが毎回チェックが面倒なのでエラーを無視する
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
endpoint: actions_context
- name: Create simple Dockerfile
run: |
echo -e "FROM busybox\nRUN echo foobar > /hello /\nRUN cat /hello" > Dockerfile
- name: Build and push
uses: docker/build-push-action@v4
with:
context: "."
https://github.com/actions/runner-container-hooks を使うにはこのリポジトリに含まれるファイルをダウンロードする必要があるため、ついに myoung34/docker-github-actions-runner
のイメージをベースに独自のDockerfileを用意する。
runner-container-hooksのスクリプトと必要な環境変数については実は actions/runner のDockerfileにも書かれているのでそのやり方をほぼコピーしている。
ただし actions/runner ではk8s用のスクリプトを使っているのに対してここではdocker用のスクリプトを使用する。
runner-container-hooks の仕組みについては今回は省略するがいずれ独立した記事にしたい。気になる人は runner-container-hooks のtsのコードを見てみると何をやってくれているのかだいたい分かると思う。
FROM myoung34/github-runner:2.304.0
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.3.2
ARG RUNNER_DIR=/actions-runner
RUN cd "${RUNNER_DIR}" \
&& curl -fLo 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
ENV ACTIONS_RUNNER_CONTAINER_HOOKS="${RUNNER_DIR}/runner-container-hooks-docker/index.js"
このDockerfileを使うようにcompose.ymlの一部を書き換える
client:
# image: ghcr.io/myoung34/docker-github-actions-runner:2.304.0
# runner-container-hooksを仕込むためにDockerfileを指定する
build:
context: .
dockerfile: Dockerfile.container_hooks
container-hooksを設定すると run-container
ジョブで先ほどはランナー側で止められてしまっていたエラーは発生しなくなり、代わりにちゃんとdockerのコマンドが実行されているログが出る。
Initialize Container 2s
Run '/actions-runner/runner-container-hooks-docker/index.js'
/usr/bin/docker ps --all --quiet --no-trunc --filter label=636f6e7461696e65722d4263665030306a425764514330
/usr/bin/docker network prune --force --filter label=636f6e7461696e65722d4263665030306a425764514330
/usr/bin/docker network create --label 636f6e7461696e65722d4263665030306a425764514330 github_network_926e2bff-5bc8-42ea-ab12-c7919d363f13
d2671a050a467e8b8ccf920bdd7e9c04de091ee3cce1a0a6637687ec1d882b75
/usr/bin/docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
Digest: sha256:dfd64a3b4296d8c9b62aa3309984f8620b98d87e47492599ee20739e8eb54fbf
Status: Image is up to date for ubuntu:latest
docker.io/library/ubuntu:latest
/usr/bin/docker create --label=636f6e7461696e65722d4263665030306a425764514330 --network=github_network_926e2bff-5bc8-42ea-ab12-c7919d363f13 --name 968c5e18201749c3a28f9f6927d2f341__471de0 --cpus 1 -e HOME -v=/var/run/docker.sock:/var/run/docker.sock -v=/_work:/__w -v=/actions-runner/externals:/__e -v=/_work/_temp:/__w/_temp -v=/_work/_actions:/__w/_actions -v=/opt/hostedtoolcache:/__t -v=/_work/_temp/_github_home:/github/home -v=/_work/_temp/_github_workflow:/github/workflow --entrypoint tail ubuntu -f /dev/null
c42c34280ece0c1e7703ac1609c730e37f415aa8582bca93a78c028c41379a79
/usr/bin/docker start c42c34280ece0c1e7703ac1609c730e37f415aa8582bca93a78c028c41379a79
Error response from daemon: error while creating mount source path '/_work/_actions': mkdir /_work/_actions: permission denied
Error: failed to start containers: c42c34280ece0c1e7703ac1609c730e37f415aa8582bca93a78c028c41379a79
Error: Error: The process '/usr/bin/docker' failed with exit code 1
Error: Process completed with exit code 1.
Error: Executing the custom container implementation failed. Please contact your self hosted runner administrator.
が、ダメ。 Error response from daemon: error while creating mount source path '/_work/_actions': mkdir /_work/_actions: permission denied
このあたりが怪しいが /_work
はすでにvolumeでランナー側とdocker側のコンテナで共有されているはずだし、それぞれのコンテナの中に入って調べてみても実際に /_work/_actions
のディレクトリ自体は存在するのを確認した。
うーん、なぜ?
コンテナの中に入って調べたところ、どうもランナー側のコンテナとdocker側のコンテナの一般ユーザーのuidが異なっているのが原因として考えられそう。
dind-rootlessは一般ユーザーの rootless
はuid:1000だが、myoung34/docker-github-actions-runnerの一般ユーザーの runner
はuid:1001になっている。
どっちのuidに合わせるかなどを考えたが、dind-rootlessのDockerfileはかなり複雑なので自分で手を入れるのは難しそうだったため、myoung34/docker-github-actions-runnerをベースにしている独自のDockerfile側でuidを書き換えることにした。
具体的にはDockerfileの中でusermodでuidを変更し、その他にこのイメージがあらかじめrunnerユーザーで用意しているディレクトリの権限を設定し直す。
RUN usermod -u 1000 runner \
&& chown -R runner:runner /_work \
&& chown -R runner:runner /actions-runner \
&& chown -R runner:runner /opt/hostedtoolcache
(myoung34/docker-github-actions-runnerを使わずに自分で一からDockerfileを書いている人は関係無し。一般ユーザーを作るときにuid:1000にすることだけ注意すれば問題ないはず)
これでいけるかと再びジョブを実行したところ同じInitialize containerステップでエラーの内容が以下に変わった
Error response from daemon: error while creating mount source path '/opt/hostedtoolcache': mkdir /opt/hostedtoolcache: permission denied
Error: failed to start containers: 2d726105da2bb5aee2c038f131f506825507dcd6c39c80f4d103356d31b1d22f
/_work/_actions
がエラーにならなくなったということは今までのPermission deniedの問題はuidを揃えたことでおそらく解消されたと思われる。
/opt/hostedtoolcache
はたしかにランナー側のコンテナに存在するがdocker側にマウントしていなかったので /_work
と同様にdocker側にもマウントが必要っぽい。
というわけであとはcompose.ymlにvolumeを追加し、ランナー側とdocker側で共有するディレクトリを追加したら動いたの。各ファイルの最終的な完成形はこちら
compose.yml
services:
docker: # クライアントから接続する際にtcp://docker:2376にする必要があるので `docker` である必要がある
image: docker:24.0-dind-rootless
privileged: true
ports:
- "2376:2376" # デフォルトで2376ポートがlistenされている
networks:
- dind-rootless
volumes:
# dind-rootlessが作成してくれる証明書をクライアント側に渡すためにボリュームをマウントする
# https://github.com/docker-library/docker/blob/679ff1a932a1bdf4341fb0d5cb06f088c77d44bf/24/dind/dockerd-entrypoint.sh#LL130C1-L130C1
- type: volume
source: rootless-certs
target: /certs
# ジョブ内でbind mountを可能にするためにactions用のワークスペースをvolumeで共有して同じパスにマウントする
- type: volume
source: workspace
target: /_work
- type: volume
source: actions-runner
target: /actions-runner
- type: volume
source: hostedtoolcache
target: /opt/hostedtoolcache
client:
# image: ghcr.io/myoung34/docker-github-actions-runner:2.304.0
# runner-container-hooksを仕込むためにDockerfileを指定する
build:
context: .
dockerfile: Dockerfile.with_rootless
environment:
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: /rootless-certs/client/
DOCKER_HOST: tcp://docker:2376 # ホスト名がdockerでないと証明書の検証が通らない。例えばtcp://dind:2376にするとこのようなエラーになる
RUNNER_SCOPE: org
ORG_NAME: ${ORG_NAME}
LABELS: dind-rootless
RUNNER_NAME_PREFIX: container
DISABLE_AUTO_UPDATE: 1
RUN_AS_ROOT: false
ACCESS_TOKEN: ${GITHUB_PAT}
RUNNER_WORKDIR: /_work
networks:
- dind-rootless
volumes:
# 同じボリュームをマウントすることで証明書を持ち込む
- type: volume
source: rootless-certs
target: /rootless-certs
# ジョブ内でbind mountを可能にするためにactions用のワークスペースをvolumeで共有して同じパスにマウントする
- type: volume
source: workspace
target: /_work
- type: volume
source: actions-runner
target: /actions-runner
- type: volume
source: hostedtoolcache
target: /opt/hostedtoolcache
depends_on:
- docker
networks:
dind-rootless:
volumes:
rootless-certs:
workspace:
actions-runner:
hostedtoolcache:
Dockerfile.with_rootless
# dind-rootlessをsidecarで立ててdockerをTCP経由で使う場合に必要な設定を追加したDockerfile
FROM myoung34/github-runner:2.304.0
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.3.2
# https://github.com/actions/runner-container-hooks
ARG RUNNER_DIR=/actions-runner
RUN cd "${RUNNER_DIR}" \
&& curl -fLo 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
ENV ACTIONS_RUNNER_CONTAINER_HOOKS="${RUNNER_DIR}/runner-container-hooks-docker/index.js"
# ランナーとdockerのコンテナでuidが一致していないとpermission問題が発生するため、dind-rootlessイメージのrootlessユーザーuid=1000に合わせる
RUN usermod -u 1000 runner \
&& chown -R runner:runner /_work \
&& chown -R runner:runner /actions-runner \
&& chown -R runner:runner /opt/hostedtoolcache
このcompose.ymlとDockerfileで立ち上げたランナーは以下のワークフローを完走できる。
ちなみにuidを調整する前は bind mount込みのdocker run 読み込みはできたが書き込みはpermission deniedで不可能だった。要調査
としてスキップしていた run: docker run --rm -v $(pwd):/workdir -w /workdir busybox touch foo
ステップもuidを揃えてからは完走できるようになったのでコメントアウトを解除した。
name: "Docker and container hooks test"
on:
# push:
workflow_dispatch:
jobs:
docker:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
runs-on:
- self-hosted
- ${{ matrix.docker }}
timeout-minutes: 10
steps:
- run: env
- name: Show docker info
run: |
docker -v
docker info
docker ps
- run: ls
- run: pwd
docker-bind-mount:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
runs-on:
- self-hosted
- ${{ matrix.docker }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
# Bind mount to read
- run: docker run --rm -v $(pwd):/workdir:ro -w /workdir busybox ls README.md
# Bind mount to write
- run: docker run --rm -v $(pwd):/workdir -w /workdir busybox touch foo
- run: ls -l foo
run_container:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
runs-on:
- self-hosted
- ${{ matrix.docker }}
services:
redis:
image: redis
container:
image: ubuntu
options: --cpus 1
timeout-minutes: 10
steps:
- run: env
- run: ls
- run: pwd
docker-compose:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
concurrency: docker-compose
runs-on:
- self-hosted
- ${{ matrix.docker }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
repository: docker/awesome-compose
- name: Show docker compose
run: docker compose version
# dindでdocker自体へtcp://docker:2376で接続しているため、同様にlocalhostではなくdocker:8080にする必要がある
# compose.yml側でブリッジのネットワークにすればlocalhsotで接続できるようになる?
- name: docker compose
run: |
cd nginx-golang-mysql
docker compose up --wait
curl http://docker:80
docker-build-and-push:
strategy:
fail-fast: false
matrix:
docker: [dind-rootless]
runs-on:
- self-hosted
- ${{ matrix.docker }}
timeout-minutes: 10
steps:
# NOTE: rootlessだと動かないので諦める
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v2
# TCPで接続しているdockerの場合だと以下のエラーが出るっぽい?のでその対策
# CircleCIであれば docker context create circleci && docker buildx create --use circleci で対応したワークアラウンド
# ERROR: could not create a builder instance with TLS data loaded from environment.
# Please use `docker context create <context-name>` to create a context for current environment and then create a builder instance with `docker buildx create <context-name>`
- name: Setup docker context
run: docker context create actions_context
continue-on-error: true # 2回目以降ではエラーになるが毎回チェックが面倒なのでエラーを無視する
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
endpoint: actions_context
- name: Create simple Dockerfile
run: |
echo -e "FROM busybox\nRUN echo foobar > /hello /\nRUN cat /hello" > Dockerfile
- name: Build and push
uses: docker/build-push-action@v4
with:
context: "."
まとめると、sidecarで立ち上げたrootless dockerを使ってdindする場合に注意するポイントは以下
- rootless dockerは
privileged: true
で起動する必要がある - rootless docker側で生成された/certs/clientの証明書をクライアント側のコンテナにマウントして見えるようにする
- クライアント側ではTCP経由でrootless dockerのコンテナのdockerに接続するように環境変数を設定しておく
- github actionsランナーのワークスペースやその他の関係するディレクトリは全てrootless docker側にも共有されるようにマウントしておく
- 自分の例だと必要なディレクトリのパスは
myoung34/docker-github-actions-runner
が設定しているパスなので、自分でセルフホストランナー用のDockerfileを作成している場合はマウントが必要なパスは異なるはずです
- 自分の例だと必要なディレクトリのパスは
- ランナー側とrootless docker側の一般ユーザーのuidは揃えておく。rootelss docker側がuid:1000なのでこちらに合わせる
あとはこのdocker composeをECSのタスク定義に翻訳できればs4ichiさんの構成を再現できそう。
ネットワークやボリュームはdocker composeとECSで多少異なるはずなので、主にそのあたりで微調整が必要そう。
そして、dockerが使えるところまで実現できればcontainer-hooksの導入はDockerfileにちょっと追加するだけで動く。container-hooksを入れると今まではコンテナで動かすランナーだと不可能だったジョブ中でコンテナを立ち上げる機能が使えるようになるため、VMの上でランナーを動かす場合と機能の差はなくなる。
container-hooksについてはそれ単独でのもう少し詳しい解説をいずれ書く。
おわり
動作確認環境
Windows 11 WSL2
Docker Desktop v4.19.0
$ docker version
Client:
Version: 20.10.21
API version: 1.41
Go version: go1.18.1
Git commit: 20.10.21-0ubuntu1~20.04.2
Built: Thu Apr 27 05:56:19 2023
OS/Arch: linux/amd64
Context: default
Experimental: true
Server: Docker Desktop
Engine:
Version: 23.0.5
API version: 1.42 (minimum version 1.12)
Go version: go1.19.8
Git commit: 94d3ad6
Built: Wed Apr 26 16:17:45 2023
OS/Arch: linux/amd64
Experimental: true
containerd:
Version: 1.6.20
GitCommit: 2806fc1057397dbaeefbea0e4e17bddfbd388f38
runc:
Version: 1.1.5
GitCommit: v1.1.5-0-gf19387a
docker-init:
Version: 0.19.0
GitCommit: de40ad0