GitLab CI/CD での依存関係のキャッシュ

GitLabCI/CDは、ジョブ実行の際に時間を節約できる、キャッシュの仕組みを提供しています。

キャッシュとは、前のジョブで利用したコンテンツを再利用することで、ジョブの実行時間を短縮することです。 ビルド時にインターネット経由で取得した他のライブラリに依存するソフトウェアを開発している場合などには特に有効です。

キャッシュを有効にすると、GitLab9.0以降ではデフォルトで、パイプラインとジョブ間のプロジェクトレベルでキャッシュが共有されます。 プロジェクト間では共有されません。

``キャッシュのリファレンスを読み、.gitlab-ci.ymlでどのように定義されているかを確認してください。

Cache vs artifacts

注意 キャッシュやアーティファクトを使用してジョブに同じパスを保存する場合、キャッシュはアーティファクトの前に復元され、コンテンツ上書きの可能性があり、注意が必要です。

キャッシュは、プロジェクトのコンパイルに必要なランタイムの依存関係を保存するように設計されているので、ステージ間でのアーティファクトの受け渡しには使用しないでください。

  • cache: プロジェクトの依存関係を保存するため

    キャッシュは、ダウンロードした依存関係を保存することで、後続のパイプラインでのジョブの実行を高速化するために使用され、インターネットから再度取得する必要がないようにします (npmパッケージやGoベンダーパッケージなど)。キャッシュはステージ間で中間的なビルド結果を渡すように設定できますが、これは代わりにアーティファクトで処理すべきです。

  • artifacts: ステージ間で渡されるステージ結果に使用します

    アーティファクトは、ジョブによって生成されたファイルで、保存およびアップロードされ、同じパイプラインの後のステージのジョブから取得して使用できます。 言い換えれば、ステージ1のジョブAでアーティファクトを作成し、ステージ1のジョブBでこのアーティファクトを使用できません。このデータは異なるパイプラインでは利用できませんが、UIからダウンロード可能になっています。

アーティファクトという名前は、最終イメージのダウンロードなどジョブの外でしか使えないように聞こえますが、パイプライン内の後続のステージでも利用可能です。 したがって、必要なモジュールをすべてダウンロードしてアプリケーションを構築した場合、後続のステージで利用できるように、それらをアーティファクトとして宣言する必要があるかもしれません。 アーティファクトを長持ちさせないために有効期限を宣言したり、どのジョブがアーティファクトを取得するかを制御するために依存関係を使用したりするなどの最適化もあります。

Caches:

  • グローバルに定義されていない場合、またはジョブごとに定義されていない場合(キャッシュ:を使用している場合)は無効になります。
  • グローバルに有効になっていれば、.gitlab-ci.yml内のすべてのジョブで利用可能です。
  • キャッシュが作成されたのと同じジョブで、後続のパイプラインで使用できます(グローバルに定義されていない場合)。
  • Runnerがインストールされている場所に保存され分散キャッシュが有効になっている場合はS3にアップロードされます。
  • ジョブごとに定義されている場合は、以下のように使用されます。
    • 後続のパイプラインの同じジョブ単位
    • 同一の依存関係を持つ場合、同じパイプライン内の後続のジョブ単位

Artifacts:

  • ジョブごとに定義されていない場合(artifacts:)は無効になります。
  • グローバルではなく、ジョブごとにのみ有効にできます。
  • パイプライン実行中に作成され、現在アクティブなパイプラインの後続のジョブで使用できます。
  • 常にGitLab(コーディネーターとしての)にアップロードされています。
  • ディスク使用量を制御するために有効期限を持つことができます(デフォルトでは30日)。

注: 注意: アーティファクトとキャッシュは、どちらもプロジェクトディレクトリからの相対パスを定義しており、プロジェクト外のファイルにリンクできません。

良いキャッシングの実践例

開発者(ジョブ内でキャッシュを消費する)の視点から見たキャッシュと、Runnerの視点から見たキャッシュがあります。 どのタイプのRunnerを使用しているかによって、キャッシュは異なる動作が可能となります。

開発者の視点では、キャッシュの可用性を最大限に確保するために、ジョブで キャッシュ を宣言する際には以下のいずれかまたは複数を組み合わせて使用します。

:ヒント: パイプラインに同じRunnerを使用することは、1つのステージやパイプラインでファイルをキャッシュし、このキャッシュを後続のステージやパイプラインに保証された方法で渡す最もシンプルで効率的な方法です。

