この記事は GitLab Advent Calendar 2022 の 11 日目の記事です。
3行で
- ジョブを 処理 と 起動条件 の2要素に分解して考える。
- 起動条件を実装する
rules
部分だけを hidden ジョブとして外部化し、それを継承してジョブを作成する。 - hidden ジョブを定義した yaml ファイルを持つリポジトリを
include
する形式にすることで、部品として管理できるようになる。
rules
について
.gitlab-ci.yml
には、パイプラインにジョブを含めるか否かを制御するための rules
キーワード があります。リポジトリへのプッシュ時やタグ作成時などのタイミングで、パイプラインを作成する際に rules
で指定した条件が評価され、パイプラインで実行するジョブが決まります。
GitLab 11 系までは only
、except
キーワードを使ってジョブを制御していましたが、GitLab 12 系(12.3)以降では、rules
の利用が推奨されています。
rules
配下には、次のルールの配列を定義できます。
ルール | 説明 |
---|---|
if | 指定した条件式に一致するか? |
changes | 特定のファイルの変更があるか? |
exists | 特定のファイルがリポジトリに存在するか? |
allow_failure | ジョブが追加された場合、パイプラインを止めずにジョブの失敗を許す。 |
variables | 条件に合致した場合、ジョブに変数を追加する。 |
when | ジョブをどのタイミングで実行するか制御する。1 |
この中でも、使用頻度が高いのは if
かと思います。特定のブランチへpushされた場合や、特定の名前のタグが作成された場合、特定の変数が設定された場合等にジョブを起動するように制御することができます。
GitLab の公式ドキュメントにある rules:if
の例 は次のとおりです。
job:
script: echo "Hello, Rules!"
rules:
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != $CI_DEFAULT_BRANCH
when: never
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/
when: manual
allow_failure: true
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
この job
ジョブの条件は次のようになります。
- マージリクエストのマージ元ブランチ名が
feature
から始まり、マージ先ブランチがリポジトリのデフォルトブランチの場合は、ジョブを追加しない。 - マージリクエストのマージ元ブランチ名が
feature
から始まる場合は、手動起動のジョブを追加し、ジョブの失敗を許す。 - マージリクエストのマージ元ブランチ名がある場合(=マージリクエストにより作成されたパイプラインの場合)は、ジョブを追加する。
このように、複数の条件式を組み合わせて、ジョブの起動条件を細かく指定することができます。
ジョブの処理と起動条件を分離する
ジョブの起動条件は、開発プロセスの中でのブランチやマージリクエスト、タグの使われ方に密接に関連しています。したがって、システム全体の開発プロセスを定め、それに合わせて rules
を実装していくことが多いと思います。つまり、一つのプロジェクト(≠ GitLab の Project)の中で複数のリポジトリを持つ場合にも、同じようなルールを使って CI/CD を構成していくことになります。
通常、GitLab ではリポジトリ単位の .gitlab-ci.yml
でパイプラインを構成するため、複数のリポジトリを持つ場合はそれぞれ別の .gitlab-ci.yml
が作られることになりますが、先に述べたとおり、ルールの部分については実装が重複しがちです。
そこで、ジョブの構成要素を 処理
と 起動条件
の2つに分離して考えてみます。例えば、feature
ブランチへのpush時に、テスト環境へデプロイするジョブ は次の2つの要素に分離します。
- 処理:テスト環境へデプロイする
- 条件:feature ブランチへのpush時にジョブを起動する
処理の部分は、開発する言語やリポジトリの用途によって変わるものであり、カスタマイズが必要な部分です。一方で、条件の部分は他の処理の場合にも使い回しが利くものです。例えば、feature
ブランチへのpush時にビルドするジョブ であれば、条件については全く同じものを利用できます。このように、処理と条件を分離することで、条件部分を部品的に使い回すことができ、メンテナンス性を高めることができます。
処理と条件を分離した場合の、ジョブの構成のイメージを図示するとこうなります。
.gitlab-ci.yml
は extends
キーワード2を使ってジョブを継承できるため、rules
だけを定義した hidden ジョブ3 を用意し、処理を記載するジョブ側でそれを継承することで、上記のような構成を実現できます。
継承を使った処理と条件の分離の例を次に示します。
# . から始まるジョブは hidden ジョブであり、ジョブとして評価されない
.rules-push-feature:
rules:
- if: "$CI_COMMIT_BRANCH =~ /^feature/"
stages:
- build
job:
stage: build
# hidden ジョブを継承して rules 部分を取り込む
extends:
- .rules-push-feature
script:
- echo "job-feature"
rules の部品化
さて、上のイメージ図を見ても分かるとおり、条件部分を複数のリポジトリ間で共有する必要があります。これは include
キーワード4を使って実現できます。
まず、rules
用の hidden ジョブを実装した yaml ファイルを別のリポジトリに切り出します。そして、パイプラインを動かす側のリポジトリでは、その yaml ファイルを include
で取り込みます。すると、extends
で hidden ジョブを継承できるようになるので、rules
の部分は 継承元をそのまま利用し、処理など必要な箇所のみを実装するようにします。
このように、rules
を部品のように扱うことで、ジョブの起動条件だけをプロジェクト全体で一律に管理できるようになります。ブランチ名やタグ名のルールに修正が入る場合には部品を修正するだけで済むので、修正コストも小さくすることができます。
先ほどのイメージ図を少し修正し、わかりやすくしましょう。
.gitlab-ci.yml
の構成例は以下のとおりです。
.rules-push-feature:
rules:
- if: "$CI_COMMIT_BRANCH =~ /^feature/"
同じ GitLab インスタンス 内のプロジェクトから参照する場合は include:project
を使うことで読み込むことができます。
include:
project: <some-group>/<project-name>
file: rules.yml
stages:
- build
job-feature:
stage: build
extends:
- .rules-push-feature
script:
- echo "job-feature"
上記のような構成で、rules
以外の部分も部品化することができ、共通的な処理やvariableを一括で管理することができたりします。小規模なプロジェクトで小回りが効くケースでは、このような部品化のメリットは少ないかもしれませんが、中〜大規模なプロジェクトでは一度採用を検討してみてもいいかもしれません。
まとめ
ジョブを 処理と起動条件に分離して考え、rules
部分を部品化する方法について説明しました。
rules は少し癖のある書き方が必要なため、有識者がまとめて実装し、その部品を使うだけにしたほうが安心かと思います。5
本記事で紹介したやり方を参考に、皆さんのプロジェクトでも include
と extends
を活用してみてください。
-
デフォルトは
when: on_success
であり、前のステージのすべてのジョブについて、成功するか あるいはallow_failure: true
の場合に ジョブが実行されます。 ↩ -
例えば、
$CI_PIPELINE_SOURCE == "push"
という条件はブランチまたはタグがpushされたときにジョブが稼働します。ブランチだけを対象とする場合は$CI_PIPELINE_SOURCE == "branch"
ではなく、$CI_COMMIT_BRANCH
を使う必要があります。しかし、$CI_COMMIT_BRANCH
は スケジュールビルドでも可動をするため、いわゆる「コミット」を契機として起動するわけではありません。厳密に「ブランチへのpush時だけ」にするには、複合条件を書く必要があります。 ↩