三つのサービスのうち一つしか変えていないのに全部ビルドされた
目標
parallel:matrixで組み合わせの数だけジョブを増やし、除外する組み合わせと特定の組み合わせの待機を扱ったうえで、モノレポをサービスごとの子パイプラインと、リポジトリの構造から生成する動的パイプラインに分けて、gitlab-ci-localで実行します。
なぜ重要なのか
設定が大きくなる仕方は、たいていコピー&ペーストです。Pythonのバージョンが1つ増えるたびにジョブをコピーし、サービスが1つ増えるたびに親ファイルにブロックを貼り付けていくと、1つのファイルにすべてのチームのルールが積み上がり、すべての変更がすべてのサービスを再ビルドします。matrixは組み合わせを1つのブロックにまとめ、子パイプラインはサービスの設定をそのサービスのディレクトリに戻し、changesと動的生成は変わったものだけを動かします。その代わり、ジョブ名・アーティファクト・変数がどう流れるかを知っていてはじめて、needsとルールを正しく掛けられます。
ステップ
/root/glci-monoをgitリポジトリにし(.gitignoreに.gitlab-ci-local/を入れます)、.gitlab-ci.ymlにstages[build, package, trigger, generate, dynamic]とジョブbuild(stageはbuild)を置いてください。parallel: matrixで、PYは"3.11"・"3.12"、OSはlinux・alpineの組み合わせを作り、スクリプトはdist/py$PY-$OS.txtにpy=<PY> os=<OS>を書き、artifactsでdist/をアップロードします。コミットして実行してください。ジョブ名がbuild: [3.11,linux]の形で4つできるはずです。- buildにrulesを追加し、
OSがalpineかつPYが"3.11"の組み合わせだけwhen: never、残りはwhen: on_successにしてください。コミットして実行すると、ジョブが3つだけ動くはずです。 - ジョブ
package-linux(stageはpackage)を追加し、needsでbuildのPY: "3.12"、OS: linuxの組み合わせ1つだけを指して(needs:parallel:matrix)、スクリプトはls distにしてください。コミットして実行すると、package-linuxのログにpy3.12-linux.txtだけが見えるはずです。 /root/glci-mono/services/api/ci.ymlにジョブapi-unit(echo "api unit svc=$SVC")を置き、親にジョブtrigger-api(stageはtrigger)を追加して、変数SVC: apiとtrigger: include: services/api/ci.ymlを置いてください。コミットして実行すると、子パイプラインのapi-unitのログがapi unit svc=apiになるはずです。services/web/ci.ymlにジョブweb-unit(echo "web unit svc=$SVC")を置き、親にtrigger-web(SVCはweb)を追加してください。2つのtriggerジョブには、それぞれrules: changes:でservices/api/**/*・services/web/**/*を置きます。/root/glci-mono-origin.gitにbareリポジトリを作ってoriginとして登録し、コミットしたmainをpushしたあとでgit remote set-head origin mainを実行してください。採点ツールは、コピーでwebだけを直したブランチを作り、trigger-webだけが一覧にあるかを確認します。/root/glci-mono/scripts/generate.shが、services/の下のディレクトリごとにジョブlint-<이름>(echo "lint <이름>")を含むYAMLを標準出力に出すようにしてください(プレースホルダーはディレクトリ名です)。親のジョブgenerate(stageはgenerate)がその出力をgenerated.ymlとして保存してartifactsでアップロードし、ジョブrun-generated(stageはdynamic、needs: [generate])がtrigger: include: - artifact: generated.yml, job: generateでそれを子パイプラインとして動かすようにします。コミット・pushして実行すると、lint-apiとlint-webが動くはずです。/root/glci-mono/services/billing/に、ci.ymlではなくREADME.md(内容は何でもかまいません)だけを作って、コミット・pushしてください。親の.gitlab-ci.ymlは直しません。実行すると、動的な子パイプラインにlint-billingが新しくできて動くはずです。採点ツールは、コピーにサービスのディレクトリをもう1つ作っても確認します。
参考
- このVMにはGitLabサーバーもランナーもなく、gitlab-ci-local 4.75.1が.gitlab-ci.ymlをGitLabと同じルールで解釈し、シェルでジョブを実行します。
image:を書くとDockerで動かそうとするので使いません。保護変数・マスキング・CI_JOB_TOKEN・ランナーのタグ・マージリクエストパイプラインの作成はサーバーの機能なので、ここでは再現されません。 - 実行はリポジトリのルートで
gitlab-ci-local --shell-isolation --no-artifacts-to-source(ジョブごとに別の作業ディレクトリを使い、アーティファクトをリポジトリに書き戻さない)、ジョブ一覧はgitlab-ci-local --list-csv-all、解釈された設定はgitlab-ci-local --previewです。gitlab-ci-localはgitが追跡しているファイルだけをジョブに渡すので、ファイルを作ったらgit addしてください。採点ツールはリポジトリをコピーし、すべてのファイルをコミットしたあとで、同じツールでもう一度実行します。 - gitlab-ci-localの違い(実測): 子パイプラインは実験的機能で、trigger:strategyに従わないため、子が失敗してもtriggerジョブは成功で終わります。
--variableは子に渡されず、triggerジョブのvariablesだけが渡されます。 - 子パイプラインのジョブのログも
.gitlab-ci-local/output/<잡이름>.logに残ります(プレースホルダーはジョブ名です)。 - parallel:matrix(Job control)・Downstream pipelines・needs・Specify when jobs run with rules
Python2種とOS2種、4つのジョブを1つのブロックにまとめる
/root/glci-monoをgitリポジトリにし(.gitignoreに.gitlab-ci-local/を入れます)、.gitlab-ci.ymlにstages[build, package, trigger, generate, dynamic]とジョブbuild(stageはbuild)を置いてください。parallel: matrixで、PYは"3.11"・"3.12"、OSはlinux・alpineの組み合わせを作り、スクリプトはdist/py$PY-$OS.txtにpy=<PY> os=<OS>を書き、artifactsでdist/をアップロードします。コミットして実行してください。ジョブ名がbuild: [3.11,linux]の形で4つできるはずです。
matrixの1つの項目の中に複数の変数を書くと、すべての組み合わせがジョブになります。バージョンの値は引用符で囲んでおけば、3.10が3.1になる心配がありません。一覧はgitlab-ci-local --list-csv-allで見ます。
サポートしない組み合わせを1つだけ除く
buildにrulesを追加し、OSがalpineかつPYが"3.11"の組み合わせだけwhen: never、残りはwhen: on_successにしてください。コミットして実行すると、ジョブが3つだけ動くはずです。
matrixの変数は、rules:ifでたいていCI/CD変数のように使えます。組み合わせを除くためにmatrixを2つの項目に分けると、増えるたびに一覧を計算し直すことになりますが、ルールで除けば例外だけを書けば済みます。
組み合わせ1つのアーティファクトだけを待って受け取る
ジョブpackage-linux(stageはpackage)を追加し、needsでbuildのPY: "3.12"、OS: linuxの組み合わせ1つだけを指して(needs:parallel:matrix)、スクリプトはls distにしてください。コミットして実行すると、package-linuxのログにpy3.12-linux.txtだけが見えるはずです。
matrixで生まれたジョブをneedsに名前のまま(build: [3.12,linux])書く代わりに、parallel:matrixで変数の値を書いて指します。指した組み合わせのアーティファクトだけを受け取ります。
サービスの設定はサービスのディレクトリに置く
/root/glci-mono/services/api/ci.ymlにジョブapi-unit(echo "api unit svc=$SVC")を置き、親にジョブtrigger-api(stageはtrigger)を追加して、変数SVC: apiとtrigger: include: services/api/ci.ymlを置いてください。コミットして実行すると、子パイプラインのapi-unitのログがapi unit svc=apiになるはずです。
triggerジョブは、スクリプトの代わりに別のパイプラインを作ります。includeで指したファイルが子パイプライン全体の設定になり、triggerジョブのvariablesは子に渡されます。サービスチームが自分のディレクトリのファイルだけを直せば済む構造です。
変更があったサービスのパイプラインだけを動かす
services/web/ci.ymlにジョブweb-unit(echo "web unit svc=$SVC")を置き、親にtrigger-web(SVCはweb)を追加してください。2つのtriggerジョブには、それぞれrules: changes:でservices/api/**/*・services/web/**/*を置きます。/root/glci-mono-origin.gitにbareリポジトリを作ってoriginとして登録し、コミットしたmainをpushしたあとでgit remote set-head origin mainを実行してください。採点ツールは、コピーでwebだけを直したブランチを作り、trigger-webだけが一覧にあるかを確認します。
モノレポですべてのサービスを毎回ビルドすると、パイプラインの時間がサービスの数だけ増えます。changesはリモートのデフォルトブランチと比べた変更を見る(gitlab-ci-localの場合)ので、リモートが必要です。
リポジトリの構造を読んで子パイプラインを作る
/root/glci-mono/scripts/generate.shが、services/の下のディレクトリごとにジョブlint-<이름>(echo "lint <이름>")を含むYAMLを標準出力に出すようにしてください(プレースホルダーはディレクトリ名です)。親のジョブgenerate(stageはgenerate)がその出力をgenerated.ymlとして保存してartifactsでアップロードし、ジョブrun-generated(stageはdynamic、needs: [generate])がtrigger: include: - artifact: generated.yml, job: generateでそれを子パイプラインとして動かすようにします。コミット・pushして実行すると、lint-apiとlint-webが動くはずです。
子パイプラインの設定を、ジョブが実行中に作れます。サービスが増えるたびに親の設定を直す代わりに、ルール(ディレクトリ=ジョブ)をスクリプトに置きます。生成された設定の中のincludeにはCI/CD変数を使えないという制約が、公式ドキュメントにあります。
サービスを1つ足しても親の設定はそのまま
/root/glci-mono/services/billing/に、ci.ymlではなくREADME.md(内容は何でもかまいません)だけを作って、コミット・pushしてください。親の.gitlab-ci.ymlは直しません。実行すると、動的な子パイプラインにlint-billingが新しくできて動くはずです。採点ツールは、コピーにサービスのディレクトリをもう1つ作っても確認します。
動的パイプラインのルールがリポジトリの構造にあるので、ディレクトリができるだけでジョブができます。逆に、servicesの下にサービスではないディレクトリを置くと、それもジョブになるということなので、ルールをドキュメントとして残しておく必要があります。