ステージの行列とneedsのDAG
一言でいうと
stagesはジョブをステージという枠に並べて、前の枠がすべて終わって初めて次の枠が出発するようにします。needsはその枠の壁を無視して「自分が実際に待つべきもの」だけを指定し、パイプラインをDAGに変えます。
なぜ必要なのか
ステージモデルには、理解しやすいという大きな長所があります。buildがすべて終わればtestが始まり、testがすべて終わればdeployが始まります。問題は、この単純さがそのまま無駄になることです。
testステージにジョブが5つあり、そのうち1つが12分かかる統合テストだとします。残りの4つは1分で終わりますが、deployステージのジョブは12分をまるごと待ちます。実際には、デプロイが統合テストの結果だけあればよいこともあるのに、ステージモデルにはその事実を表現する方法がありません。ジョブ間の本当の依存関係は枠よりずっと疎で、枠はその疎な関係を密なものに丸めてしまいます。
もっともどかしいのは、その反対側のケースです。ソースだけ見ればよいlintジョブが、buildステージの後ろにあるという理由で、ビルドが終わるまで始まることすらできません。開発者は、タイプミス1つを知るためにビルド時間を待ちます。パイプライン全体の時間が15分を超えると、人々は結果を待たずに別の作業をしに行ってしまい、フィードバックループが崩れます。この無駄な待ち時間が、その15分の大きな部分を占めています。
どう動くのか
needsをジョブに書くと、そのジョブはステージの順序を無視して、列挙されたジョブだけを待ちます。このとき、3つのルールが一緒に働きます。
1つ目は、needsをまったく書かないジョブは従来どおりだということです。自分より前のステージにあるすべてのジョブが終わって初めて出発します。そのため、1つのファイルの中でステージ方式とDAG方式が混ざっていてもかまいません。
2つ目は、needs: []は「何も待たない」という意味だということです。キーがないことと空のリストは、正反対の意味を持ちます。この1行が、lintジョブをパイプラインの一番前に引き出します。
3つ目は、needsは前のステージだけを指せるわけではないということです。同じステージのジョブも指せて、そうすると同じ枠の中でも順序が生まれます。反対に、後ろのステージのジョブを指すと循環になり、GitLabが設定を拒否します。
この3つのルールを合わせると、パイプラインは有向非巡回グラフになり、実行順序はグラフのトポロジカルソートで決まります。実行計画を目で見たいなら、ウェーブで数えればよいです。待つものが1つもないジョブが最初のウェーブで一緒に出発し、それらが終わって初めて出発できるジョブが2番目のウェーブになります。ウェーブの数がそのパイプラインの最小の深さであり、ランナーをいくら増やしても減らない、時間の下限です。
実務上の制約が1つあります。1つのジョブが掛けられるneedsの数には上限があり(デフォルトは50)、そのため、ジョブ数百個のパイプラインを完全なDAGにしようという試みは、たいてい途中で止まります。そういうときは、ボトルネックになる数個だけを選んでneedsを掛け、残りはステージに任せるほうがよいです。
現場での姿
DAGに変えたチームが最初に経験する驚きは、時間が減ることではなく、依存関係が文書化されることです。needsを書くには、このジョブが何のために待っているのかに答えなければなりませんが、いざ書こうとすると、誰も理由を知らない待ち時間がいくつも出てきます。その待ち時間は、ほとんどが消せるものでした。
反対方向の事故もあります。needsで前倒ししたジョブが前のジョブのアーティファクトを使っていたのに、その依存をneedsに書かなかったため、ファイルがないと言って失敗します。この失敗は、ランナーが空いているときは再現されず、忙しいときにだけ現れることもあるので、フレーキーテストと誤解されやすいです。
続けて見ること
同じパイプラインでも、ブランチによって、またマージリクエストかどうかによって、異なるジョブが作られる必要があります。その選択を担当するrulesを、次のレッスンで見ます。