TT Lab
はじめる
学ぶ 学習パス コース

GitLab CI/CD

設定でパイプラインの実行モデルを作る

TT Labで続きを見る

目標

.gitlab-ci.yml1枚を8つのステップにわたって積み上げながら、GitLab CIの実行モデル(ステージの順序、needsが作るDAG、rulesが分けるジョブ生成の有無、artifactsとcacheの違い)を、自分の手で作ります。最後に、そのモデルを計算する解釈器を自分で組みます。

なぜ重要なのか

このPodにはGitLabサーバーもGitLab Runnerもありません。そのため、このラボはパイプラインが実際に動くふりをしません。代わりに、ランナーがなくても誠実に学べるものだけを扱います。設定言語とその実行モデルです。実務でパイプラインのせいで行き詰まる瞬間は、ほとんどの場合、コマンドを知らないからではなく、なぜこのジョブが作られなかったのか、なぜあのジョブがまだ待っているのかを説明できないからです。その答えはすべて、このファイルの構造から出てきます。採点も、ファイルを目で流し読みするのではなく、YAMLパーサーで読んで構造を確認します。インデントが1つずれてジョブが見当違いのキーの下にぶら下がった設定は、人の目には問題なく見えるのに、実際には動かないからです。

ステップ

  1. /root/glci/.gitlab-ci.ymlを作成してください。トップレベルのstagesにbuild、test、deployをこの順序で置き、build-appジョブをstage: buildにして、scriptを1つ以上書きます。
  2. ジョブを追加して、3つのステージをすべて埋めてください。lintとunit-testはstage: test、deploy-stagingはstage: deployです。すべてのジョブにscriptが必要で、すべてのstageの値はstagesのリストの中にある必要があります。
  3. .python-baseという隠しジョブを作ってimageとbefore_scriptを入れ、build-app・lint・unit-testの3つのジョブがextends: .python-baseで引き継ぐようにしてください。引き継いだジョブではimageを書き直しません。トップレベルのdefaultにデフォルトのimageを置きます。
  4. needsを付けてDAGを作ってください。lintはneeds: []、unit-testはneedsにbuild-app、deploy-stagingはneedsにunit-testを書きます。build-appにはneedsを置きません。
  5. deploy-stagingにrulesを付けて、$CI_COMMIT_BRANCH == "main"のときwhen: on_successとし、最後の項目は条件のないwhen: neverで閉じてください。deploy-prodジョブをstage: deployで新しく作ってneedsにdeploy-stagingを書き、同じブランチ条件でwhen: manual・allow_failure: falseとし、やはり条件のないwhen: neverで閉じます。onlyとexceptは使いません。
  6. build-appにartifactsを付けて、pathsとexpire_inを書いてください。unit-testのneedsを長い形式に変えてjob: build-app・artifacts: trueとし、deploy-stagingのneedsはartifacts: falseにします。
  7. /root/glci/requirements.txtを中身のあるファイルとして作ってください。build-appとunit-testにcacheを付け、keyはfilesでrequirements.txtを指すようにします。build-appはpolicy: pull-push、unit-testはpolicy: pullです。cache.pathsはartifacts.pathsと重ならないようにします。
  8. /root/glci/plan.pyを作成してください。引数で受け取った設定ファイルを読んで、{"stages": [...], "waves": [[...], ...]}を標準出力にJSONで出力します。needsがあればその一覧だけを、なければ前のステージのすべてのジョブを待ちます(デフォルトのstageはtestです)。ドットで始まるキーと予約キー(stages・variables・default・include・workflow)は、ジョブではありません。各ウェーブは名前順に並べます。どのジョブも出発できなければ、cycleという語を出力して、0以外の終了コードで終わらせます。最後に、自分の設定に対して実行した出力を/root/glci/plan.jsonに保存します。

参考

ステージの順序と最初のジョブ

/root/glci/.gitlab-ci.ymlを作成してください。トップレベルのstagesにbuild、test、deployをこの順序で置き、build-appジョブをstage: buildにして、scriptを1つ以上書きます。

/root/glci/.gitlab-ci.ymlを作り、トップレベルにstagesのリストを置きます。このリストの順序がそのままデフォルトの実行順序です。その下にbuild-appというトップレベルのキーを置き、stageとscriptをその中にインデントして書きます。採点はgrepではなくYAMLパーサーで読むので、インデントが1つずれただけでも、ジョブが別のキーの下位項目になって失敗します。

3つのステージをすべて埋める

ジョブを追加して、3つのステージをすべて埋めてください。lintとunit-testはstage: test、deploy-stagingはstage: deployです。すべてのジョブにscriptが必要で、すべてのstageの値はstagesのリストの中にある必要があります。

lintとunit-testをtestステージに、deploy-stagingをdeployステージに追加します。同じステージにジョブが2つあれば、その2つは互いに並列で動きます。stagesのリストにない名前をstageに書くと、GitLabはその設定をまるごと拒否するので、綴りを確認してください。すべてのジョブにはscriptが必要です。

