before_script を継承したのに一行消えた
目標
include・extends・!reference・YAMLアンカー・default・spec:inputsで設定の重複を取り除き、それぞれのマージのルールがジョブに実際に何を残すかを、実行ログとマージ後の設定で確認します。
なぜ重要なのか
パイプラインファイルが大きくなると、共通部分を切り出して再利用するようになります。ところが、再利用の仕組みごとにマージのルールが違います。ハッシュはマージされ、配列は置き換わり、アンカーはファイルの中だけで、defaultは誰も指定しなかったときだけ使われます。このルールを知らないと、「引き継いだと信じていた準備コマンドが1つのジョブでだけ抜ける」という事故が、エラーなしで起きます。入力を受け取るテンプレートは、同じジョブを環境ごとにコピーしなくて済むようにしてくれますが、許容範囲を決めておかないと、綴りを間違えた環境名もそのままジョブになります。
ステップ
/root/glci-reuseをgitリポジトリにしてください。/root/glci-reuse/ci/templates.ymlに隠しジョブ.base(before_scriptはecho base-setupの1行、変数はLOG_LEVEL: info)を置き、.gitlab-ci.ymlはinclude: - local: ci/templates.ymlでそのファイルを取り込んだうえで、stages[build, test]とジョブbuild(stageはbuild、extends: .base、scriptはecho "LOG=$LOG_LEVEL")を置きます。gitlab-ci-local --shell-isolation --no-artifacts-to-sourceで実行し、buildのログにbase-setupとLOG=infoが出力されるか確認してください。- ジョブを2つ追加してください。
test(stageはtest)は.baseをextendsしながら、変数をLOG_LEVEL: debug、PYTEST: "1"と書き、scriptにecho "LOG=$LOG_LEVEL PYTEST=$PYTEST"を置きます。lint(stageはtest)は.baseをextendsしながら、自分のbefore_script(echo lint-setup)とscriptecho lintを置きます。実行して、testのログにbase-setupとLOG=debug PYTEST=1があり、lintのログにはlint-setupだけがあってbase-setupはないことを確認してください。 - ジョブ
package(stageはbuild)を追加してください。extendsは使わず、before_scriptを!reference [.base, before_script]とecho package-setupの2項目で書き、実行するとbase-setupの次にpackage-setupが出力されるようにします。scriptはecho packageです。 /root/glci-reuse/broken-anchor.ymlでci/templates.ymlをincludeし、このファイルで定義されていないアンカー*base_varsを、variablesに<<: *base_varsとして使うジョブbuildを置いてください。gitlab-ci-local --file broken-anchor.yml --listの出力を/root/glci-reuse/anchor-error.txtに保存します。そして.gitlab-ci.ymlには、同じファイルの中でアンカー&docs_vars(DOCS_OUT: public)を定義し、ジョブdocs(stageはtest)のvariablesに<<: *docs_varsとDOCS_FMT: htmlを置いて、scriptのecho "$DOCS_OUT/$DOCS_FMT"でpublic/htmlが出力されるようにしてください。.gitlab-ci.ymlのトップレベルにdefault:でbefore_scriptecho default-setupを置いてください。そしてジョブreport(stageはtest、scriptはecho report)は、inherit: default: falseでデフォルトを引き継がないようにします。実行して、docsのログにはdefault-setupが、reportのログには準備の行が1つもなく、build・testのログには依然としてbase-setupが出力されていることを確認してください。/root/glci-reuse/ci/deploy.ymlを、spec:inputsヘッダーのあるテンプレートにしてください。入力envはstaging・productionのどちらかだけを許可し、replicasは数値で、デフォルトは1です。ヘッダーの後ろ(---)には、ジョブdeploy-$[[ inputs.env ]](stageはdeploy)がecho "deploy <env> replicas=<replicas>"を実行するように書きます。.gitlab-ci.ymlはstagesにdeployを追加し、このファイルを2回includeします。stagingはデフォルトのreplicas、productionはreplicas 3です。gitlab-ci-local --previewの出力を/root/glci-reuse/expanded.ymlに保存してください。このファイルには、include・extends・!reference・アンカー・default・inputsがすべて解決された結果が入っている必要があります。採点ツールは、リポジトリのコピーで同じコマンドを実行して内容が同じかどうかと、いくつかの値を確認します。
参考
- このVMにはGitLabサーバーもランナーもなく、gitlab-ci-local 4.75.1が.gitlab-ci.ymlをGitLabと同じルールで解釈し、シェルでジョブを実行します。
image:を書くとDockerで動かそうとするので使いません。保護変数・マスキング・CI_JOB_TOKEN・ランナーのタグ・マージリクエストパイプラインの作成はサーバーの機能なので、ここでは再現されません。 - 実行はリポジトリのルートで
gitlab-ci-local --shell-isolation --no-artifacts-to-source(ジョブごとに別の作業ディレクトリを使い、成果物をリポジトリに書き戻さない)、ジョブ一覧はgitlab-ci-local --list-csv-all、解釈された設定はgitlab-ci-local --previewです。gitlab-ci-localはgitが追跡しているファイルだけをジョブに渡すので、ファイルを作ったらgit addしてください。採点ツールはリポジトリをコピーし、すべてのファイルをコミットしたあとで、同じツールでもう一度実行します。 - ジョブのログの見方は、実行出力の
잡이름 > ...の行、または.gitlab-ci-local/output/<잡이름>.logです(プレースホルダーはジョブ名です)。 - よくあるミスは、断片からbefore_scriptを受け取ると信じたままジョブにもbefore_scriptを書いてしまい、断片のコマンドが消えることです。
- CI/CD YAML syntax reference・include・Optimize GitLab CI/CD configuration files(アンカー・extends・!reference)・gitlab-ci-local
テンプレートファイルをincludeしてextendsで引き継ぐ
/root/glci-reuseをgitリポジトリにしてください。/root/glci-reuse/ci/templates.ymlに隠しジョブ.base(before_scriptはecho base-setupの1行、変数はLOG_LEVEL: info)を置き、.gitlab-ci.ymlはinclude: - local: ci/templates.ymlでそのファイルを取り込んだうえで、stages[build, test]とジョブbuild(stageはbuild、extends: .base、scriptはecho "LOG=$LOG_LEVEL")を置きます。gitlab-ci-local --shell-isolation --no-artifacts-to-sourceで実行し、buildのログにbase-setupとLOG=infoが出力されるか確認してください。
includeは、複数のファイルを1つの設定にまとめてから解釈します。名前がドットで始まるジョブは一覧に載らず、引き継がれる断片としてだけ使われます。gitlab-ci-localはgitが追跡しているファイルだけを見るので、新しいファイルはgit addします。
ハッシュはマージされ、配列は丸ごと置き換わる
ジョブを2つ追加してください。test(stageはtest)は.baseをextendsしながら、変数をLOG_LEVEL: debug、PYTEST: "1"と書き、scriptにecho "LOG=$LOG_LEVEL PYTEST=$PYTEST"を置きます。lint(stageはtest)は.baseをextendsしながら、自分のbefore_script(echo lint-setup)とscriptecho lintを置きます。実行して、testのログにbase-setupとLOG=debug PYTEST=1があり、lintのログにはlint-setupだけがあってbase-setupはないことを確認してください。
extendsは深いマージです。variablesのようなハッシュはキー単位でマージされ、引き継いだキーの上に上書きされますが、before_scriptのような配列はマージされず、ジョブが書いたもので丸ごと置き換わります。
配列をマージしたいときは!reference
ジョブpackage(stageはbuild)を追加してください。extendsは使わず、before_scriptを!reference [.base, before_script]とecho package-setupの2項目で書き、実行するとbase-setupの次にpackage-setupが出力されるようにします。scriptはecho packageです。
!referenceは、ほかのジョブ(隠しジョブを含む)の特定のキーの値を、その場に差し込むタグです。includeしたファイルの断片も指せるので、配列が丸ごと置き換わるextendsの限界を補います。
アンカーはファイルの境界を越えられない
/root/glci-reuse/broken-anchor.ymlでci/templates.ymlをincludeし、このファイルで定義されていないアンカー*base_varsを、variablesに<<: *base_varsとして使うジョブbuildを置いてください。gitlab-ci-local --file broken-anchor.yml --listの出力を/root/glci-reuse/anchor-error.txtに保存します。そして.gitlab-ci.ymlには、同じファイルの中でアンカー&docs_vars(DOCS_OUT: public)を定義し、ジョブdocs(stageはtest)のvariablesに<<: *docs_varsとDOCS_FMT: htmlを置いて、scriptのecho "$DOCS_OUT/$DOCS_FMT"でpublic/htmlが出力されるようにしてください。
アンカーとエイリアスは、YAMLパーサーがファイル1つを読むときに処理します。includeはそのあとにGitLabがマージする段階なので、別のファイルのアンカーはすでに消えたあとです。ファイルをまたぐ再利用は、extendsか!referenceで行います。
defaultは下地の値であり、extendsとinheritに押される
.gitlab-ci.ymlのトップレベルにdefault:でbefore_scriptecho default-setupを置いてください。そしてジョブreport(stageはtest、scriptはecho report)は、inherit: default: falseでデフォルトを引き継がないようにします。実行して、docsのログにはdefault-setupが、reportのログには準備の行が1つもなく、build・testのログには依然としてbase-setupが出力されていることを確認してください。
defaultは、何も指定していないジョブに埋められるグローバルな下地の値なので、extendsでbefore_scriptをすでに受け取ったジョブでは使われません。inheritは、デフォルトを受け取るかどうかをジョブごとに切るスイッチです。
入力を受け取るテンプレートで環境別のジョブを生み出す
/root/glci-reuse/ci/deploy.ymlを、spec:inputsヘッダーのあるテンプレートにしてください。入力envはstaging・productionのどちらかだけを許可し、replicasは数値で、デフォルトは1です。ヘッダーの後ろ(---)には、ジョブdeploy-$[[ inputs.env ]](stageはdeploy)がecho "deploy <env> replicas=<replicas>"を実行するように書きます。.gitlab-ci.ymlはstagesにdeployを追加し、このファイルを2回includeします。stagingはデフォルトのreplicas、productionはreplicas 3です。
specヘッダーは、includeするときに渡せる値の形と許容範囲を決めます。テンプレートの中では$[[ inputs.이름 ]]で値を使います(プレースホルダーは入力の名前です)。許可リストにない値を渡すと、パイプラインが作られる前に拒否されます。
GitLabが見ることになるマージ後の設定を取り出しておく
gitlab-ci-local --previewの出力を/root/glci-reuse/expanded.ymlに保存してください。このファイルには、include・extends・!reference・アンカー・default・inputsがすべて解決された結果が入っている必要があります。採点ツールは、リポジトリのコピーで同じコマンドを実行して内容が同じかどうかと、いくつかの値を確認します。
設定が複数のファイルに散らばるほど、「このジョブが実際に何を実行するのか」をファイルだけを見て答えるのは難しくなります。マージされた結果をレビューに貼っておけば、マージのルールを頭の中で計算しなくて済みます。GitLab画面のパイプラインエディターにも、同じ機能(全体の設定を見る)があります。