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

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

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

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

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

Cache vs artifacts

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

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

  • cache: For storing project dependencies

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

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

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

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

Caches:

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

Artifacts:

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

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

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

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

開発者の観点からは、ジョブでキャッシュを宣言する際に、キャッシュを最大限に利用できるようにするために、以下のいずれか、またはそれらを組み合わせて使用します。

TIP: Tip: Tip: パイプラインに同じ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 Locally, stored under Docker volumes: /var/lib/docker/volumes/<volume-id>/_data/<user>/<project>/<cache-key>/cache.zip.
Docker machine (autoscale Runners) Behaves the same as the Docker executor.

How archiving and extracting works

In the most simple scenario, consider that you use only one machine where the Runner is installed, and all jobs of your project run on the same host.

Let’s see the following example of two jobs that belong to two consecutive stages:

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

Here’s what happens behind the scenes:

  1. Pipeline starts.
  2. job A runs.
  3. before_script is executed.
  4. script is executed.
  5. after_script is executed.
  6. cache runs and the vendor/ directory is zipped into cache.zip. This file is then saved in the directory based on the Runner’s setting and the cache: key.
  7. job B runs.
  8. The cache is extracted (if found).
  9. before_script is executed.
  10. script is executed.
  11. Pipeline finishes.

By using a single Runner on a single machine, you’ll not have the issue where job B might execute on a Runner different from job A, thus guaranteeing the cache between stages. That will only work if the build goes from stage build to test in the same Runner/machine, otherwise, you might not have the cache available.

During the caching process, there’s also a couple of things to consider:

  • If some other job, with another cache configuration had saved its cache in the same zip file, it is overwritten. If the S3 based shared cache is used, the file is additionally uploaded to S3 to an object based on the cache key. So, two jobs with different paths, but the same cache key, will overwrite their cache.
  • When extracting the cache from cache.zip, everything in the zip file is extracted in the job’s working directory (usually the repository which is pulled down), and the Runner doesn’t mind if the archive of job A overwrites things in the archive of job B.

The reason why it works this way is because the cache created for one Runner often will not be valid when used by a different one which can run on a different architecture (e.g., when the cache includes binary files). And since the different steps might be executed by Runners running on different machines, it is a safe default.

Cache mismatch

In the following table, you can see some reasons where you might hit a cache mismatch and a few ideas how to fix it.

Reason of a cache mismatch How to fix it
You use multiple standalone Runners (not in autoscale mode) attached to one project without a shared cache Use only one Runner for your project or use multiple Runners with distributed cache enabled
You use Runners in autoscale mode without a distributed cache enabled Configure the autoscale Runner to use a distributed cache
The machine the Runner is installed on is low on disk space or, if you’ve set up distributed cache, the S3 bucket where the cache is stored doesn’t have enough space Make sure you clear some space to allow new caches to be stored. Currently, there’s no automatic way to do this.
You use the same key for jobs where they cache different paths. Use different cache keys to that the cache archive is stored to a different location and doesn’t overwrite wrong caches.

Let’s explore some examples.

使用例

Let’s assume you have only one Runner assigned to your project, so the cache will be stored in the Runner’s machine by default. If two jobs, A and B, have the same cache key, but they cache different paths, cache B would overwrite cache A, even if their paths don’t match:

We want job A and job B to re-use their cache when the pipeline is run for a second time.

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 runs.
  2. public/ is cached as cache.zip.
  3. job B runs.
  4. The previous cache, if any, is unzipped.
  5. vendor/ is cached as cache.zip and overwrites the previous one.
  6. The next time job A runs it will use the cache of job B which is different and thus will be ineffective.

To fix that, use different keys for each job.

In another case, let’s assume you have more than one Runners assigned to your project, but the distributed cache is not enabled. The second time the pipeline is run, we want job A and job B to re-use their cache (which in this case will be different):

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/

In that case, even if the key is different (no fear of overwriting), you might experience that the cached files “get cleaned” before each stage if the jobs run on different Runners in the subsequent pipelines.

Clearing the cache

GitLab Runners use cache to speed up the execution of your jobs by reusing existing data. This however, can sometimes lead to an inconsistent behavior.

To start with a fresh copy of the cache, there are two ways to do that.

Clearing the cache by changing cache:key

All you have to do is set a new cache: key in your .gitlab-ci.yml. In the next run of the pipeline, the cache will be stored in a different location.

Clearing the cache manually

Introduced in GitLab 10.4.

If you want to avoid editing .gitlab-ci.yml, you can easily clear the cache via GitLab’s UI:

  1. Navigate to your project’s CI/CD > Pipelines page.
  2. Click on the Clear Runner caches button to clean up the cache.

    Clear Runners cache

  3. On the next push, your CI/CD job will use a new cache.

Behind the scenes, this works by increasing a counter in the database, and the value of that counter is used to create the key for the cache by appending an integer to it: -1, -2, etc. After a push, a new key is generated and the old cache is not valid anymore.