CI/CD開発ガイドライン

CI/CDに特化した開発者向けガイドラインを掲載しています:

CI/CD YAMLリファレンスドキュメンテーションガイドを参照し、リファレンスページを更新する方法を学んでください。

CI/CDの使用例

私たちはci-sample-projects グループをメンテナーしており、GitLab CI/CD の様々なユースケースの.gitlab-ci.yml の例を紹介しているプロジェクトがあります。また、異なるシナリオで使用できる特定の構文もカバーしています。

CIアーキテクチャの概要

以下はCIアーキテクチャの簡略図です。主要なコンポーネントに焦点を当てるため、いくつかの詳細は省略しています。

CI software architecture

左側には、様々なイベント(ユーザーやオートメーションによってトリガーされる)に基づいてパイプラインをトリガーできるイベントがあります:

これらのイベントのいずれかをトリガーすると、CreatePipelineService が起動し、イベントデータとトリガーしたユーザーを入力として受け取り、パイプラインの作成を試みます。

CreatePipelineServiceYAML Processor コンポーネントに大きく依存しています。 コンポーネントは入力として YAML blob を受け取り、パイプラインの抽象データ構造 (ステージとすべてのジョブを含む) を返します。このコンポーネントはYAMLを処理する間、YAMLの構造を検証し、構文もしくはセマンティックエラーを返します。YAML Processor コンポーネントはパイプラインを構造化するために利用可能なすべてのキーワードを定義する場所です。

CreatePipelineServiceYAML Processorによって返された抽象データ構造を受け取り、それを(パイプライン、ステージ、ジョブのような)永続化されたモデルに変換します。その後、パイプラインを処理する準備が整います。パイプラインの処理とは、以下のいずれかになるまで、実行順(ステージまたはDAG)にジョブを実行することです:

  • 期待されるすべてのジョブが実行されました。
  • 失敗によりパイプラインの実行が中断されました。

パイプラインを処理するコンポーネントはProcessPipelineServiceで、パイプラインのすべてのジョブを完了した状態にする責任を負います。パイプラインが作成されると、そのジョブはすべて初期created 状態に createdなります。created このサービスは created、パイプライン構造に基づいて、ステージcreated 内のどのジョブが created処理される資格があるかを調べます。そして、それらをpending の状態に移動します。つまり、Runnerがジョブをピックアップできるようになります。ジョブは実行された後、成功または失敗します。パイプライン内のジョブの各ステータス遷移は、このサービスを再度トリガーし、完了に向けて遷移する次のジョブを探します。その間、ProcessPipelineService はジョブ、ステージ、パイプライン全体のステータスを更新します。

図の右側には、GitLabインスタンスに接続しているRunnerのリストがあります。これらは共有Runner、グループRunner、プロジェクトRunnerとなります。Runner と Rails サーバーとの通信は、API エンドポイントのグループRunner API Gatewayを通して行われます。

ランナーを登録、削除、検証することができ、データベースへの読み取り/書き込みクエリも発生します。ランナーが接続されると、次のジョブの実行を要求し続けます。これにより、RegisterJobService が呼び出され、次のジョブが選択されてランナーに割り当てられます。この時点で、ジョブはrunning 状態に遷移します。状態遷移により、再びProcessPipelineService がトリガーされます。詳細はジョブ・スケジューリングを参照してください。)

ジョブが実行されている間、Runnerはサーバーにログを送り返すと同時に、保存が必要なアーティファクトを送信します。また、ジョブは以前のジョブのアーティファクトに依存して実行されることがあります。この場合、ランナーは専用のAPIエンドポイントを使用してそれらをダウンロードします。

アーティファクトはオブジェクトストレージに保存され、メタデータはデータベースに保存されます。アーティファクトの重要な例として、レポート(JUnit、SAST、DASTなど)があり、マージリクエストで解析されレンダリングされます。

ジョブステータスの遷移はすべて自動化されているわけではありません。ユーザーは手動ジョブを実行したり、パイプラインをキャンセルしたり、特定の失敗したジョブやパイプライン全体を再試行したりすることができます。パイプライン全体のステータスを追跡する責任があるため、ジョブのステータスを変更させるものはすべて、ProcessPipelineService をトリガーします。

