GitLab CI/CD での依存関係のキャッシュ
GitLabCI/CDは、ジョブ実行の際に時間を節約できる、キャッシュの仕組みを提供しています。
キャッシュとは、前のジョブで利用したコンテンツを再利用することで、ジョブの実行時間を短縮することです。 ビルド時にインターネット経由で取得した他のライブラリに依存するソフトウェアを開発している場合などには特に有効です。
キャッシュを有効にすると、GitLab9.0以降ではデフォルトで、パイプラインとジョブ間のプロジェクトレベルでキャッシュが共有されます。 プロジェクト間では共有されません。
.gitlab-ci.yml
でどのように定義されているかについては、cache
リファレンス を必ずお読みください。
キャッシュ vs アーティファクト
キャッシュは、プロジェクトのコンパイルに必要なランタイムの依存関係を保存するように設計されているので、ステージ間でのアーティファクトの受け渡しには使用しないでください。
-
cache
プロジェクトの依存関係を保存します。キャッシュは、ダウンロードした依存関係を保存することで、後続のパイプラインでのジョブの実行を高速化するために使用され、インターネットから再度取得する必要がないようにします (npmパッケージやGoベンダーパッケージなど)。キャッシュはステージ間で中間的なビルド結果を渡すように設定できますが、これは代わりにアーティファクトで処理すべきです。
-
artifacts
ステージ間で受け渡されるステージ結果に使用します。アーティファクトは、ジョブによって生成されたファイルで、保存およびアップロードされ、同じパイプラインの後のステージのジョブから取得して使用できます。 言い換えれば、ステージ1のジョブAでアーティファクトを作成し、ステージ1のジョブBでこのアーティファクトを使用できません。このデータは異なるパイプラインでは利用できませんが、UIからダウンロード可能になっています。
artifacts
という名前は、最終的な画像のダウンロードのように、ジョブの外部でのみ利用できるように聞こえますが、アーティファクトはパイプライン内の後のステージでも利用できます。 したがって、必要なモジュールをすべてダウンロードすることでアプリケーションをビルドする場合、後続のステージがそれらを利用できるように、それらをアーティファクトとして宣言したいと思うかもしれません。 アーティファクトをあまり長く保持しないように有効期限を宣言したり、どのジョブがアーティファクトをフェッチするかを制御するために依存関係を使用したりするような、いくつかの最適化があります。
キャッシュ
- グローバルまたはジョブごとに定義されていない場合、無効になります (
cache:
を使用)。 - グローバルに有効な場合、
.gitlab-ci.yml
のすべてのジョブで利用できます。 - キャッシュが作成されたのと同じジョブで、後続のパイプラインで使用できます(グローバルに定義されていない場合)。
- Runnerがインストールされている場所に保存され、分散キャッシュが有効になっている場合はS3にアップロードされます。
- ジョブごとに定義されている場合は、以下のように使用されます。
- 後続のパイプラインの同じジョブ単位
- 同一の依存関係を持つ場合、同じパイプライン内の後続のジョブ単位
アーティファクト:
- ジョブごとに定義されていない場合は無効になります (
artifacts:
を使用)。 - グローバルではなく、ジョブごとにのみ有効にできます。
- パイプライン実行中に作成され、現在アクティブなパイプラインの後続のジョブで使用できます。
- 常にGitLab(コーディネーターとしての)にアップロードされています。
- ディスク使用量を制御するために有効期限を持つことができます(デフォルトでは30日)。
良いキャッシングの実践例
開発者(ジョブ内でキャッシュを消費する)の視点から見たキャッシュと、Runnerの視点から見たキャッシュがあります。 どのタイプのRunnerを使用しているかによって、キャッシュは異なる動作が可能となります。
開発者の観点から、キャッシュの最大可用性を確保するために、ジョブでcache
:
- Runnerにタグを付け、キャッシュを共有しているジョブでタグを使用します。
- 特定のプロジェクトでのみ利用可能なSticky Runnerを使用します。
-
あなたのワークフローに合った
key
を使ってください(たとえば、ブランチごとに異なるキャッシュを使うなど)。そのためには、CI/CD の定義済み変数を活用できます。
Runnerのキャッシュが効果的に機能するためには、以下のうちのどれかが満たされていなければなりません。
- すべてのJobでシングル構成のRunnerを使用する。
- (GitLab.comの共有Runnerのように)S3バケットに保存されているような分散型キャッシュを使用した複数のRunner(オートスケールモードかどうかは不問)を使用する。
- キャッシュが保存される共通のネットワークマウントされたディレクトリ(NFSなどを使用)を共有する同じアーキテクチャの(オートスケールモードではない)複数のRunnerを使用する。
同じブランチ間でのキャッシュの共有
key: ${CI_COMMIT_REF_SLUG}
でキャッシュを定義し、各ブランチのジョブが常に同じキャッシュを使用するようにします:
cache:
key: ${CI_COMMIT_REF_SLUG}
これはキャッシュを誤って上書きしてしまうことを防ぐための安全策のように考えられるかもしれませんが、マージリクエストの最初のパイプラインが遅くなることを意味し良くない実装となります。 次に新しいコミットがブランチにプッシュされたときには、キャッシュが再利用されます。
ジョブ単位およびブランチ単位のキャッシングを有効にしたい場合:
cache:
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
ブランチごと、ステージごとのキャッシングを有効にしたい場合:
cache:
key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
異なるブランチ間でのキャッシュの共有
キャッシングするファイルをすべてのブランチとすべてのジョブで共有する必要がある場合は、すべてのブランチで同じキーを使用できます。
cache:
key: one-key-to-rule-them-all
ブランチ間で同じキャッシュを共有するが、ジョブごとに分離したい場合:
cache:
key: ${CI_JOB_NAME}
特定のジョブでのキャッシュの無効化
キャッシュをグローバルに定義している場合、各ジョブが同じ定義を使用することを意味します。 しかし、ジョブごとにオーバーライドできます。完全に無効にしたい場合は以下のように空のハッシュを使用してください。
job:
cache: {}
グローバル設定を継承しながら、ジョブごとに特定の設定をオーバーライドする場合
アンカーを使用することで、グローバルキャッシュを上書きすることなく、キャッシュの設定を上書きすることができます。 たとえば、1つのジョブに対してpolicy
:
cache: &global_cache
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- public/
- vendor/
policy: pull-push
job:
cache:
# inherit all global cache settings
<<: *global_cache
# override the policy
policy: pull
微調整については、cache: policy
もご参照ください。
一般的な使用例
キャッシュの最も一般的な使用例は、依存関係や一般的に使用されるライブラリ(Node.jsパッケージ、PHPパッケージ、rubygems、Pythonライブラリなど)のような前後関係があるジョブ間で利用されるコンテンツを保存することで、それらをインターネットから再取得する必要がないようにしています。
Node.js の依存関係のキャッシュ
あなたのプロジェクトがNode.jsの依存関係をインストールするためにnpmを使用していると仮定すると、次の例ではすべてのジョブがそれを継承するようにcache
をグローバルに定義します。デフォルトでは、npmはホームフォルダ~/.npm
にキャッシュデータを格納しますが、プロジェクトディレクトリの外にあるものをキャッシュすることはできないので、代わりに./.npm
を使用するようにnpmに指示し、ブランチごとにキャッシュされるようにします:
#
# https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
#
image: node:latest
# Cache modules in between jobs
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/
before_script:
- npm ci --cache .npm --prefer-offline
test_async:
script:
- node ./specs/start.js ./specs/async.spec.js
PHP の依存関係をキャッシュする
プロジェクトが Composerを使ってPHP 依存モジュールをインストールしているとすると、次の例ではcache
をグローバルに定義して、すべてのジョブがそれを継承するようにしています。 PHP ライブラリモジュールはvendor/
にインストールされ、ブランチごとにキャッシュされます:
#
# https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
#
image: php:7.2
# Cache libraries in between jobs
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- vendor/
before_script:
# Install and run Composer
- curl --show-error --silent https://getcomposer.org/installer | php
- php composer.phar install
test:
script:
- vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never
Python の依存関係をキャッシュする
あなたのプロジェクトがPythonの依存関係をインストールするためにpipを使用していると仮定すると、以下の例ではcache
をグローバルに定義し、すべてのジョブがそれを継承するようにしています。 Pythonライブラリはvenv/
の仮想環境にインストールされ、pipのキャッシュは.cache/pip/
の下に定義され、どちらもブランチごとにキャッシュされます:
#
# https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml
#
image: python:latest
# Change pip's cache directory to be inside the project directory since we can
# only cache local items.
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
# Pip's cache doesn't store the python packages
# https://pip.pypa.io/en/stable/reference/pip_install/#caching
#
# If you want to also cache the installed packages, you have to install
# them in a virtualenv and cache it as well.
cache:
paths:
- .cache/pip
- venv/
before_script:
- python -V # Print out python version for debugging
- pip install virtualenv
- virtualenv venv
- source venv/bin/activate
test:
script:
- python setup.py test
- pip install flake8
- flake8 .
Ruby の依存関係をキャッシュする
プロジェクトがgemの依存関係をインストールするためにBundlerを使用していると仮定すると、次の例ではすべてのジョブがそれを継承するようにグローバルにcache
。gemはvendor/ruby/
、ブランチごとにキャッシュされます:
#
# https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
#
image: ruby:2.6
# Cache gems in between builds
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- vendor/ruby
before_script:
- ruby -v # Print out ruby version for debugging
- bundle install -j $(nproc) --path vendor/ruby # Install dependencies into ./vendor/ruby
rspec:
script:
- rspec spec
Goの依存関係をキャッシュする
プロジェクトがGoモジュールを使ってGoの依存関係をインストールしていると仮定すると、次の例ではgo-cache
テンプレートでcache
を定義しています。どのジョブもこのテンプレートを拡張できます。Goモジュールは${GOPATH}/pkg/mod/
にインストールされ、go
プロジェクトのすべてにキャッシュされます:
.go-cache:
variables:
GOPATH: $CI_PROJECT_DIR/.go
before_script:
- mkdir -p .go
cache:
paths:
- .go/pkg/mod/
test:
image: golang:1.13
extends: .go-cache
script:
- go test ./... -v -short
キャッシュの可用性
キャッシングは最適化されていますが、常に動作するとは限らないので、キャッシュされたファイルを必要とするジョブごとに再生成する準備をしておく必要があります。
ワークフローに従って.gitlab-ci.yml
でcache
を適切に定義したと仮定すると、キャッシュの可用性は最終的に Runner がどのように構成されているか(executor のタイプと、ジョブ間でキャッシュを渡すために異なる Runner が使用されているかどうか)に依存します。
キャッシュの格納場所
Runnerはキャッシュを保存する責任があるので、キャッシュがどこに保存されているかを知ることは不可欠です。.gitlab-ci.yml
のジョブの下で定義されたすべてのキャッシュパスは、1つのcache.zip
ファイルにアーカイブされ、Runnerの設定されたキャッシュの場所に保存されます。デフォルトでは、それらはRunnerがインストールされているマシンにローカルに保存され、executorのタイプに依存します。
GitLab Runnerのexecutor | キャッシュのデフォルトパス |
---|---|
シェル | 内部では、gitlab-runner ユーザーのホームディレクトリ/home/gitlab-runner/cache/<user>/<project>/<cache-key>/cache.zip に保存されています。
|
docker | 内部ではDockerボリュームに格納されています:/var/lib/docker/volumes/<volume-id>/_data/<user>/<project>/<cache-key>/cache.zip .
|
Dockerマシン(オートスケールランナー) | Docker executorと同じ動作をします。 |
アーカイブと抽出の仕組み
Runnerがインストールされているマシンを1台だけ使用し、プロジェクトのすべてのジョブを同じホスト上で実行しているという最もシンプルなシナリオで説明します。
2つの連続したステージに属する2つのジョブの例を見てみましょう。
stages:
- build
- test
before_script:
- echo "Hello"
job A:
stage: build
script:
- mkdir vendor/
- echo "build" > vendor/hello.txt
cache:
key: build-cache
paths:
- vendor/
after_script:
- echo "World"
job B:
stage: test
script:
- cat vendor/hello.txt
cache:
key: build-cache
中ではこんなことが起きています。
- パイプライン開始。
-
job A
が走ります。 -
before_script
が実行されます。 -
script
が実行されます。 -
after_script
が実行されます。 -
cache
が実行され、vendor/
ディレクトリがcache.zip
にzip圧縮されます。 このファイルは、Runnerの設定に基づいてディレクトリに保存され、cache: key
。 -
job B
が走ります。 - キャッシュが抽出されます(見つかった場合)。
-
before_script
が実行されます。 -
script
が実行されます。 - パイプライン完了
1台のマシンで1つのRunnerを使用することで、job A
と異なるRunnerでjob B
が実行されるイシューは発生しません。そのため、ステージ間のキャッシュが保証されます。これは、ビルドが同じRunner/マシンでステージbuild
からtest
まで実行される場合にのみ機能します。そうでない場合は、キャッシュが利用できない可能性があります。
キャッシングのプロセスで、考慮すべき点もいくつかあります。
- 別のキャッシュ設定を持つ他のジョブが同じzipファイルにキャッシュを保存していた場合、上書きされます。 S3ベースの共有キャッシュを使用している場合、ファイルはキャッシュキーに基づいたオブジェクトに追加してS3にアップロードされます。 そのため、異なるパスで同じキャッシュキーを持つ2つのジョブは、それらのキャッシュを上書きします。
-
cache.zip
からキャッシュを展開するとき、zip ファイル内のすべてはジョブの作業ディレクトリ(通常はプルダウンされたリポジトリ)に展開され、job A
のアーカイブがjob B
のアーカイブを上書きしても Runner は気にしません。
このように動作する理由は、あるランナーのために作成されたキャッシュが異なるアーキテクチャで動作する別のランナーで使用された場合(例:キャッシュにバイナリファイルが含まれている場合)、有効にならないことが多いからです。 また、異なるステップは、異なるマシン上で動作するRunnerによって実行される可能性があるため安全を期してこのようになっています。
キャッシュの不一致
次の表では、不一致に陥る可能性のあるいくつかの理由と、それを修正する方法についてのいくつかのアイデアを案内しています。
キャッシュの不一致の理由 | 修正方法 |
---|---|
1つのプロジェクトに接続された複数のスタンドアロンRunner(オートスケールモードではない)を共有キャッシュなしで使用します。 | プロジェクトに1つのRunnerのみを使用するか、分散キャッシュを有効にして複数のランナーを使用します。 |
分散キャッシュを有効にせずにオートスケールモードでRunnerを使用する場合 | 分散キャッシュを使用するようにオートスケールRunnerを設定します。 |
Runnerがインストールされているマシンのディスク容量が少ないか、分散キャッシュを設定している場合、キャッシュが保存されているS3バケットに十分な容量がない。 | 新しいキャッシュを保存できるようにスペースを空けておきましょう。 現状では自動化できる方法はありません。 |
異なるパスをキャッシュするジョブには、同じkey を使用します。
| キャッシュアーカイブが別の場所に保存され、間違ったキャッシュを上書きしないように、異なるキャッシュキーを使用します。 |
いくつかの例をみてみましょう。
使用例
プロジェクトに割り当てられたRunnerが1つだけで、キャッシュはデフォルトでRunnerのマシンに保存されるとします。 2つのジョブ、AとBが同じキャッシュキーを持ち、異なるパスをキャッシュする場合、それらのpaths
が一致しなくても、キャッシュBはキャッシュAを上書きします:
job A
、job B
、2回目のパイプライン実行時にキャッシュを再利用するようにします。
stages:
- build
- test
job A:
stage: build
script: make build
cache:
key: same-key
paths:
- public/
job B:
stage: test
script: make test
cache:
key: same-key
paths:
- vendor/
-
job A
が走ります。 -
public/
は cache.zip としてキャッシュされます。 -
job B
が走ります。 - 前のキャッシュがあれば、それを解凍します。
-
vendor/
は cache.zip としてキャッシュされ、前のものを上書きします。 - 次に
job A
を実行すると、job B
のキャッシュが使用されます。キャッシュは異なるため、効果がありません。
それを解決するには、ジョブごとにkeys
。
別のケースとして、プロジェクトに複数のRunnerが割り当てられているが、ディストリビューションキャッシュが有効になっていないとします。 2回目のパイプラインの実行時に、job A
、job B
、キャッシュを再利用するようにします(この場合、キャッシュは異なるものになります):
stages:
- build
- test
job A:
stage: build
script: build
cache:
key: keyA
paths:
- vendor/
job B:
stage: test
script: test
cache:
key: keyB
paths:
- vendor/
その場合、key
が異なっていても(上書きの心配はありません)、後続のパイプラインでジョブが異なる Runner 上で実行されると、各ステージの前にキャッシュされたファイルが「クリーニングされる」ことを経験するかもしれません。
キャッシュのクリア
GitLab Runnersはキャッシュを使用して、既存のデータを再利用してジョブの実行を高速化します。 しかし、これは時に一貫性のない動作を引き起こす可能性があります。
キャッシュの新しいコピーから始めるには、2つの方法があります。
を変更してキャッシュをクリアします。cache:key
cache: key
.gitlab-ci.yml
次のパイプラインの実行では、キャッシュは別の場所に保存されます。
キャッシュを手動でクリアする
GitLab 10.4 で導入されました。
.gitlab-ci.yml
の編集を避けたい場合は、GitLabのUIから簡単にキャッシュをクリアできます:
- プロジェクトのCI/CD > パイプラインページに移動します。
- Runnerのキャッシュを削除ボタンをクリックして、キャッシュを削除します。
- 次のプッシュで、CI/CDジョブは新しいキャッシュを使用します。
舞台裏では、データベースのカウンターを増加させ、そのカウンターの値を使用して、-1
、-2
などの整数を付加してキャッシュのキーを作成します。プッシュの後、新しいキーが生成され、古いキャッシュは無効になります。