Runnerのキャッシュが効果的に機能するためには、以下のうちのどれかが満たされていなければなりません。

  • すべてのJobでシングル構成のRunnerを使用する。
  • (GitLab.comの共有Runnerのように)S3バケットに保存されているような分散型キャッシュを使用した複数のRunner(オートスケールモードかどうかは不問)を使用する。
  • キャッシュが保存される共通のネットワークマウントされたディレクトリ(NFSなどを使用)を共有する同じアーキテクチャの(オートスケールモードではない)複数のRunnerを使用する。

ヒント:cacheの可用性を読むと、その構造について詳細に学べ、キャッシュがどのように動作するかについてのより良いアイデアを得られるでしょう。

同じブランチ間でのキャッシュの共有

各ブランチのジョブが常に同じキャッシュを利用するように、${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つのジョブのポリシーをオーバーライドしたい場合は以下のように記載します:

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

詳細なチューニングについては、以下の記事もお読みください。キャッシュ: ポリシー.

一般的な使用例

キャッシュの最も一般的な使用例は、依存関係や一般的に使用されるライブラリ(Node.jsパッケージ、PHPパッケージ、rubygems、Pythonライブラリなど)のような前後関係があるジョブ間で利用されるコンテンツを保存することで、それらをインターネットから再取得する必要がないようにしています。

NOTE:Note:より多くの例については、GitLab CI/CDtemplatesを参照してみてください。

Node.js の依存関係のキャッシュ

デフォルトでは、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の依存関係をインストールしている場合、次の例では、すべてのジョブがキャッシュを継承するようグローバルにキャッシュを定義しています。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を使用している場合、以下の例ではすべてのジョブがキャッシュを継承するようグローバルにキャッシュを定義しています。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 の依存関係をキャッシュする

Bundlerを使用してgemの依存関係をインストールしている場合、以下の例では、すべてのジョブがそれを継承するようにグローバルにキャッシュを定義しています。 gemsはベンダー/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キャッシュテンプレートでキャッシュを定義しています。 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で適切にキャッシュを定義していると仮定すると、キャッシュの可用性は最終的にRunnerの設定方法(executorの種類やジョブ間でキャッシュを渡すために異なるRunnerを使用しているかどうか)に依存します。

キャッシュの格納場所

Runnerはキャッシュを保存しておく責任があるので、どこにキャッシュが保存されているかを知ることが重要です。.gitlab-ci.ymlのジョブで定義されたすべてのキャッシュパスは、1つのcache.zipファイルにアーカイブされ、Runnerの設定したキャッシュの場所に保存されます。 デフォルトでは、Runnerがインストールされたマシンのローカル領域に保存され、executorのタイプによって異なります。

GitLab Runnerのexecutor キャッシュのデフォルトパス
Shell ローカルでは、gitlab-runner のユーザーのホームディレクトリ以下に保存されます。
Docker ローカルでは、/var/lib/docker/volumes<volume-id>//_data<user><project><cache-key>////cache.zip</cache-key></project></user></volume-id>というDockerボリュームの下に格納されています。
Docker machine (autoscale Runners) Docker executorと同じ動作をします。

How archiving and extracting works

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

中ではこんなことが起きています。

  1. パイプライン開始。
  2. ジョブAが実行されます。
  3. before_scriptが実行されます。
  4. スクリプトが実行されます。
  5. after_scriptが実行されます。
  6. cacheが実行され、vendor/ディレクトリがcache.zipにzip圧縮されます。このファイルは、その後、Runnerの設定cache:keyに基づいてディレクトリに保存され
  7. ジョブBが実行されます。
  8. キャッシュが抽出されます(見つかった場合)。
  9. before_scriptが実行されます。
  10. スクリプトが実行されます。
  11. パイプライン完了

単一のマシンで単一のRunnerを使用すると、ジョブBジョブAとは異なるランナーで実行されたり、ステージ間のキャッシュの保証等の問題は発生しません。 ビルドがステージビルドから同じRunner/マシンでテストに移行する場合にのみ機能します。そうでない場合はキャッシュが利用できない可能性があります

キャッシングのプロセスで、考慮すべき点もいくつかあります。

  • 別のキャッシュ設定を持つ他のジョブが同じzipファイルにキャッシュを保存していた場合、上書きされます。 S3ベースの共有キャッシュを使用している場合、ファイルはキャッシュキーに基づいたオブジェクトに追加してS3にアップロードされます。 そのため、異なるパスで同じキャッシュキーを持つ2つのジョブは、それらのキャッシュを上書きします。
  • cache.zipからキャッシュを抽出する場合、zipファイル内のすべてのものがジョブの作業ディレクトリ(通常はPullされたリポジトリ)に抽出され、Runnerはジョブ A のアーカイブがジョブ B のアーカイブ内のものを上書きしても問題ありません。

このように動作する理由は、あるランナーのために作成されたキャッシュが異なるアーキテクチャで動作する別のランナーで使用された場合(例:キャッシュにバイナリファイルが含まれている場合)、有効にならないことが多いからです。 また、異なるステップは、異なるマシン上で動作するRunnerによって実行される可能性があるため安全を期してこのようになっています。

キャッシュの不一致

次の表では、不一致に陥る可能性のあるいくつかの理由と、それを修正する方法についてのいくつかのアイデアを案内しています。

キャッシュの不一致の理由 修正方法
1つのプロジェクトに接続された複数のスタンドアロンRunner(オートスケールモードではない)を共有キャッシュなしで使用します。 プロジェクトに1つのRunnerのみを使用するか、分散キャッシュを有効にして複数のランナーを使用します。
分散キャッシュを有効にせずにオートスケールモードでRunnerを使用する場合 分散キャッシュを使用するようにオートスケールRunnerを設定します。
Runnerがインストールされているマシンのディスク容量が少ないか、分散キャッシュを設定している場合、キャッシュが保存されているS3バケットに十分な容量がない。 新しいキャッシュを保存できるようにスペースを空けておきましょう。 現状では自動化できる方法はありません。
異なるパスをキャッシュするジョブでは、同じキーを使用します。 キャッシュアーカイブが別の場所に保存され、間違ったキャッシュを上書きしないように、異なるキャッシュキーを使用します。

いくつかの例をみてみましょう。

使用例

プロジェクトに割り当てられたRunnerは1台だけで、キャッシュはデフォルトでRunnerのマシンに保存されているとします。2つのジョブAとBが同じキャッシュキーを持っていて、異なるパスをキャッシュしている場合、パスが一致しなくても、キャッシュBがキャッシュAを上書きします。

パイプラインが2回目に実行されたときに、ジョブAジョブBがそのキャッシュを再利用するようにしたい。

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/
  1. ジョブAが実行されます。
  2. public/はcache.zipとしてキャッシュされます。
  3. ジョブBが実行されます。
  4. 前のキャッシュがあれば、それを解凍します。
  5. vendor/はcache.zipとしてキャッシュされ、前のものを上書きします。
  6. 次回のジョブAの実行時には、異なるジョブBのキャッシュを使用するため、有効ではありません。

それを修正するには、ジョブごとに異なるキーを使用します。

別のケースでは、プロジェクトに複数のRunnerが割り当てられているが、分散キャッシュが有効になっていないとします。 2回目のパイプライン実行時に、ジョブAジョブ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/

その場合、キーが異なっていても(上書きの恐れがない)、ジョブが後続のパイプラインでジョブが異なるRunner上で実行された場合、各ステージの前にキャッシュされたファイルが「クリアされた状態」になるでしょう。

キャッシュのクリア

GitLab Runnersはキャッシュを使用して、既存のデータを再利用してジョブの実行を高速化します。 しかし、これは時に一貫性のない動作を引き起こす可能性があります。

キャッシュの新しいコピーから始めるには、2つの方法があります。

cache:keyを変更してキャッシュをクリアする

.gitlab-ci.ymlに新しいcache: keyを設定するだけです。 次のパイプラインの実行時には、キャッシュは別の場所に保存されます。

キャッシュを手動でクリアする

GitLab 10.4 で導入されました

.gitlab-ci.ymlの編集を避けたい場合は、GitLabのUIから簡単にキャッシュをクリアできます。

  1. プロジェクトのCI/CD > パイプラインページに移動します。
  2. Runnerのキャッシュを削除ボタンをクリックして、キャッシュを削除します。

Clear Runners cache

  1. 次のプッシュで、CI/CDジョブは新しいキャッシュを使用します。

裏では、これはデータベース内のカウンタを増加させることで動作しています。そのカウンタの値は -1-2など、整数にしてください。プッシュ後、新しいキーが生成され、古いキャッシュはもう有効ではなくなります。