シェルで作る3段階パイプライン
このラボは本物のVM上で動きます
この箱はPodではなく、KubeVirtが起動した仮想マシンです。Linuxカーネルが別に動き、systemdが実際にサービスを管理し、dockerは模倣ではなく本物のDockerエンジンです。docker runで起動したコンテナは実際にプロセスになり、docker execもdocker logsもそのまま動作します。
以前はこのラボがPodの中で動いていました。カーネル権限をすべて手放した箱だったので、コンテナを起動するステップが塞がれており、イメージのアーカイブを自分で展開してみる回り道で学んでいました。もう回り道は必要ありません。
知っておくことが2つあります。
- 最初に起動するまで1分ほどかかります。VMが起動し、Dockerをインストールするためです。Podのラボ(通常40秒)より遅くなります。
- ブラウザープレビューがありません。VMへの接続は採点用のポート1つだけが開いています。Webサーバーを起動したら、VMの中で
curlを使って確認してください。
目標
シェルスクリプトだけでbuild → test → packageの3ステップのパイプラインを作り、ソースのハッシュで名前を付けた不変のアーティファクトとイメージタグまで自分の手で作ります。
なぜ重要なのか
CIツールは数年ごとに入れ替わりますが、原理は変わりません。ステップには順序があり、前のステップが失敗したら後のステップは実行されてはならず、アーティファクトは何から作ったのかを名前だけで判断できなければなりません。タグは人が付け替えられるラベルなので、tj-actions/changed-filesの事件のようにまるごと付け替えられることがあります。そのため、デプロイの識別子はコミットSHAのような変更できない値でなければなりません。本番環境にlatestしかないと、何がデプロイされたのか追跡できず、戻す対象もありません。このラボは、その原理をベンダーのUIなしで手作業で再現します。
ステップ
/root/ci1/pipeline.shを作り、chmod +xで実行権限を付けてください。スクリプトの冒頭にset -euo pipefailを入れます(採点ではset -e系、set -u、pipefailをそれぞれ確認します)。ビルド対象として/root/ci1/src/ディレクトリを作り、ファイルを2つ以上入れてください。- パイプラインは各ステップの開始時に
::stage build、::stage test、::stage packageをこの順に1回ずつ出力し、終わったらSUCCESSを出力してください。成功した実行の出力を/root/ci1/run1.logに保存してください。 - packageステップは
/root/ci1/out/app-<해시>.tar.gzをちょうど1つ作ってください。<해시>はcat /root/ci1/src/* | sha256sum | cut -c1-12で計算した12文字です。ファイルはtar tzfで開ける本物のgzip tarでなければなりません。 - 同じソースでもう一度実行したら、すでにあるアーティファクトを再利用してください。このとき
::artifact-existsを出力し、その実行ログを/root/ci1/run2.logに保存します。/root/ci1/outのapp-*.tar.gzは、やはり1つのままでなければなりません。 /root/ci1/out/build-info.jsonを作ってください。フィールドはsource_hash(上の12文字のハッシュ)、status(文字列success)、stages(数値3)、created_at(時刻の文字列)です。/root/ci1/tests/fail-flagファイルを作ってtestステップが失敗するようにし、パイプラインをもう一度実行してください。出力は/root/ci1/fail.log、終了コードは/root/ci1/fail-exit.txtに保存します(0以外でなければなりません)。fail.logには::stage testはあり、::stage packageはあってはいけません。pipeline.shには|| trueを使わないでください。確認が終わったら/root/ci1/tests/fail-flagを削除して元に戻してください。- アーティファクトを入れたイメージを
labhub/ci:<해시12>タグ(プレースホルダーは12文字のハッシュです)でビルドしてください。ベースイメージは、Podにあらかじめ用意されているalpine:3.20、busybox:1.36、python:3.12-alpine、nginx:1.27-alpineの中からだけ選んでください。 - 同じイメージに
docker tagでlabhub/ci:latestを追加で付けてください。そしてbuild-info.jsonにtags配列を入れ、labhub/ci:<해시12>とlabhub/ci:latestの2つの値をどちらも記録してください(2つ以上)。
参考
- ハッシュは必ず採点と同じ方法で計算してください:
H=$(cat /root/ci1/src/* | sha256sum | cut -c1-12)。グロブの順序が変わると値が変わります。 - ステップ3以降に
/root/ci1/srcの内容を変えると、ハッシュが変わってアーティファクトが2つになります。ソースを直したら、古いアーティファクトを削除して作り直してください。 - JSONは、文字列を手で連結するより、
jq -n --arg h "$H" '{source_hash:$h, status:"success", stages:3}'のように作るほうが安全です。 - よくあるミスは、
|| trueで失敗を握りつぶしてしまうこと(ゲートではなくなります)、fail-flagを削除せずに終えてしまうこと(以降の採点が失敗し続けます)、latestを別のビルドで作ってイメージIDが変わってしまうことです。
パイプラインの骨格と安全オプション
/root/ci1/pipeline.shを作り、chmod +xで実行権限を付けてください。スクリプトの冒頭にset -euo pipefailを入れてください(採点ではset -e系、set -u、pipefailをそれぞれ確認します)。ビルド対象として/root/ci1/src/ディレクトリを作り、ファイルを2つ以上入れてください。
/root/ci1/pipeline.shを作ったら、chmod +xを忘れないでください。冒頭にset -euo pipefailを入れます。採点では-e系、-u、pipefailの3つをそれぞれ探します。失敗したステップで止まらないなら、それはゲートではなくログ生成器です。ビルド対象は/root/ci1/src/の下にファイルを2つ以上です。
build → test → packageの順序
パイプラインは各ステップの開始時に::stage build、::stage test、::stage packageをこの順に1回ずつ出力し、終わったらSUCCESSを出力してください。成功した実行の出力を/root/ci1/run1.logに保存してください。
各ステップの開始時に::stage build、::stage test、::stage packageを1回ずつだけ出力し、最後にSUCCESSを出力します。成功した実行の出力を/root/ci1/run1.logに保存してください。マーカーを何度も出力すると、順序の検査が壊れます。
ソースのハッシュで名前を付ける
packageステップは/root/ci1/out/app-<해시>.tar.gzをちょうど1つ作ってください。<해시>はcat /root/ci1/src/* | sha256sum | cut -c1-12で計算した12文字です。ファイルはtar tzfで開ける本物のgzip tarでなければなりません。
ハッシュは採点とまったく同じ方法で計算する必要があります: cat /root/ci1/src/* | sha256sum | cut -c1-12。結果は/root/ci1/out/app-<해시>.tar.gz(プレースホルダーはハッシュです)の1つだけで、tar tzfで開けなければなりません。名前に作った材料が書かれていれば、あとでたどり直せます。
同じ入力なら作り直さない
同じソースでもう一度実行したら、すでにあるアーティファクトを再利用してください。このとき::artifact-existsを出力し、その実行ログを/root/ci1/run2.logに保存してください。/root/ci1/outのapp-*.tar.gzは、やはり1つのままでなければなりません。
packageステップで目的のファイルがすでにあれば、::artifact-existsを出力してスキップします。2回目の実行ログは/root/ci1/run2.logです。アーティファクトの数が増えたら、それは同じ入力から別の出力が出たという意味です。
ビルドのメタデータを残す
/root/ci1/out/build-info.jsonを作ってください。フィールドはsource_hash(上の12文字のハッシュ)、status(文字列success)、stages(数値3)、created_at(時刻の文字列)です。
/root/ci1/out/build-info.jsonにsource_hash、status、stages、created_atを入れます。statusは文字列のsuccess、stagesは数値の3です。引用符の事故を減らすには、jq -n --argで作ってください。
失敗を握りつぶさず止める
/root/ci1/tests/fail-flagファイルを作ってtestステップが失敗するようにし、パイプラインをもう一度実行してください。出力は/root/ci1/fail.log、終了コードは/root/ci1/fail-exit.txtに保存してください(0以外でなければなりません)。fail.logには::stage testはあり、::stage packageはあってはいけません。pipeline.shには|| trueを使わないでください。確認が終わったら/root/ci1/tests/fail-flagを削除して元に戻してください。
/root/ci1/tests/fail-flagを作ってtestを失敗させ、出力は/root/ci1/fail.log、終了コードは/root/ci1/fail-exit.txtに残します。packageステップは実行されてはいけません。pipeline.shに|| trueがあってはいけません。確認が終わったら、fail-flagを必ず削除して元に戻してください。
コミットごとに変わるタグでイメージをビルドする
アーティファクトを入れたイメージをlabhub/ci:<해시12>タグ(プレースホルダーは12文字のハッシュです)でビルドしてください。ベースイメージは、Podにあらかじめ用意されているalpine:3.20、busybox:1.36、python:3.12-alpine、nginx:1.27-alpineの中からだけ選んでください。
labhub/ci:<해시12>(プレースホルダーは12文字のハッシュです)でビルドします。オフラインなのでpullできません。ベースはあらかじめあるalpine:3.20、busybox:1.36、python:3.12-alpine、nginx:1.27-alpineの中から選んでください。podmanはlocalhost/という接頭辞を付けて表示しますが、採点はそれを取り除いて比較します。
latestはエイリアス、ハッシュは識別子
同じイメージにdocker tagでlabhub/ci:latestを追加で付けてください。そしてbuild-info.jsonにtags配列を入れ、labhub/ci:<해시12>とlabhub/ci:latestの2つの値をどちらも記録してください(2つ以上)。
docker tagで同じイメージに名前をもう1つ付けます。再ビルドしてlatestを作るとイメージIDが変わって失敗します。build-info.jsonのtags配列に、2つのタグをどちらも記録してください。