CI/CD 캐싱 예시

CI/CD 캐싱 예시 (CI/CD caching examples)

캐싱을 쓰면 job이 실행될 때마다 의존성과 빌드 산출물을 다시 내려받지 않아도 돼요. 이전에 받아 둔 콘텐츠를 재사용해서 CI/CD 파이프라인을 빠르게 만들어 주는 것이죠. 더 많은 예시는 GitLab CI/CD 템플릿을 참고하세요.

출처: 문서

본문

캐시 전략 (Cache strategies)

이 예시들은 job과 브랜치 사이에서 캐시를 공유하는 여러 접근 방식을 보여줘요.

같은 브랜치의 job들 사이에서 캐시 공유 (Share caches between jobs in the same branch)

각 브랜치의 job들이 같은 캐시를 쓰게 하려면 key: $CI_COMMIT_REF_SLUG으로 캐시를 정의하세요.

cache:
  key: $CI_COMMIT_REF_SLUG

이 구성은 실수로 캐시를 덮어쓰는 걸 막아줘요. 다만 MR의 첫 파이프라인은 느립니다. 브랜치에 커밋을 다시 푸시하면 그다음부터 캐시가 재사용되어 job이 빨라져요. job별·브랜치별 캐싱을 활성화하려면 이렇게 합니다.

cache:
  key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"

스테이지별·브랜치별 캐싱을 활성화하려면 이렇게 합니다.

cache:
  key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"

다른 브랜치의 job들 사이에서 캐시 공유 (Share caches across jobs in different branches)

모든 브랜치·모든 job에 캐시를 공유하려면 모든 곳에서 같은 키를 쓰면 돼요.

cache:
  key: one-key-to-rule-them-all

브랜치 사이에는 캐시를 공유하되 job마다 고유한 캐시를 쓰려면 이렇게 합니다.

cache:
  key: $CI_JOB_NAME