隠しジョブとextendsで重複を取り除く

.python-baseという隠しジョブを作ってimageとbefore_scriptを入れ、build-app・lint・unit-testの3つのジョブがextends: .python-baseで引き継ぐようにしてください。引き継いだジョブではimageを書き直しません。トップレベルのdefaultにデフォルトのimageを置きます。

名前がドットで始まるキーは、実行されない断片です。.python-baseにimageとbefore_scriptを入れ、build-app・lint・unit-testの3つのジョブがextendsで引き継ぐようにしてください。引き継いだなら、ジョブでimageを書き直しません。断片を使わないジョブのために、トップレベルのdefaultにデフォルトのイメージを置きます。

needsでステージの壁を越える

needsを付けてDAGを作ってください。lintはneeds: []、unit-testはneedsにbuild-app、deploy-stagingはneedsにunit-testを書きます。build-appにはneedsを置きません。

unit-testはneedsでbuild-appだけを待たせ、deploy-stagingはunit-testだけを待たせます。lintにはneeds: []を与えます。キーがないことと空のリストは正反対の意味なので、空のリストであって初めて、前のステージを1つも待たずに一番前で出発します。build-appにはneedsを置きません。

rulesでブランチ条件と手動承認を掛ける

deploy-stagingにrulesを付けて、$CI_COMMIT_BRANCH == "main"のときwhen: on_successとし、最後の項目は条件のないwhen: neverで閉じてください。deploy-prodジョブをstage: deployで新しく作ってneedsにdeploy-stagingを書き、同じブランチ条件でwhen: manual・allow_failure: falseとし、やはり条件のないwhen: neverで閉じます。onlyとexceptは使いません。

deploy-stagingは$CI_COMMIT_BRANCH == "main"のときwhen: on_success、deploy-prodは同じ条件でwhen: manualにします。2つのジョブとも、最後の項目は条件のないwhen: neverでなければなりません。rulesは最初に引っかかった1つだけを適用するので、条件のない項目を途中に置くと、その下がすべて死にます。only/exceptは使いません。deploy-prodはdeployステージで、deploy-stagingを待ちます。

artifactsと受け取る側を決める

build-appにartifactsを付けて、pathsとexpire_inを書いてください。unit-testのneedsを長い形式に変えてjob: build-app・artifacts: trueとし、deploy-stagingのneedsはartifacts: falseにします。

build-appにartifactsを付け、pathsで渡すディレクトリを書き、expire_inも一緒に書きます。そしてneedsを長い形式(- job: 이름とartifacts: true|false。プレースホルダーはジョブ名です)に変え、アーティファクトを実際に使うunit-testだけをtrueで受け取り、順序だけ待つdeploy-stagingはfalseで切ります。使わないジョブにまでダウンロードさせると、パイプラインが静かに遅くなります。

cacheのキーをロックファイルのハッシュにする

/root/glci/requirements.txtを中身のあるファイルとして作ってください。build-appとunit-testにcacheを付け、keyはfilesでrequirements.txtを指すようにします。build-appはpolicy: pull-push、unit-testはpolicy: pullです。cache.pathsはartifacts.pathsと重ならないようにします。

まず/root/glci/requirements.txtを作ります。キャッシュのキーがハッシュする実際のファイルが必要です。そのあとbuild-appとunit-testにcacheを付け、keyは固定文字列ではなくfilesの形でそのファイルを指すようにしてください。キャッシュを作るbuild-appはpolicy: pull-push、読むだけのunit-testはpolicy: pullです。cache.pathsはartifacts.pathsと重なってはいけません。

パイプラインの解釈器で実行順序を計算する

/root/glci/plan.pyを作成してください。引数で受け取った設定ファイルを読んで、{"stages": [...], "waves": [[...], ...]}を標準出力にJSONで出力します。needsがあればその一覧だけを、なければ前のステージのすべてのジョブを待ちます(デフォルトのstageはtestです)。ドットで始まるキーと予約キー(stages・variables・default・include・workflow)は、ジョブではありません。各ウェーブは名前順に並べます。どのジョブも出発できなければ、cycleという語を出力して、0以外の終了コードで終わらせます。最後に、自分の設定に対して実行した出力を/root/glci/plan.jsonに保存します。

/root/glci/plan.py <설정파일>で呼び出したとき、標準出力に{"stages": [...], "waves": [[...], ...]}を出す必要があります(プレースホルダーは設定ファイルです)。ウェーブの計算ルールは3つです。needsがあればその一覧だけを、なければ前のステージのすべてのジョブを待ちます(デフォルトのstageはtest)。そして、ドットで始まるキーと予約キーはジョブではありません。各ウェーブは名前順に並べます。誰も出発できない状態になったら循環なので、cycleを出力して、0以外のコードで終了してください。診断は標準エラー出力に送らないと、標準出力がJSONのまま残りません。最後に、自分の設定に対して実行した結果を/root/glci/plan.jsonとして保存します。