ちょっと話題の記事

GitLabのCI/CDで超重要なrulesの全てを理解する

GitLabのCI/CDを制御するために欠かせないrulesについての解説記事です。公開サンプルもありGitLab.comのアカウントがあれば即手元で動かせるので、.gitlab-ci.ymlの書き方に迷っている方は一度これを機に学んでみると良いんじゃないでしょうか。
2021.12.19

「あ、あかん、このrulesの意味がぜんぜんわからん…」

ここ一年ぐらい、GitLab.com上での開発をメインでやっているハマコーです。現プロジェクトでもGitLab Runnerを利用したCI/CDを開発サイクルの中で回しているのですが、今までナンチャッテで理解していた.gitlab-ci.ymlにちょっと複雑なジョブ起動条件を設定しようとしてハマってしまいました。

主にこのあたりはrulesキーワードを使って制御していくのですが、正直慣れていないと記述方法や考え方などハマりどころが多かったため、ごく基本的なところからrulesを理解するための情報をまとめてみました。この記事が、これからGitLabで本格的にCI/CDを組もうとされている方にも、すでにばっりばりにCI/CD運用している方にも、何らしか参考になれば幸いです。

この記事はGitLabのカレンダー | Advent Calendar 2021 - Qiitaの、18日目の記事になります。

