Gitlab Runner で concurrent が 1 より大きい場合、パイプラインを 1 つの Runner で実行するよう構成したとしてもジョブ間でキャッシュが共有されないことがあります。
例えば concurrent = 4 な Runner で次のパイプラインを実行します。
image: alpine stages: - stage1 - stage2 1A: stage: stage1 tags: [ore] cache: key: {} script: - echo ok 1B: stage: stage1 tags: [ore] cache: key: hoge paths: - cache.txt script: - touch cache.txt - cat cache.txt - echo "$CI_JOB_NAME" > cache.txt 2A: stage: stage2 tags: [ore] cache: key: hoge paths: - cache.txt script: - touch cache.txt - cat cache.txt - echo "$CI_JOB_NAME" > cache.txt
1B と 2A は stage が異なる& key: hoge でキャッシュキーを固定値にしているのでキャッシュが共有されそうですが、実際には共有されません。
concurrent = 4 の場合、Runner が 1 つしか登録されていなくても実際には下記の 4 つの Runner が存在するのと同じような状態になっているためです。
runner-XXXXXXXX-project-N-concurrent-0runner-XXXXXXXX-project-N-concurrent-1runner-XXXXXXXX-project-N-concurrent-2runner-XXXXXXXX-project-N-concurrent-3
ジョブ開始時の runner-XXXXXXXX-project-N-concurrent-0 via ... のようなメッセージでどの Runner で実行されているかわかります。
キャッシュはこのそれぞれの Runner ごとに保持されます。そのため、↑のパイプラインだと次のように Runner とジョブが対応して実行されるので、
runner-XXXXXXXX-project-N-concurrent-0: 1A -> 2Arunner-XXXXXXXX-project-N-concurrent-1: 1B
1A と 2A ならキャッシュが共有されますが、1B とは共有されません。
.gitlab-ci.yml 上で 1A と 1B の位置を入れ替えたり、あるいは同じ Runner で実行されている別のジョブがあってたまたま 1B と 2A が同じ runner-XXXXXXXX-project-N-concurrent-? で実行されれば共有されることもあります。
Docker executor のキャッシュの保存先
ジョブが実行されるコンテナは --cache-dir で指定されたコンテナ内のパスにキャッシュのアーカイブを保存します。これはデフォルトでは /cache です。
ジョブの実行時、--docker-volumes で指定されたディレクトリがジョブを実行するコンテナにマウントされます。これは <host-path>:<path> の形式と <path> の形式で指定することができて、<host-path>:<path> の形式ならホストのディレクトリがコンテナに bind マウントされます。
<path> の形式の場合、--docker-cache-dir が指定されていれば、そのディレクトリの中の runner-<short-token>-project-<id>-concurrent-<job-id>/<unique-id> のようなサブディレクトリをコンテナにマウントします。--docker-cache-dir が指定されていなければ runner-<short-token>-project-<id>-concurrent-<job-id>-cache-<unique-id> のような名前のデータボリュームコンテナを作成して、そのボリュームをコンテナにマウントします。ただし --docker-disable-cache が指定されていると --docker-cache-dir が指定されていてもデータボリュームコンテナが作成され、かつ、そのデータボリュームコンテナはジョブの終了時に自動で削除されます。
<unique-id> の部分はコンテナのパス(<path>)に基づくハッシュ値です。
要約すると・・・
- ジョブのコンテナは
--cache-dirのパスにアーカイブを保存する- デフォルトは
/cache
- デフォルトは
--docker-volumesが・・<host-path>:<path>の形式なら・・- ホストのディレクトリをジョブのコンテナにマウントする
<path>の形式なら・・--docker-disable-cacheが指定されていれは・・- データボリュームコンテナを作成してボリュームをジョブのコンテナにマウントする
- ジョブの終了時に自動で削除される
--docker-cache-dirが指定されていれば・・- そのディレクトリのサブディレクトリをジョブのコンテナにマウントする
--docker-cache-dirが未指定なら- データボリュームコンテナを作成してボリュームをジョブのコンテナにマウントする
- デフォルトは
/cache
デフォルトの動きは次のようになります。
- ジョブのコンテナは
/cacheにアーカイブを保存する - ジョブの開始時にデータボリュームコンテナを作成して
/cacheにマウントする
Docker executor でジョブ間でキャッシュを共有
--docker-volumes でホストのディレクトリを /cache にマウントすれば runner-<short-token>-project-<id>-concurrent-<job-id>/<unique-id> のようなサブディレクトリは作成されないため(サブディレクトリが作成されるのは <path> の形式のときだけだから)、前述のような異なる Runner の concurrent になってもジョブ間でキャッシュが共有されるようになります。
gitlab-runner register \ --non-interactive \ --url "$url" \ --registration-token "$token" \ --name 'ore-no-runner' \ --executor 'docker' \ --run-untagged \ --tag-list 'ore' \ --docker-image 'alpine:latest' \ --docker-volumes '/srv/gitlab-runner/cache:/cache'
ただ、並列に実行される複数のジョブが同じアーカイブを読み書きしようとするだろうので、壊れたアーカイブが作成されたりしてしまいそうです、たぶんこれはやめておいたほうが良いでしょう。
Docker executor のビルドディレクトリ
--docker-disable-cache や --docker-cache-dir の指定はソースがチェックアウトされるディレクトリにも関係します。これらの指定に基づいてソースファイルをチェックアウトするためのデータボリュームやホストのディレクトリのマウントが行われます(cache という名のついたパラメータなのに build ディレクトリに関係するのわかりにくい・・・)。
--build-dir でコンテナのどこにマウントするかを指定できます(デフォルトは /builds)。このディレクトリに /builds/<namespace>/<project-name> のようにサブディレクトリを掘ってチェックアウトされます。
つまり、<--docker-cache-dir>/runner-<short-token>-project-<id>-concurrent-<job-id>/<unique-id>/ のようなホストのディレクトリ、または、runner-<short-token>-project-<id>-concurrent-<job-id>-cache-<unique-id> のようなデータボリュームが、ジョブのコンテナの /builds/<namespace>/<project-name> にマウントされます。
なお、/cache とは異なり、--docker-volumes でホストのディレクトリを /builds にマウントしている場合は /builds/<short-token>/<concurrent-id>/<namespace>/<project-name> のようなサブディレクトリが掘られてそこにチェックアウトされるようになります。
分散キャッシュ
--docker-volumes で変なことをやらなくても、分散キャッシュを使えばジョブ間でキャッシュを共有させられます。
S3 や Google Cloud Storage を使えば楽そうですけど minio でホストさせても OK です。
# minio のコンテナを開始 docker run --detach \ --name minio \ --hostname minio \ --restart always \ --publish 9005:9000 \ --volume /srv/minio/root/.minio:/root/.minio \ --volume /srv/minio/export:/export \ minio/minio:latest server /export # バケット用のディレクトリを作成 sudo mkdir /srv/minio/export/runner # ホストの IP アドレスをメモる hostname -i # アクセスキーとシークレットキーをメモる docker exec minio cat /export/.minio.sys/config/config.json | grep Key # Runner を登録 gitlab-runner register \ --non-interactive \ --url "$url" \ --registration-token "$token" \ --request-concurrency 4 \ --name 'ore-no-runner' \ --executor 'docker' \ --run-untagged \ --tag-list 'ore' \ --docker-image 'alpine:latest' \ --docker-cache-dir '/srv/gitlab-runner/cache' \ --cache-type 's3' \ # minio の IP アドレスとポート番号 --cache-s3-server-address '192.0.2.123:9005' \ # アクセスキーとシークレットキー --cache-s3-access-key 'abc...' \ --cache-s3-secret-key 'abc...' \ # バケット名 --cache-s3-bucket-name 'runner' \ --cache-s3-insecure true
Gitlab Runner も Docker で実行して minio と同じネットワークに入れれば --publish 9005:9000 などせずに --cache-s3-server-address 'minio:9000' で大丈夫かと思いきやそんなことはありません(最初そうしようとしてあれー?と思いました)。
minio にはジョブとして実行されるコンテナの中からアクセスできる必要があり、ジョブのコンテナを minio と同じネットワークにはできないので、minio で --publish 9005:9000 のようにポートを晒して --cache-s3-server-address '192.0.2.123:9005' のようにホストのアドレス・ポートを指定する必要があります。
参考
- https://docs.gitlab.com/runner/executors/docker.html#the-persistent-storage
- Gitlab Runner の永続化ストレージについての説明
- https://gitlab.com/gitlab-org/gitlab-runner/blob/v12.4.1/executors/docker/internal/volumes/manager.go#L55
- Gitlab Runner のボリューム関係のコード
- https://docs.gitlab.com/runner/install/registry_and_cache_servers.html#install-your-own-cache-server
- minio で分散キャッシュを作る方法