特別なタイプのジョブはブリッジジョブでpending 状態に遷移するときにサーバーサイドで実行されます。このジョブは、マルチプロジェクトや子パイプラインなどのダウンストリームパイプラインを作成します。ワークフローのループは、ダウンストリームパイプラインがトリガーされる度にCreatePipelineService から再スタートします。

CI Backend Architectural Walkthroughでアーキテクチャのウォークスルーを見ることができます。

ジョブスケジューリング

パイプラインが作成されると、すべてのジョブはすべてのステージで一度に作成され、初期状態はcreated です。これにより、パイプラインの全内容を可視化することができます。

created 状態のジョブはまだ Runner には見えません。ジョブを Runner に割り当てるためには、ジョブはまずpending 状態に遷移する必要があります:

  1. ジョブはパイプラインの最初のステージで作成されます。
  2. ジョブは手動で開始する必要があり、トリガーされました。
  3. 前のステージのジョブはすべて正常に完了しました。この場合、次のステージからすべてのジョブをpendingに移行します。
  4. ジョブはneeds: を使用して DAG 依存関係を指定し、依存するジョブはすべて完了します。
  5. ジョブは、Ci::PipelineCreation::DropNotRunnableBuildsService によって実行不可能な状態にあるため、削除されていません。

Runnerが接続されると、サーバーを連続的にポーリングすることによって、次のpending ジョブの実行を要求します。

note
ランナーが GitLab とやりとりするために使う API エンドポイントは lib/api/ci/runner.rb

サーバーはリクエストを受け取った後、Ci::RegisterJobService アルゴリズム に基づいてpending ジョブを選択し、ジョブを割り当てて Runner に送信します。

現在のステージのすべてのジョブが完了すると、サーバーは次のステージのすべてのジョブの状態をpending に変更することで「ロックを解除」します。これにより、ランナーが新しいジョブを要求したときに、スケジューリングアルゴリズムがこれらのジョブを選択できるようになります。

Runner と GitLab サーバー間の通信

登録トークンを使って Runner が登録されると、サーバーは実行できるジョブの種類を知ることができます。これは

  • 登録されたランナーのタイプ:
    • 共有ランナー
    • グループランナー
    • プロジェクト・ランナー
  • 関連するタグ

Runner はジョブの実行をPOST /api/v4/jobs/request でリクエストすることで通信を開始します。ポーリングは数秒おきに行われますが、ジョブキューが変更されない場合は、HTTPヘッダによるキャッシュを活用してサーバー側の作業負荷を軽減します。

このAPIエンドポイントはCi::RegisterJobServiceを実行します:

  1. pending ジョブのプールから次に実行するジョブをピックします。
  2. ランナーに割り当てます。
  3. APIレスポンスでランナーに提示

Ci::RegisterJobService

このサービスがジョブの大部分を収集するために使用するトップレベルのクエリが3つあり、それらはランナーが登録されているレベルに基づいて選択されます:

  • 共有ランナー(インスタンスレベル)のジョブの選択
    • 実行中のビルドが少ないプロジェクトを優先する公平なスケジューリングアルゴリズムを利用
  • グループRunnerのジョブを選択
  • プロジェクトランナーのためのジョブを選択します。

このジョブリストは、ジョブタグとランナータグの間のタグのマッチングによってさらにフィルタリングされます。

note
ジョブにタグが含まれている場合、ランナーはすべてのタグに一致しないジョブを選択しません。ランナーはジョブに対して定義されたタグよりも多くのタグを持つことができますが、その逆はできません。

最後に、ランナーがタグ付けされたジョブのみを選択できる場合、タグ付けされていないジョブはすべてフィルタリングされます。

この時点で、残りのpending ジョブをループし、追加のポリシーに基づいて Runner が “選択できる” 最初のジョブを割り当てようとします。たとえば、protected とマークされた Runner は、保護されたブランチ (本番デプロイなど) に対してのみ実行されるジョブを選ぶことができます。

プール内の Runner の数を増やすと、同じジョブを異なる Runner に割り当てた場合に発生するコンフリクトの可能性も増えます。それを防ぐために、コンフリクトエラーを潔くレスキューし、リストの次のジョブを割り当てます。

スタックしたビルドの削除