변수로 job의 캐시 정책 제어 (Use a variable to control a job's cache policy)

pull 정책만 다른 중복 job을 줄이려면 CI/CD 변수를 사용할 수 있어요. 예를 들면 이렇습니다.

conditional-policy:
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      variables:
        POLICY: pull-push
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      variables:
        POLICY: pull
  stage: build
  cache:
    key: gems
    policy: $POLICY
    paths:
      - vendor/bundle
  script:
    - echo "This job pulls and pushes the cache depending on the branch"
    - echo "Downloading dependencies..."

이 예시에서 job의 캐시 정책은:

  • 기본 브랜치 변경에는 pull-push.
  • 다른 브랜치 변경에는 pull.

의존성 캐시 (Cache dependencies)

이 예시들은 프로그래밍 언어별로 일반적인 의존성 캐싱 방법을 보여줘요.

Node.js

프로젝트가 npm으로 Node.js 의존성을 설치한다면, 다음 예시처럼 모든 job이 상속받는 기본 cache를 정의하면 돼요. 기본적으로 npm은 홈 폴더(~/.npm)에 캐시 데이터를 저장해요. 하지만 프로젝트 디렉터리 밖은 캐시할 수 없어요. 대신 npm이 ./.npm을 쓰도록 하고 브랜치별로 캐시하세요.

default:
  image: node:latest
  cache:  # Cache modules in between jobs
    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
잠금 파일에서 캐시 키 계산 (Compute the cache key from the lock file)

cache:key:files를 쓰면 package-lock.json이나 yarn.lock 같은 잠금 파일로 캐시 키를 계산해서 여러 job에서 재사용할 수 있어요.

default:
  cache:  # Cache modules using lock file
    key:
      files:
        - package-lock.json
    paths:
      - .npm/
오프라인 미러와 함께 쓰는 Yarn (Yarn with offline mirror)

Yarn을 쓴다면 yarn-offline-mirror로 압축된 node_modules tarball을 캐시할 수 있어요. 압축해야 할 파일이 더 적어서 캐시 생성이 더 빨라집니다.

job:
  script:
    - echo 'yarn-offline-mirror ".yarn-cache/"' >> .yarnrc
    - echo 'yarn-offline-mirror-pruning true' >> .yarnrc
    - yarn install --frozen-lockfile --no-progress
  cache:
    key:
      files:
        - yarn.lock
    paths:
      - .yarn-cache/

PHP

프로젝트가 Composer로 PHP 의존성을 설치한다면, 다음 예시처럼 모든 job이 상속받는 기본 cache를 정의하면 돼요. PHP 라이브러리는 vendor/에 설치되고 브랜치별로 캐시됩니다.

default:
  image: php:latest
  cache:  # Cache libraries in between jobs
    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

프로젝트가 pip으로 Python 의존성을 설치한다면, 다음 예시처럼 모든 job이 상속받는 기본 cache를 정의하면 돼요. pip의 캐시는 .cache/pip/ 아래에 정의되고 브랜치별로 캐시됩니다.

default:
  image: python:latest
  cache:                      # Pip's cache doesn't store the python packages
    paths:                    # https://pip.pypa.io/en/stable/topics/caching/
      - .cache/pip
  before_script:
    - python -V               # Print out python version for debugging
    - pip install virtualenv
    - virtualenv venv
    - source venv/bin/activate

variables:  # Change pip's cache directory to be inside the project directory because GitLab can only cache local items.
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

test:
  script:
    - python setup.py test
    - pip install ruff
    - ruff --format=gitlab .

Ruby

프로젝트가 Bundler로 gem 의존성을 설치한다면, 다음 예시처럼 모든 job이 상속받는 기본 cache를 정의하면 돼요. gem은 vendor/ruby/에 설치되고 브랜치별로 캐시됩니다.

default:
  image: ruby:latest
  cache:                                            # Cache gems in between builds
    key: $CI_COMMIT_REF_SLUG
    paths:
      - vendor/ruby
  before_script:
    - ruby -v                                       # Print out ruby version for debugging
    - bundle config set --local path 'vendor/ruby'  # The location to install the specified gems to
    - bundle install -j $(nproc)                    # Install dependencies into ./vendor/ruby

rspec:
  script:
    - rspec spec

다른 gem이 필요한 job들이 있다면 전역 cache 정의에서 prefix 키워드를 사용하세요. 이 구성은 job마다 다른 캐시를 만들어 줍니다. 예를 들어 테스트 job은 프로덕션에 배포하는 job과 다른 gem이 필요할 수 있어요.

default:
  cache:
    key:
      files:
        - Gemfile.lock
      prefix: $CI_JOB_NAME
    paths:
      - vendor/ruby

test_job:
  stage: test
  before_script:
    - bundle config set --local path 'vendor/ruby'
    - bundle install --without production
  script:
    - bundle exec rspec

deploy_job:
  stage: production
  before_script:
    - bundle config set --local path 'vendor/ruby'   # The location to install the specified gems to
    - bundle install --without test
  script:
    - bundle exec deploy

Go

프로젝트가 Go Modules로 Go 의존성을 설치한다면, 다음 예시처럼 어떤 job이든 확장할 수 있는 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:latest
  extends: .go-cache
  script:
    - go test ./... -v -short

빌드 산출물과 다운로드 캐시 (Cache build artifacts and downloads)

이 예시들은 빌드를 빠르게 하기 위해 컴파일된 객체와 내려받은 파일을 캐시하는 방법을 보여줘요.

Ccache로 C/C++ 컴파일 캐시 (Cache C/C++ compilation using Ccache)

C/C++ 프로젝트를 컴파일한다면 Ccache로 빌드 시간을 단축할 수 있어요. Ccache는 이전 컴파일을 캐시하고 같은 컴파일이 다시 수행되는 걸 감지해서 재컴파일을 빠르게 합니다. Linux 커널 같은 큰 프로젝트를 빌드할 때 상당히 빨라지는 걸 기대할 수 있어요. 만들어진 캐시를 job 사이에서 재사용하려면 cache를 사용하면 됩니다. 예를 들면 이렇습니다.

job:
  cache:
    paths:
      - ccache
  before_script:
    - export PATH="/usr/lib/ccache:$PATH"  # Override compiler path with ccache (this example is for Debian)
    - export CCACHE_DIR="${CI_PROJECT_DIR}/ccache"
    - export CCACHE_BASEDIR="${CI_PROJECT_DIR}"
    - export CCACHE_COMPILERCHECK=content  # Compiler mtime might change in the container, use checksums instead
  script:
    - ccache --zero-stats || true
    - time make                            # Actually build your code while measuring time and cache efficiency.
    - ccache --show-stats || true

단일 저장소에 여러 프로젝트가 있다면 각각에 별도 CCACHE_BASEDIR를 둘 필요는 없어요.

cURL로 다운로드 캐시 (Cache downloads with cURL)

프로젝트가 cURL로 의존성이나 파일을 내려받는다면 다운로드한 콘텐츠를 캐시할 수 있어요. 더 새로운 다운로드가 가능해지면 파일이 자동으로 갱신됩니다.

job:
  script:
    - curl --remote-time --time-cond .curl-cache/caching.md --output .curl-cache/caching.md "https://docs.gitlab.com/ci/caching/"
  cache:
    paths:
      - .curl-cache/

이 예시에서 cURL은 웹서버에서 파일을 내려받아 .curl-cache/의 로컬 파일로 저장해요. --remote-time 플래그는 서버가 보고한 마지막 수정 시간을 저장하고, cURL은 --time-cond로 캐시된 파일의 타임스탬프와 비교합니다. 원격 파일의 타임스탬프가 더 최신이면 로컬 캐시가 자동으로 갱신돼요.

더 알아보기

캐시 키를 어떻게 잡느냐가 캐시 전략의 핵심이에요. $CI_COMMIT_REF_SLUG로 브랜치별, $CI_JOB_NAME으로 job별로 공유 범위를 나눌 수 있고, 잠금 파일(cache:key:files)로 의존성 변경 시점에만 캐시가 갱신되게 할 수도 있어요. 언어별로 npm·Composer·pip·Bundler·Go 모듈의 기본 캐시 경로를 맞춰 보고, Ccache나 cURL처럼 산출물 캐싱도 함께 적용해 보세요.