(祭) ∧ ∧
 Y  ( ゚Д゚)
 Φ[_ソ__y_l〉     GitLabマツリダワッショイ
    |_|_|
    し'´J

.gitlab-ci.ymlとは?

GitLabで、CI/CDを実現するために、gitリポジトリに格納するファイルです。標準のファイル名は.gitlab-ci.ymlで、ルートディレクトリに格納されCI/CD設定の全てをこのファイルに記述します。

公式情報

.gitlab-ci.yml自体のマニュアルトップ。利用上の注意点やスコープが記載されています

.gitlab-ci.ymlのキーワードのリファレンス。利用できる予約語に対する詳細な説明が含まれています。

実際にGitLabでCI/CDを実現するためのインフラとしてGitLab Runnerの設定が必要になりますが、本記事ではスコープ外です。もし詳しい情報が必要な方は、こちらを参考に。

ただ、特に各ProjectでGitLab Runnerを設定していなくても、デフォルトではGitLab.comにホストされたShared Runnerを利用してくれるはずなので、今回のサンプルの実行には別途GitLab Runnerの設定は不要かと思います。

何故rulesについての記事を書くのか?

.gitlab-ci.ymlには、CI/CDで必要となる全ての情報を埋め込む必要があるんですが、rulesは頻出の割には、理解がちょっと難しいと感じたからです。

プロジェクトの取り決めによりますが、一つのリポジトリにアプリケーションコードもテストコードもインフラのコードも全て含まれている場合、CI/CDの中で、各ジョブの起動条件を制御することは頻出なのですが、自分の英語力のへぼさも相まって、最初公式マニュアルだけを見てても理解に苦しみました。

そんな頻出設定なrulesをこれから記述する人向けに、この記事が参考になれば幸いです。

rulesではなくて、onlyやexceptを使えばよいのでは?

GitLabのCI/CDでのブランチコミットの起動条件で検索するとonlyexceptを利用したサンプルが多数でてきますが、現在はrulesの利用が推奨されています。

only and except are not being actively developed. rules is the preferred keyword to control when to add jobs to pipelines.
引用:Keyword reference for the `.gitlab-ci.yml` file | GitLab

onlyは直感的に利用できて便利なのですが、より汎用的にジョブの起動条件を設定するのにrulesを公式が推しているのであれば、rulesを学んでおいて損は無いでしょう。

手元で動かすための公開サンプルプロジェクト

今回、実際に手元で動かして試してみたい方もいると思うので、学習用のsampleプロジェクト公開しています。forkしてらえれば手元でも動きますので、理解を深めるためにこのサンプルを役立ててもらえれば幸いです。

hamako9999-Group / sample-to-understand-rules-in-gitlab-ci · GitLab

ディレクトリ構成。ルートディレクトリの.gitlab-ci.ymlから、各サンプルディレクトリをchild pipelineとして定義。

$ tree -a .
.
├── .gitlab-ci.yml
├── README.md
└── sample00
    └── .gitlab-ci.yml
└── sample01
    └── .gitlab-ci.yml
└── sample02
    └── .gitlab-ci.yml
└── sample04
    └── .gitlab-ci.yml

ルートディレクトリに設定したgitlab-ci.yml。この内容で全てのサンプルプロジェクトが起動します。

.gitlab-ci.yml

stages:
  - samples

trigger-sample00:
  stage: samples
  trigger:
    include: sample00/.gitlab-ci.yml

trigger-sample01:
  stage: samples
  trigger:
    include: sample01/.gitlab-ci.yml

## and more samples

とりあえず、GitLabのWebメニュー、CI/CD -> Pipelinesから、[Run Pipeline]ボタンからPipelineを起動してみてください。おそらく、GitLab.comでホストしているShared Runnerが動くはずですが、うまく動かない場合は、Shared Runnerあたりの設定(GitLab Runner | GitLab)を確認しましょう。

サンプル0「rules学ぶ前の元ネタ」

1ステージ、1ジョブでechoだけするシンプルな元ネタを利用します。rulesも何も含まれていないため、当然ですがPipelineを起動することで、echoが動作します。

sample00/.gitlab-ci.yml

stages:
  - build

build-code-job:
  stage: build
  script:
    - echo "無事に動いてますか?"

サンプル1「初めてのrules」

さて、前置き長くなりましたが、ようやく.gitlab-ci.ymlrulesを組み込んでいきます。全て公式マニュアル Keyword reference for rulesに記載されている内容ですが、それらをかいつまんで説明していくのが、このブログの趣旨です。

ここで早速、rulesを使う上での注意点。

  • rulesは、Pipelineが起動されたタイミングで評価され、各ジョブがそのPipelineに含まれるかどうかをrulesに則って評価します。そのため、リポジトリに格納したファイルの内容を読み込んで、rulesの評価対象にすることはできません
  • rulesは、onlyexceptキーワードを置換するため、双方のキーワードを同時に利用することはできません。もし設定した場合、key may not be used with rulesというエラーになります

rulesには、以下のキーワードを配列として含めることができます。

  • if
  • changes
  • exists
  • allow_failure
  • variables
  • when

とりあえず、whenのみを利用したシンプルなrulesを見てみます。

sample01/.gitlab-ci.yml

stages:
  - build

build-job-work:
  stage: build
  script:
    - echo "これは動く"
  rules:
    - when: on_success

build-job-work-always:
  stage: build
  script:
    - echo "これも動く"
  rules:
    - when: always

build-job-never-work:
  stage: build
  script:
    - echo "これは動きません"
  rules:
    - when: never

この場合、ジョブbuild-job-never-work:は、動きません。正確にはPipelineのジョブとして乗りません。build-job-workbuild-job-work-alwaysが動いてますね。

サンプル2「特定ブランチコミットをジョブ追加条件とする」

ここからは、もう少しよくあるユースケースでのサンプルを見ていきます。ここでは、mainブランチへのコミットをのみをジョブの追加条件としたい場合です。

この書き方では、build-job-when-main-branchジョブのみがパイプラインに追加され、残りの2つのジョブはパイプラインに追加されません。

sample02/.gitlab-ci.yml

stages:
  - build

build-job-when-main-branch:
  stage: build
  script:
    - echo $CI_COMMIT_BRANCH
    - echo "メインブランチへのコミット時のみ、パイプラインに追加される"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

build-job-when-main-branch-not-work:
  stage: build
  script:
    - echo $CI_COMMIT_BRANCH
    - echo "メインブランチへのコミット時だが、when neverがあるため、パイプラインに追加されない"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: never

build-job-when-main-branch-not-work2:
  stage: build
  script:
    - echo $CI_COMMIT_BRANCH
    - echo "ifステートメントにtrueが存在したいため、パイプラインに追加されない"
  rules:
    - if: '$CI_COMMIT_BRANCH == "amema"'
    - if: '$CI_COMMIT_BRANCH == "koryama"'
    - if: '$CI_COMMIT_BRANCH == "wassyoi"'

rules:ifステートメントの解説

rules:ifステートメントの公式マニュアル

rules:if

rules:ifステートメントの条件です。

  • ifステートメントがtrueの場合は、パイプラインにジョブが追加されます
  • ifステートメントがtrueだが、when:neverと組み合わされている場合、パイプラインにジョブが追加されません
  • ifステートメントにtrueがない場合、パイプラインにジョブが追加されません

何を当たり前のことを言っているんだ?となりそうですが、公式で提供されている以下のサンプルを読み解きながら理解していきます。

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'

上記の場合、上から以下の順番で評価されます。

  • マージリクエストのソースブランチ名が「feature」で始まっていて、かつマージリクエストのターゲットブランチがデフォルトブランチ(ここではmain)以外の場合は、パイプラインにジョブを追加しない
  • マージリクエストのソースブランチ名が「feature」で始まっていて、かつ手動起動の場合、パイプラインにジョブを追加する。その時、ジョブが失敗しても、パイプラインを停止しない
  • マージリクエストのソースブランチ名が指定されている場合(マージリクエストの場合)、パイプラインにジョブを追加する

rules:ifステートメントのその他サンプル

このrules:ifで利用できる環境変数周辺の条件式サンプルは、こちらのマニュアルで豊富なサンプルが定義されているので、必ず合わせて参照することをオススメします。

Choose when to run jobs | GitLab

こういった、正規表現を使ったタグのセマンティックバージョニング書式の指定も可能です。

  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+.\d+.\d+/

CI/CDで利用できる環境変数について

CI/CDで利用できる事前定義の環境変数マニュアルはこちら。

Predefined variables reference | GitLab

この環境変数一覧とニラメッコすることで、どの条件の時にパイプラインにジョブを追加できるかを指定できるようになるはずです。指定方法のイメージは湧いたのではないでしょうか。

サンプル3「特定ブランチのタグ付与をジョブ追加条件とする」

結論から言うと、特定ブランチのタグ付与を条件としてジョブを起動する方法は、GitLabのCIの仕様としてできません。

試しにやってみましょう。mainブランチにタグが付与されたときだけ、プロダクション環境にデプロイする意図で、builddeployの2つのステージがある、以下の.gitlab-ci.ymlを作成します。

sample03/.gitlab-ci.yml

stages:
  - build
  - deploy

build-job:
  stage: build
  script:
    - echo $CI_COMMIT_BRANCH
    - echo $CI_COMMIT_TAG
    - echo $CI_COMMIT_REF_NAME
    - echo "アーティファクトのビルド"

deploy-job:
  stage: deploy
  script:
    - echo $CI_COMMIT_BRANCH
    - echo $CI_COMMIT_TAG
    - echo $CI_COMMIT_REF_NAME
    - echo "この条件は真にならず、デプロイは実行されない"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG != null'

mainブランチへpushしてみると、以下のようにdeployステージのジョブがPipelineに追加されません。まぁここまではタグを付与していないので当たり前の話ですが。

では、mainブランチ上にタグをv1.0.0として定義し、プッシュしてみます。

$ git tag -a v1.0.0 -m 'test tag push'

$ git log
commit 629c5edeea0bb360f10fb6cb49db56c36007fdc2 (HEAD -> main, tag: v1.0.0, origin/main, origin/HEAD)

$ git push origin v1.0.0

これにより、deployステージが実行されるかと思いきや実行されず。mainブランチへのpushと同じ結果になります。

環境変数の内容を確認

build-jobの中で、下記3種類の環境変数をechoしているのでそのログを確認すると以下の結果となっています。

mainブランチにpushした場合。

$ echo $CI_COMMIT_BRANCH
main
$ echo $CI_COMMIT_TAG
$ echo $CI_COMMIT_REF_NAME
main

mainブランチのコミットにタグを付与してpushした場合。

$ echo $CI_COMMIT_BRANCH
$ echo $CI_COMMIT_TAG
v1.0.0
$ echo $CI_COMMIT_REF_NAME
v1.0.0

このあたりマニュアルに記載されているとおりですが、以下の内容となります。

  • CI_COMMIT_BRANCH
    • ブランチからプッシュした時にブランチ名が入る。タグプッシュの場合は、空白
  • CI_COMMIT_TAG
    • タグプッシュした時にタグ名が入る。ブランチプッシュの場合は、空白
  • CI_COMMIT_REF_NAME
    • ブランチ名かタグ名のいずれかが入る

そもそもGitのタグは、任意のコミットに対するタグ名の付与という概念なので、タグとブランチの間に直接の関係性はないためそういう仕様のようです。つまり、Pipelineの起動時、タグ名とそのタグが付与されたブランチの名前の両方を取得することは不可能です。

自分はこの仕様に気づくまでそれなりに時間かけてしまったので、同じところでハマる人が今後現れないよう願っています。実際にプロダクション環境へのリリースを、タグ付与をトリガーとするか、もしくは例えばプロダクションリリース用のreleaseブランチへのマージをトリガーにするかは、各組織の運用体制によると思いますが、タグ付与の権限制限や、ブランチのプロテクトなど、リポジトリ側の設定と合わせてこのあたり設計することをおすすめします。

GitLab flowは、Git flowともGitHub flowとも違いますので、GitLab.comでのブランチ運用の検討には、下記GitLab flowの解説を参考にするのも良いと思います。

Introduction to GitLab Flow | GitLab

サンプル4「任意のファイルの変更をジョブ追加条件とする」

一つのリポジトリで、複数種類(アプリコード、インフラコードなど)のファイルを管理する場合、ファイル変更をジョブ追加条件に指定するのは必須です。その場合は、rules:changesが利用できます。

rules:changes

中身は何でも良いので、新たにapplication.txtinfra.txtのファイルを追加します。

ディレクトリ構成。

$ tree sample04 -a
sample04
├── .gitlab-ci.yml
├── application.txt
└── infra.txt

以下の、.gitlab-ci.ymlを用意します。

sample04/.gitlab-ci.yml

stages:
  - build

build-application-job:
  stage: build
  script:
    - echo "アプリケーションコードのビルド"
    - cat sample04/application.txt
  rules:
    - changes:
      - sample04/application.txt

build-infra-job:
  stage: build
  script:
    - echo "インフラコードのビルド"
    - cat sample04/infra.txt
  rules:
    - changes:
      - sample04/infra.txt

プッシュのタイミングによりますが、プッシュ時に各ファイルに変更が入っていれば、各ジョブが実行されます。

ルートディレクトリに置いた親.gitlab-ci.ymlでのchangesの使い方

この記事の冒頭、ルートディレクトリに配置し、child pipelineとしてサンプルプロジェクトのジョブを起動する以下の.gitlab-ci.ymlを紹介しました。ただ、この場合だとchild pipelineを無条件で起動してしまいます。

.gitlab-ci.yml

stages:
  - samples

trigger-sample00:
  stage: samples
  trigger:
    include: sample00/.gitlab-ci.yml

## more samples

もし、サンプルプロジェクト配下のファイルのみが変更された時に、そのchild pipelineを起動する場合は、以下のようにrules:changesを記述することで実現できます。

.gitlab-ci.yml

stages:
  - samples

trigger-sample00:
  stage: samples
  trigger:
    include: sample00/.gitlab-ci.yml
  rules:
    - changes: 
      - sample00/**/*
## more samples

child pipelineは、複数種類のコードをまとめて同じリポジトリで管理する場合に、ほぼ必須の考え方となるので、詳細は以下の公式ドキュメントをしっかり把握することをおすすめします。

Parent-child pipelines | GitLab

公式ドキュメント提供の有用なサンプル

こちらに、workflow用のサンプルが掲載されています。workflowは、rulesがジョブのパイプラインへの追加条件を指定するものであったのに対し、パイプラインそのものの起動条件を定義します。が、rulesの書き方のサンプルになるものも多数記載されているので、こちらも見ておくと、利用できるシチュエーションがいろいろ把握できるのでおすすめです。

GitLab CI/CD workflow keyword | GitLab

まとめ:これからのGitLab環境のCI/CDの整備のお供としての基本的な考え方を知っておこう

今回、rulesの把握のため、公式ドキュメントをひたすら読み漁りながら理解を深めてきました。丁寧に読み込めば、記載の意図や動作の仕組みなどが詳細に記載されているので、めちゃくちゃ有用でした。

CI/CDのユースケースに応じて関連キーワードでググることで、有象無象のブログに引き当たることが多くあると思いますが、エッジケース含めた対応力応用力をつけておこうとするとそれでは足りず、公式ドキュメントを参照する必要が必ずあると思います。

一度、動かしてみてだいたいの感触を掴んだ後は、ジョブやパイプラインそのものの考え方も含めて、下記Gitlab.comのCI/CDのドキュメントをしっかり読み込むことが結局は近道かなと思います。

それでは、今日はこのへんで。濱田(@hamako9999)でした。