GitLab - Rustで使う用の.gitlab-ci.yml (Runnerを専有可能な環境向け)

RustなプロジェクトのCIを回す際、ビルドやカバレッジ取得のたびに依存関係がリコンパイルされたり、キャッシュが大きくなったりと色々つらかったので何とかしようと試行錯誤したメモ。

やった事は主に以下二点で、今のところは満足。
一部GitLab Runnerの専有が必要という環境制約あり。

  • CIジョブ実行時のgit cleanの対象から./targetを除外する
  • CIジョブで使用するtarpaulin入りのDockerイメージをあらかじめ作っておく

目次

環境

  • GitLab: 14.8.2-ee *1

サーバ構成

制約

  • GitLab Runnerを専有する必要がある
    今回のやり方では、Docker executorのvolume上に./targetを保持したままにすることで、volumeそのものをキャッシュ代わりにする。
    そのため、共有RunnerのようにCIジョブ実行都度volumeが片付けられてしまう環境では効果がない。

    参考: GitLab CI ジョブの高速化 (キャッシュを使わない)

定義ファイル類

配置

イメージをつかむのに必要そうなものだけ書いた。

.
|-- .gitlab-ci.yml
|-- Cargo.toml
|-- Docker
|   |-- CI
|   |   `-- Dockerfile.rust.ci    … CIジョブで使用するimageのビルド用
|   `-- Dockerfile.rust           … 開発環境用
|-- docker-compose.yml
|-- src
|   `-- main.rs
`-- target

.gitlab-ci.yml

パイプラインイメージ

パイプライン構成

  1. git push時にビルドやテストなどを実行する
  2. MergeRequest作成時にカバレッジを計測する
  3. 任意のタイミングでビルドとカバレッジ計測に使用するDockerイメージを再作成する
  • 各ジョブとも任意のタイミングでGitLabのWebUIから個別に実行できる
  • 個別に実行する場合、ビルドとカバレッジ計測は実行時のVariablesとしてCLEANを付与することでクリーンビルドできる

.gitlab-ci.yml

variables:
  CARGO_INCREMENTAL: 0
  FF_USE_FASTZIP: "true"
  # ARTIFACT_COMPRESSION_LEVEL: fast

  # https://zenn.dev/masakura/articles/1ba1d9ec95cfad
  # GitLab CI ジョブの高速化 (キャッシュを使わない)
  GIT_CLEAN_FLAGS: -ffdx -e target/

  # CI時に使用するrustのイメージ名
  CI_RUST_IMAGE: "$CI_REGISTRY_IMAGE/ci-rust"

stages:
  - build
  - coverage
  - rebuild_ci_image

image: rust:slim


# 通常のフローでは`./target`を掃除できなくなっているので、
# フォールバック手段を用意しておく。
# GitLabのPipeline実行画面で変数`CLEAN`(値は任意)を指定して実行すると、
# ビルド実行前に`./target`を削除できる。
# あと、本体へのマージ時にも一応。
.clean: &clean >
  [ "${CLEAN:-}" != "" ] || [ "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-}" = $CI_DEFAULT_BRANCH ]
  && echo ----- CLEAN ------------------------------
  && rm -rf ./target
  && ls -la

build:
  stage: build
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: never
    - if: $REBUILD_RUST_IMAGE
      when: never
    - when: always
  image: $CI_RUST_IMAGE
  script:
    # 始めはビルドやテストなどを個別のジョブに細分化していたが、ジョブごとに
    # キャッシュの圧縮/展開を繰り返してストレージに優しくない感じだったので
    # 1ジョブにまとめた。
    - ls -la
    - cargo --version
    - *clean
    - >
      : ----- FORMAT ----------------------------
    - cargo fmt --version
    - time cargo fmt -- --check
    - >
      : ----- BUILD -----------------------------
    - rustc --version
    - time cargo build
    - >
      : ----- LINT ------------------------------
    - cargo clippy --version
    - time cargo clippy
    - >
      : ----- BUILD - TEST ----------------------
    - time cargo test --no-run
    - >
      : ----- TEST ------------------------------
    - time cargo test
    - ls -la

coverage:
  stage: coverage
  tags: 
    # coverage用のrunnerに振り分ける。
    # buildと一緒のrunnerで処理すると毎回リコンパイルされてしまうので。
    - coverage
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  image: $CI_RUST_IMAGE
  script:
    - >
      : ----- TEMP MERGE ----------------------------
    # たまに`*** Please tell me who you are.`されるのでダミーを入れておく。
    - git config --local user.name "gitlab-runner"
    - git config --local user.email "gitlab-runner@example.com"
    - git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
    # https://ngyuki.hatenablog.com/entry/2020/02/15/205425
    # Gitlab CI でマージリクエストのマージ結果でパイプラインを実行する
    - git checkout "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
    - git merge --squash -v -
    - git diff --stat --staged
    - ls -la
    - *clean
    - >
      : ----- COVERAGE ------------------------------
    - cargo tarpaulin --version
    - cargo tarpaulin --skip-clean --out Html Xml --output-dir ./coverage
    - ls -la
  coverage: '/^\d+.\d+% coverage/'
  artifacts:
    paths:
      # カバレッジレポートをGitLab Pagesで参照できるようにする。
      - coverage/tarpaulin-report.html
    reports:
      cobertura: 
        # Merge RequestのChangesタブで変更箇所のカバレッジ状況を参照できるようにする。
        - coverage/cobertura.xml
    expose_as: 'coverage report'


rebuild_ci_image:
  stage: rebuild_ci_image
  rules:
    - if: $REBUILD_RUST_IMAGE
  tags: 
    - rebuild_image
  script:
    - ls -la
    - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY
    - docker build --pull -t $CI_RUST_IMAGE -f Docker/CI/Dockerfile.rust.ci Docker/CI/
    - docker push $CI_RUST_IMAGE

Dockerfile.rust.ci

ビルドやカバレッジ取得で使用するDockerイメージのビルドファイル。

カバレッジ取得はtarpaulinの公式イメージを使うという手もあるのだが、パイプライン内でgit操作したかったり、プロジェクトで使用しているクレートが外部のパッケージに依存していたりするため、個別にDockerイメージを作ることにした。

FROM rust:slim

# pkg-config, libssl-dev:
#   - cargo-tarpaulinのコンパイルに必要
#   - プロジェクトで利用しているrequestクレートのコンパイルに必要
# git:
#   - Merge Requestのパイプライン内で一時的にマージ状態を作るためにgit操作が必要
RUN apt-get update -qq && \
    apt-get install -qqy pkg-config libssl-dev git && \
    rustup component add rustfmt clippy && \
    cargo install cargo-tarpaulin

/etc/gitlab-runner/config.toml

GitLab Runnerをインストールした際に自動生成される定義ファイル。
Docker executorがgit cloneする際にホスト名を名前解決できるように追記しておく。
また、tarpaulinを使用するexecutorには特権が必要になる。

[[runners]]
  name = "Docker executor for build"
  executor = "docker"
    :
  [runners.docker]
    volumes = ["/cache"]
        :
+   extra_hosts = ["MY_HOST_NAME:192.0.2.1"]
+   privileged = true                        # tarpaulinを使用するexecutorのみ

参考リンク

*1:EEでもライセンスなしの場合はCEと同じ状態になる。
Community EditionとEnterprise Editionの違い | GitLab.JP