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を使用することは、1つのステージまたはパイプラインでファイルをキャッシュし、保証された方法で後続のステージまたはパイプラインにこのキャッシュを渡す、最もシンプルで効率的な方法です。

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ライブラリなど)のような前後関係があるジョブ間で利用されるコンテンツを保存することで、それらをインターネットから再取得する必要がないようにしています。

Note:より多くの例については、GitLab CI/CDテンプレートをご覧ください。

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.ymlcache を適切に定義したと仮定すると、キャッシュの可用性は最終的に 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

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

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

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

それを解決するには、ジョブごとにkeys

別のケースとして、プロジェクトに複数のRunnerが割り当てられているが、ディストリビューションキャッシュが有効になっていないとします。 2回目のパイプラインの実行時に、job Ajob 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から簡単にキャッシュをクリアできます:

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

Clear Runners cache

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

舞台裏では、データベースのカウンターを増加させ、そのカウンターの値を使用して、-1-2などの整数を付加してキャッシュのキーを作成します。プッシュの後、新しいキーが生成され、古いキャッシュは無効になります。