ビルドを “stuck “としてマークし、ドロップするには2つの方法があります。

  1. ビルドが作成されるとき、Ci::PipelineCreation::DropNotRunnableBuildsService はジョブを実行不可能にするような既知の条件を前もってチェックします:
    • ビルドを実行するのに十分なCI/CD Minutesがない場合、ci_quota_exceeded
    • 将来、プロジェクトがallowed_plans を通してビルドに必要なランナーが利用可能なプランにない場合、ビルドはno_matching_runner で即座に中止されます。
  2. ビルドをピックアップする利用可能なRunnerがいない場合、1時間後にCi::StuckBuilds::DropPendingService
    • ジョブが24時間以内にRunnerによってピックアップされなかった場合、そのジョブは自動的に処理キューから削除されます。
    • 保留中のジョブがスタックした場合、そのジョブを処理できるランナーがいない場合は、1時間後にキューから削除されます。
    • どちらの場合も、ジョブのステータスは適切な失敗理由とともにfailed に変更されます。

この違いの背景には

CIミニッツクォータの仕組みは、ジョブが作成された早い段階で処理されます。一旦プロジェクトが制限を超えると、それにマッチするすべての次のジョブは来月が始まるまで適用されます。もちろん、プロジェクトオーナーは追加分を購入することができますが、それはプロジェクトが手動で行うアクションです。

allowed_plans 。プロジェクトが必要なプランになく、ジョブがそのようなランナーをターゲットにしている場合、プロジェクトオーナーが設定を変更するか、名前空間を必要なプランにアップグレードするまで、そのジョブは常に失敗します。

これら2つのメカニズムは非常にSaaSに特有であり、同時にSaaSの規模を考慮するとかなり計算コストがかかります。ジョブが保留に移行する前にチェックを行い、早期に失敗させることは非常に理にかなっています。

なぜ保留中のジョブを早期に処理しないのでしょうか?場合によっては、ジョブがペンディング状態になっているのは、Runnerがジョブを取り込むのが遅いからにほかなりません。これは GitLab レベルではわかりません。Runnerの設定や容量、GitLabのキューのサイズによって、ジョブはすぐに取り込まれることもあれば、待つ必要があることもあります。

他にも理由があるかもしれません:

  • ランナーのメンテナンスをしていて、しばらくの間まったく利用できない場合、
  • 設定を更新しているときに、誤ってタグ付けや保護フラグを間違えてしまった場合(またはSaaSインスタンスランナーの場合、間違ったコストファクターやallowed_plans の設定を割り当ててしまった場合)。

これらはすべて、一時的な問題であり、ほとんどの場合、起こるとは予想されておらず、早期に発見され、修正されることが期待されています。このような状況が発生した場合、すぐにジョブを中断することは絶対に避けなければなりません。Runnerの能力が限界に達したから、あるいは一時的に利用できない/設定ミスがあったからという理由だけでジョブを落とすことはユーザーにとって非常に有害です。

GitLab CI/CDにおける “ジョブ “の定義

GitLab CIにおける “ジョブ “とは、継続的インテグレーション、デリバリー、デプロイを推進するためのタスクを指します。通常、パイプラインは複数のステージを含み、ステージは複数のジョブを含みます。

Active Recordモデリングでは、ジョブはCommitStatus クラスとして定義されます。それに加えて、以下のようなジョブの種類があります:

  • Ci::Build…Runnerによって実行されるジョブ。
  • Ci::Bridge…ダウンストリームパイプラインのトリガーとなるジョブ。
  • GenericCommitStatus…外部のCI/CDシステム、例えばJenkinsで実行するジョブ。

コードベースで “ジョブ “という用語を使うと、読者はそのクラス/オブジェクトが上記の任意のタイプであると考えるでしょう。特にCi::Build クラスに言及する場合、”job” というオブジェクト/クラス名を使うべきではありません。ドキュメントでは、”Build” の代わりに一般的に “Job” を使うべきです。

私たちのコードベースにはリファクタリングすべきいくつかの矛盾があります。たとえば、CommitStatusCi::Job に、Ci::JobArtifactCi::BuildArtifactにすべきです。完全なリファクタリング計画についてはこのイシューを参照してください。

クォータの計算

GitLab 16.1で “CI/CD minutes “から “compute quota “と “compute minutes “に名称変更

この図は、Compute quota機能とそのコンポーネントがどのように機能するかを示しています。

compute quota architecture

以下のビデオでこの機能の詳細をご覧ください。