こんにちは、GMO インターネットグループ株式会社の岩本です。モノレポ構成のプロジェクトにおける GitLab CI の設定方法について調べましたので紹介します。
はじめに
今回モノレポ構成のプロジェクトを作る機会があり GitLab CI の設定をどのようにすればよいか調べました。というのも、GitLab CI を動かすためにはプロジェクトルートの .gitlab-ci.yml に CI の設定を書く必要がありますが、複数プロジェクトの CI の設定を 1 つのファイルで管理するのは大変に感じたためです。
trigger:include:local, trigger:include:artifact を使った方法と、include:local を使った方法の計 3 種類の方法についてまとめました。この記事では GitLab Community Edition 15.3.4, GitLab Runner 15.3.2 を使用しています。
trigger:include:local を使った方法
GitLab CI には trigger というキーワードがあり、これを使うことで別リポジトリのパイプラインを実行したり1,2、同じリポジトリ内の別の .gitlab-ci.yml を実行したりできます3 。今回はモノレポ構成のため、同一リポジトリ内にある .gitlab-ci.yml を対象にした trigger:include:local を使います。
ディレクトリ構成は以下のようにしました。今回は 2 つのプロジェクト project_a, project_b を 1 つのリポジトリで管理する構成とします。この構成は後述の trigger:include:artifact でも同様です。
.
|-- .gilab-ci.yml
|-- project_a
| └-- .gilab-ci.yml
└-- project_b
└-- .gilab-ci.yml
プロジェクトルートの .gitlab-ci.yml, 各プロジェクトの .gitlab-ci.yml は以下のように書きます。
プロジェクトルートの .gitlab-ci.yml
project_a:
trigger:
include: project_a/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- "project_a/**/*"
project_b:
trigger:
include: project_b/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- "project_b/**/*"
各プロジェクトの .gitlab-ci.yml
workflow:
rules:
- if: $CI_MERGE_REQUEST_IID
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
stages:
- test
- deploy
job1:
stage: test
script: echo test
job2:
stage: deploy
script: echo deploy
ポイント
trigger:strategy の設定
trigger:strategy を付けないデフォルトの設定では、子パイプラインが作成された直後に親パイプラインが成功となります4。今回は子パイプラインで起きたエラーを表示してほしいため trigger:strategy を設定しています。
子パイプラインに rules:if: $CI_MERGE_REQUEST_IID を付けないと動作しない場合がある
マージリクエストのタイミングで実行される子パイプラインを、マージリクエストパイプラインであると明示的に設定する必要があります。これを付けない場合子パイプラインがブランチパイプラインとして認識され、パイプラインが期待したとおりに動かない場合があります5。
ブランチパイプラインは git push された内容を、マージリクエストパイプラインはマージリクエストの修正対象全体を見るという違いがあります6。
子パイプラインがブランチパイプラインとして認識され正しく動かなかった例
trigger で実行するパイプラインに有効なジョブがなければ失敗する
trigger を発火したはいいものの、子パイプラインの中に実行するジョブが一つも無い場合、親パイプラインは失敗します。実行元でそもそも trigger を発火させないなど何かしら工夫が必要です。
trigger 先のパイプラインに有効なジョブがなかった場合のエラー
trigger:include:artifact を使った方法
今回は project_a, project_b だけでしたがもっとプロジェクト数が多い場合、プロジェクトルートの .gitlab-ci.yml の管理が大変になりそうです。そこで、各プロジェクトごとに実行するパイプラインを動的に生成し管理の手間を減らせないか検討しました。
具体的には trigger:include:artifact を使った方法7です。
上記で紹介した trigger:include:local を持つ .gitlab-ci.yml を生成します。生成されたファイルをアーティファクトにし、後続のジョブで trigger:include:artifact を使って読み込みます。
.gitlab-ci.yml
image: ubuntu
stages:
- build
- triggers
workflow:
rules:
- if: $CI_MERGE_REQUEST_IID
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
variables:
workflow: >
workflow:
rules:
- if: __CI_MERGE_REQUEST_IID
- if: __CI_COMMIT_BRANCH == __CI_DEFAULT_BRANCH
template: >
PROJECT_NAME:
trigger:
include: PROJECT_NAME/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- "PROJECT_NAME/**/*"
generate-artifact:
stage: build
script:
- echo -e "${workflow}" | sed -i -e 's/__/$/g' > generated.yml
- >
find . -name .gitlab-ci.yml -type f -not -path "./.gitlab-ci.yml" | cut -d'/' -f2 |
while read PROJECT_NAME; do
export PROJECT_NAME
echo -e "${template}" | sed -e "s/PROJECT_NAME/$PROJECT_NAME/g" >> generated.yml
done
artifacts:
paths:
- generated.yml
child-pipeline:
stage: triggers
trigger:
include:
- artifact: generated.yml
job: generate-artifact
strategy: depend
ポイント
パイプラインの生成
各プロジェクトごとに用意している .gitlab-ci.yml を find コマンドを使って見つけ、trigger:include:local の対象にしています。
これでプロジェクトルートの .gitlab-ci.yml の管理の手間から開放されました。
YAML ファイル生成ジョブが毎回実行される
この方法では毎回ファイルを生成するため、その分時間がかかります。rules:changes などを使って README.md だけの変更の場合は実行しない、などの工夫もしたほうがよいかもしれません。
子パイプラインが 1 つにまとめられる
個々のマージリクエスト画面で CI ジョブの成功/失敗がチェックマーク等で表示されますが、この方法では複数の子パイプラインが 1 つにまとめられてしまいます。ぱっと見何を実行したのか分かりにくく、パイプラインが失敗した場合やどのプロジェクトの CI が実行されたか知りたい場合、パイプライン詳細画面にて確認する必要があります。
複数のパイプラインが 1 つのジョブにまとめられている
include:local を使った方法
プロジェクトごとの CI の設定は .gitlab/ ディレクトリに配置します。また、ファイル名は <プロジェクト名>.gitlab-ci.yml とします。
.
|-- .gilab-ci.yml
└-- .gitlab/
|-- project_a.gilab-ci.yml
└-- project_b.gilab-ci.yml
プロジェクトルートの .gitlab-ci.yml
stages:
- test
- deploy
default:
image: ubuntu
include:
- local: .gitlab/*.gitlab-ci.yml
各プロジェクトの .gitlab/*.gitlab-ci.yml
.project_a:rules:
rules:
- &mr
if: $CI_MERGE_REQUEST_IID
changes:
- project_a/**/*
- &main
if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
changes:
- project_a/**/*
project_a:test:
rules:
- *mr
- *main
stage: test
script:
- echo test
project_a:deploy:
rules:
- *main
stage: deploy
script:
- echo build
needs:
- project_a:build
ポイント
YAML anchors を使ったルールの定義
GitLab には extends を使って共通する設定を再利用できますが、配列の中身はマージされないため8ここでは YAML anchors を使った方法を採用しました9。
ジョブ名や変数名が重複しないように
各プロジェクトごとに設定するジョブ名が重複すると、CI がうまく動かない場合があります。<プロジェクト名>:<ジョブ名> のように、名前が被らないような工夫が必要です。
また、variables を使って変数を定義できますが、こちらも重複しないように気を付ける必要があります。
needs を使った依存関係の整理
GitLab CI のパイプラインは基本的にステージ単位で進みます。上記であればステージは test, deploy があり、test ステージに含まれる全てのジョブを完了させてから、次の deploy ステージに進みます。複数のプロジェクトを同時に更新していた場合、直接関係のないジョブの完了を待たなければならないかもしれません。
例: project_a:test は完了したが、project_b:test に時間がかかっているため、project_a:deploy が実行できない
この問題は needs を使うことでジョブ間の依存関係を記述でき、無駄な待ち時間を削減できます。また、ジョブ間の依存関係がグラフで表示されかっこよいです!10
その他
今回 .gitlab-ci.yml を書くにあたり VSCode を使いました。その際 YAML プラグイン https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml を使うと、GitLab CI の設定ファイル内で使えるキーワードを補完してくれたり、マウスホバーした際にそのキーワードの説明文を表示できたりと便利でしたのでおすすめです。
trigger:include の説明文を表示している例
まとめ
モノレポ構成のプロジェクトで GitLab CI を動かすという 1 つのお題に対し、今回挙げただけでも 3 種類の方法が考えられました。最終的に 3 つ目の include:local を使った方法を採用することとなりました。CI の結果の見やすさや設定ファイルが一か所にまとまっていること、needs を使った効率的なパイプライン処理ができることなどが決めてです。
また、GitLab CI の設定方法を調査するにあたり、公式ドキュメントの充実さに改めて気づくことができました。感謝 :bow:今回参考にしたドキュメントやリポジトリの URL を以下に記載します。中でも GitLab の .gitlab-ci.yml11はモノレポ構成でなくても参考になるかと思いますので、一度目を通すと新しい発見があるかもしれません。
参考資料
.gitlab-ci.yml keyword referencehttps://docs.gitlab.com/ee/ci/yaml/CI/CD pipelineshttps://docs.gitlab.com/ee/ci/pipelines/GitLab リポジトリの .gitlab-ci.ymlhttps://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab-ci.yml
脚注
Multi-project pipelineshttps://docs.gitlab.com/ee/ci/pipelines/downstream_pipelines.html#multi-project-pipelines
include:project の例https://docs.gitlab.com/ee/ci/pipelines/downstream_pipelines.html#use-a-child-pipeline-configuration-file-in-a-different-project
Parent-child pipelineshttps://docs.gitlab.com/ee/ci/pipelines/downstream_pipelines.html#parent-child-pipelines
trigger job to be marked as success as soon as the downstream pipeline is createdhttps://docs.gitlab.com/ee/ci/yaml/#triggerstrategy
ブランチパイプラインをマージリクエストパイプラインに切り替えるために workflow:rules を紹介しているhttps://docs.gitlab.com/ee/ci/yaml/workflow.html#switch-between-branch-pipelines-and-merge-request-pipelines
パイプライン実行時の対象とするファイルがマージリクエストパイプラインとブランチパイプラインとで異なるhttps://gitlab.com/gitlab-org/gitlab/-/blob/v15.3.4-ee/app/models/ci/pipeline.rb#L1149-1152
子パイプラインの動的生成https://docs.gitlab.com/ee/ci/pipelines/downstream_pipelines.html#dynamic-child-pipelines
Doesn’t merge the values of the keys.https://docs.gitlab.com/ee/ci/yaml/#extends
https://docs.gitlab.com/ee/ci/yaml/yaml_optimization.html#anchors
needs を使ったジョブ間の依存関係の可視化https://docs.gitlab.com/ee/ci/directed_acyclic_graph/index.html#needs-visualization
GitLab リポジトリの .gitlab-ci.ymlhttps://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab-ci.yml