急ぎで手作業で付けた例外が、二年経ってもそのまま残っていた
目標
ポリシーの例外を、期限と担当者が付いた台帳で管理し、台帳をクラスターの実際の適用範囲に変えるスクリプトと、基準日を引数で受け取る期限切れの検査ツール、ネームスペースごとの違反の集計、そして負債が増えると失敗するゲートまで、自分で作ります。
なぜ重要なのか
ポリシーをオンにしたチームがぶつかる2つ目の壁は、ルールの質ではなく、例外の寿命です。ルールをオンにすると、今すぐには直せないものが必ず出てきて、そのときの選択肢は2つだけです。ポリシーを下げるか、例外を作るかです。ポリシーを下げると、みんなが緩んで二度と上がりません。そのため、例外を作りますが、例外に期限と担当者がないと、「次のスプリントで直します」で作った例外が2年後もそのまま残り、ルール全体を信じられなくしてしまいます。例外を作る手順よりも消す手順を、先に設計すべき理由です。その設計の核心は、台帳を原本にすることです。クラスターに手で付けた目印は、誰がなぜ付けたのか誰も知りませんが、リポジトリの台帳は、レビューを経て、期限が付き、消した記録まで残ります。クラスターはその台帳の反映にすぎず、台帳にない目印は、説明できないので消されます。そして、数字があってはじめて減らせます。例外が何件、期限切れが何件、まだ誰も知らない管理外の違反が何件という、この3つの数字を1つの画面に並べて、上限をかけておけば、負債を増やす変更が、人の記憶ではなくパイプラインで止まります。
ステップ
- すべての作業は
/root/polexcで行います(export KUBECONFIG=/root/.kube/config、kubectl config use-context kwok-lab)。まず現場を用意してください。ネームスペースpolexc-pay・polexc-legacy・polexc-sandboxを作成し、kubectl create deploymentで6つを上げます。polexc-pay/pay-api(registry.internal/pay:1.4)、polexc-pay/pay-batch(vendor.example/paybatch:latest)、polexc-legacy/billing-api(vendor.example/billing:latest)、polexc-legacy/report-gen(vendor.example/report:latest)、polexc-legacy/promo-web(vendor.example/promo:latest)、polexc-sandbox/scratch-job(vendor.example/scratch:latest)です。そのあと、/root/polexc/policy.yamlにValidatingAdmissionPolicyno-latest-tagを書いてください。apps/v1のdeploymentsのCREATE・UPDATEを捉え、Podテンプレートのコンテナイメージが:latestで終わるならブロックしますが、そのワークロードのネームスペースにpolexc.io/exempt-<워크로드이름>(プレースホルダーはワークロード名です)ラベルがあれば、通過させます(namespaceObjectを見ます)。/root/polexc/binding.yamlには、ValidatingAdmissionPolicyBindingno-latest-denyを書いてください。validationActionsは["Deny"]、matchResources.namespaceSelector.matchLabelsはpolexc.io/enforce: "on"です。両方を適用した後、polexc-payとpolexc-legacyにだけpolexc.io/enforce=onラベルを付けてください(polexc-sandboxには付けません)。では、kubectl -n polexc-pay patch deployment pay-batch --type merge -p '{"metadata":{"annotations":{"polexc.io/probe":"1"}}}' --dry-run=serverの出力を、標準エラー出力まで/root/polexc/01-blocked.txtに保存してください。最後に、バインディングだけを削除し(kubectl delete -f /root/polexc/binding.yaml)、同じリクエストをpay-batchとpolexc-legacy/promo-webに1回ずつ送って、2つの出力を/root/polexc/01-off.txtに集め、バインディングをもう一度適用しておいてください。 /root/polexc/exceptions.txtに、例外台帳を書いてください。1行が1項目で、フィールドはちょうど6つ、区切り文字は|です。namespace|workload|owner|expires|ticket|reasonの順で、expiresはYYYY-MM-DDです。1行目には、#で始まるヘッダーを置いて、人が読めるようにしてください(#で始まる行と空行は、ツールが読み飛ばします)。項目は3つです。polexc-pay|pay-batch|team-pay|2026-09-10|OPS-2201|근거、polexc-legacy|billing-api|team-billing|2026-10-20|OPS-2188|근거、polexc-legacy|report-gen|team-report|2027-03-31|OPS-2245|근거(最後のプレースホルダーは理由の文です)。理由は4文字以上で、自分で書いてください。続いて、/root/polexc/scope.txtに、このポリシーの適用範囲を1行に1つずつ書いてください。polexc-pay、polexc-legacy、polexc-sandboxの3行です。polexc-sandboxは強制ラベルがありませんが、違反を数える範囲には入ります。/root/polexc/apply-exceptions.shを書いてください。引数は2つです。<등록부> <적용범위파일>(プレースホルダーは台帳と適用範囲ファイルです)。やることは2つです。(1)台帳の各項目について、そのネームスペースにラベルpolexc.io/exempt-<workload>=<ticket>を付けて、GRANT <namespace>/<workload> <ticket>を1行出力します(台帳に書かれた順序のまま)。(2)適用範囲ファイルのネームスペースを走査して、polexc.io/exempt-で始まるラベルのうち、台帳にないものを削除し、REVOKE <namespace>/<workload>を出力します(これらの行は、<namespace>/<workload>の辞書順にまとめて、GRANTの行の後に出力します)。まず、誰かが急いで手で付けておいた例外を再現してください。kubectl label ns polexc-legacy polexc.io/exempt-promo-web=MANUAL --overwriteです。そのあと、bash /root/polexc/apply-exceptions.sh /root/polexc/exceptions.txt /root/polexc/scope.txtを実行して、出力を/root/polexc/03-apply.txtに保存してください。GRANTが3行と、REVOKE polexc-legacy/promo-webが1行、出力される必要があります。- 例外が狭いことを、自分で確認します。
polexc-legacyには今、billing-api(登録された例外がある)とpromo-web(ステップ3で目印が消された)が一緒にあります。2つのワークロードに、ステップ1と同じkubectl -n polexc-legacy patch deployment <이름> --type merge -p '{"metadata":{"annotations":{"polexc.io/probe":"4"}}}' --dry-run=server(プレースホルダーは名前です)のリクエストを順に送って、2つの出力を標準エラー出力まで/root/polexc/04-narrow.txtに集めてください。billing-apiは通過し、promo-webは、同じネームスペースなのに拒否される必要があります。 /root/polexc/expiry.shを書いてください。引数は、<등록부> <기준일 YYYY-MM-DD>(プレースホルダーは台帳と、YYYY-MM-DD形式の基準日です)の2つです。台帳に書かれた順序のまま、項目ごとに1行を出力します。<상태> <namespace>/<workload> <expires> <owner> <ticket>です(最初のプレースホルダーは状態です)。状態は3つです。期限が基準日より前ならEXPIRED、基準日から基準日+30日まで(両端を含む)ならDUE、それより後ならOKです。終了コードは、EXPIREDが1つでもあれば1、なければ0です。このコードがゲートになります。bash /root/polexc/expiry.sh /root/polexc/exceptions.txt 2026-10-01を実行して、出力を/root/polexc/05-expiry.txtに保存してください(EXPIREDが1つ、DUEが1つ、OKが1つ出ます)。- ステップ5が見つけた期限切れの項目を、実際に取り消します。まず、その項目(
polexc-pay|pay-batch|...)の行を、/root/polexc/retired.txtに追記して残し、/root/polexc/exceptions.txtからは削除してください。削除した記録が残っていてはじめて、あとで「これはなぜ消えたのか」に答えられます。そのあと、bash /root/polexc/apply-exceptions.sh /root/polexc/exceptions.txt /root/polexc/scope.txtをもう一度実行して、出力を/root/polexc/06-apply.txtに保存してください(REVOKE polexc-pay/pay-batchが出る必要があります)。最後に、ステップ4と同じリクエストをpolexc-pay/pay-batchに送って、出力を標準エラー出力まで/root/polexc/06-revoked.txtに保存してください。今度は拒否される必要があります。bash /root/polexc/expiry.sh /root/polexc/exceptions.txt 2026-10-01の終了コードも、0になっている必要があります。 /root/polexc/scan.shを書いてください。引数は、<적용범위파일>(プレースホルダーは適用範囲ファイルです)の1つです。ファイルに書かれた順序で、ネームスペースごとに1行を出力します。<namespace> <위반수> <예외수> <무단수>です(プレースホルダーは順に、ネームスペース、違反数、例外数、管理外の違反数です)。違反は、コンテナイメージが:latestで終わるDeploymentで、そのうち、そのネームスペースにpolexc.io/exempt-<이름>(プレースホルダーは名前です)ラベルがあるものが例外数、ないものが管理外の違反数です(違反がないネームスペースも、0 0 0と出力します)。bash /root/polexc/scan.sh /root/polexc/scope.txtを実行して、出力を/root/polexc/scan.txtに保存してください。polexc-pay 1 0 1、polexc-legacy 3 2 1、polexc-sandbox 1 0 1の3行が出ます。/root/polexc/debt.shを書いてください。引数は、<등록부> <적용범위파일> <기준일> <부채상한>(プレースホルダーは順に、台帳、適用範囲ファイル、基準日、負債の上限です)の4つで、同じディレクトリのexpiry.shとscan.shを呼び出して使います($(dirname "$0")で探します)。出力は、ちょうど6行です。EXCEPTIONS <등록부 항목 수>、EXPIRED <만료 수>、DUE <임박 수>、UNMANAGED <무단 위반 합계>(プレースホルダーは順に、台帳の項目数、期限切れの数、期限間近の数、管理外の違反の合計です)、DEBT <EXCEPTIONS + UNMANAGED>、そして最後にGATE OKまたはGATE FAILです。EXPIREDが1つでもあるか、DEBTが上限を超えたら、GATE FAILを出力して、終了コード1で終了します。そうでなければ、GATE OKに0です。bash /root/polexc/debt.sh /root/polexc/exceptions.txt /root/polexc/scope.txt 2026-10-01 5を実行して、出力を/root/polexc/debt.txtに保存してください(DEBT 5、GATE OK、終了コード0が出ます)。同じコマンドを、上限4でもう一度実行して、GATE FAILと終了コード1も、目で確認してください。
参考
export KUBECONFIG=/root/.kube/configとkubectl config use-context kwok-labで始めます。Pod内のkwokが起動する本物のkube-apiserver v1.30.4で、すべての成果物は/root/polexcの下に置きます。- kwokはPodを実際には実行しません。このラボが見るのは、アドミッションの判定とオブジェクトの状態だけなので、
kubectl patch ... --dry-run=serverで十分です。アドミッションはそのまま通し、オブジェクトは残りません。 - このPodでは、システムの時刻を変えられません。期限切れを扱うスクリプトは、基準日を引数で受け取るようにしてください。そうすれば、過去と未来の両方をテストでき、同じ台帳が日によって違う判定になることもありません。
- 例外の目印を、ワークロード自身のラベルに置かない理由があります。目印を外すリクエスト自体がポリシーに引っかかって拒否され、期限切れの例外を取り消せなくなるからです。ネームスペースはこのポリシーのマッチの対象ではないので、いつでも付けたり外したりできます。
- ネームスペースのラベルがアドミッションの評価に反映されるまで、1、2回の往復がかかります。結果が以前のままなら、数秒後にもう一度送ってみてください。
- よくあるミス: 例外を、ネームスペース単位で解除することです。そうすると、その中で今後新しく作られるワークロードまで、永久にルールの外に置かれます。
- よくあるミス: 集計で、例外でカバーされた違反と管理外の違反を、合算してしまうことです。合算すると、両方とも見えなくなります。
- よくあるミス: 拒否メッセージを、
2>&1なしでファイルに受け取ることです。拒否と警告は、標準エラー出力に出ます。 - Validating Admission Policy · Admission Controllers Reference · Common Expression Language in Kubernetes · Policy Reports · Labels and Selectors
ルールをオンにしたら、今すぐには直せないものがブロックされた
すべての作業は/root/polexcで行います(export KUBECONFIG=/root/.kube/config、kubectl config use-context kwok-lab)。まず現場を用意してください。ネームスペースpolexc-pay・polexc-legacy・polexc-sandboxを作成し、kubectl create deploymentで6つを上げます。polexc-pay/pay-api(registry.internal/pay:1.4)、polexc-pay/pay-batch(vendor.example/paybatch:latest)、polexc-legacy/billing-api(vendor.example/billing:latest)、polexc-legacy/report-gen(vendor.example/report:latest)、polexc-legacy/promo-web(vendor.example/promo:latest)、polexc-sandbox/scratch-job(vendor.example/scratch:latest)です。そのあと、/root/polexc/policy.yamlにValidatingAdmissionPolicy no-latest-tagを書いてください。apps/v1のdeploymentsのCREATE・UPDATEを捉え、Podテンプレートのコンテナイメージが:latestで終わるならブロックしますが、そのワークロードのネームスペースにpolexc.io/exempt-<워크로드이름>(プレースホルダーはワークロード名です)ラベルがあれば、通過させます(namespaceObjectを見ます)。/root/polexc/binding.yamlには、ValidatingAdmissionPolicyBinding no-latest-denyを書いてください。validationActionsは["Deny"]、matchResources.namespaceSelector.matchLabelsはpolexc.io/enforce: "on"です。両方を適用した後、polexc-payとpolexc-legacyにだけpolexc.io/enforce=onラベルを付けてください(polexc-sandboxには付けません)。では、kubectl -n polexc-pay patch deployment pay-batch --type merge -p '{"metadata":{"annotations":{"polexc.io/probe":"1"}}}' --dry-run=serverの出力を、標準エラー出力まで/root/polexc/01-blocked.txtに保存してください。最後に、バインディングだけを削除し(kubectl delete -f /root/polexc/binding.yaml)、同じリクエストをpay-batchとpolexc-legacy/promo-webに1回ずつ送って、2つの出力を/root/polexc/01-off.txtに集め、バインディングをもう一度適用しておいてください。
ポリシー(何を判定するか)とバインディング(どこにどの強度で)は、別々のオブジェクトです。そのため、バインディングだけを削除すると、ルールはそのままあるのに、何もブロックされません。これが「ポリシーをオフにするとみんなが緩む」ことの実体であり、例外が必要な理由です。CEL式では、リクエストのオブジェクトをobject、そのネームスペースのオブジェクトをnamespaceObjectとして読みます。ないかもしれないフィールドは、has(...)で先に確認し、文字列をつなげて作ったキーがラベルにあるかどうかは、inで見ます。--dry-run=serverは、アドミッションをそのまま通しつつ、オブジェクトは残しません。拒否メッセージは標準エラー出力に出るので、2>&1が必要です。
例外を頭の中ではなく、ファイルに書く
/root/polexc/exceptions.txtに、例外台帳を書いてください。1行が1項目で、フィールドはちょうど6つ、区切り文字は|です。namespace|workload|owner|expires|ticket|reasonの順で、expiresはYYYY-MM-DDです。1行目には、#で始まるヘッダーを置いて、人が読めるようにしてください(#で始まる行と空行は、ツールが読み飛ばします)。項目は3つです。polexc-pay|pay-batch|team-pay|2026-09-10|OPS-2201|근거、polexc-legacy|billing-api|team-billing|2026-10-20|OPS-2188|근거、polexc-legacy|report-gen|team-report|2027-03-31|OPS-2245|근거(最後のプレースホルダーは理由の文です)。理由は4文字以上で、自分で書いてください。続いて、/root/polexc/scope.txtに、このポリシーの適用範囲を1行に1つずつ書いてください。polexc-pay、polexc-legacy、polexc-sandboxの3行です。polexc-sandboxは強制ラベルがありませんが、違反を数える範囲には入ります。
例外台帳に備えるべきものは、「何を解除するか」だけではありません。担当者がいなければ尋ねる相手がおらず、期限がなければ消す根拠がなく、追跡番号がなければ、なぜ解除したかをたどれません。この4つがあってはじめて、例外がスイッチではなく、負債の一覧になります。形式は、人が読みやすく、かつawk -F'|'やwhile IFS='|' readで一度に分割できるものがよいです。適用範囲を別に書く理由は、あとでわかります。台帳にない目印を消すには、「どこまで走査するのか」が決まっている必要があります。
手で付けておいた例外が、台帳の前で消された
/root/polexc/apply-exceptions.shを書いてください。引数は2つです。<등록부> <적용범위파일>(プレースホルダーは台帳と適用範囲ファイルです)。やることは2つです。(1)台帳の各項目について、そのネームスペースにラベルpolexc.io/exempt-<workload>=<ticket>を付けて、GRANT <namespace>/<workload> <ticket>を1行出力します(台帳に書かれた順序のまま)。(2)適用範囲ファイルのネームスペースを走査して、polexc.io/exempt-で始まるラベルのうち、台帳にないものを削除し、REVOKE <namespace>/<workload>を出力します(これらの行は、<namespace>/<workload>の辞書順にまとめて、GRANTの行の後に出力します)。まず、誰かが急いで手で付けておいた例外を再現してください。kubectl label ns polexc-legacy polexc.io/exempt-promo-web=MANUAL --overwriteです。そのあと、bash /root/polexc/apply-exceptions.sh /root/polexc/exceptions.txt /root/polexc/scope.txtを実行して、出力を/root/polexc/03-apply.txtに保存してください。GRANTが3行と、REVOKE polexc-legacy/promo-webが1行、出力される必要があります。
ここで決めるのは技術ではなく、どちらが原本かです。台帳が原本なら、クラスターはその反映にすぎないため、台帳にない目印は、説明できないものなので消す必要があります。逆に、クラスターを原本にすると、誰がいつなぜ解除したのか、誰にもわからなくなります。ラベルの値に追跡番号を入れておけば、クラスターだけを見ても、根拠をたどれます。ラベルを削除するときは、キーの後ろに-を付けます。ネームスペースのラベルキーの一覧は、kubectl get ns <n> -o jsonをjqで走査すれば得られます。スクリプトを2回実行しても、結果が同じである必要があります。
例外がネームスペース全体を解除していないか確認する
例外が狭いことを、自分で確認します。polexc-legacyには今、billing-api(登録された例外がある)とpromo-web(ステップ3で目印が消された)が一緒にあります。2つのワークロードに、ステップ1と同じkubectl -n polexc-legacy patch deployment <이름> --type merge -p '{"metadata":{"annotations":{"polexc.io/probe":"4"}}}' --dry-run=server(プレースホルダーは名前です)のリクエストを順に送って、2つの出力を標準エラー出力まで/root/polexc/04-narrow.txtに集めてください。billing-apiは通過し、promo-webは、同じネームスペースなのに拒否される必要があります。
例外の範囲が広がる事故は、ほとんどが静かです。ネームスペース単位で解除した例外は、その中で今後新しく作られるワークロードまで、永久にルールの外に置きますが、エラーが何も出ないため、数か月後にようやく表に出ます。そのため、例外を作った直後に、解除されるべきでない隣のワークロードを一度突いてみることが、手順の一部である必要があります。2つの出力が1つのファイルに入る必要があるので、まとめて渡すか、2つ目を追記してください。
期限を書いておいても、誰も読まなければ、ないのと同じ
/root/polexc/expiry.shを書いてください。引数は、<등록부> <기준일 YYYY-MM-DD>(プレースホルダーは台帳と、YYYY-MM-DD形式の基準日です)の2つです。台帳に書かれた順序のまま、項目ごとに1行を出力します。<상태> <namespace>/<workload> <expires> <owner> <ticket>です(最初のプレースホルダーは状態です)。状態は3つです。期限が基準日より前ならEXPIRED、基準日から基準日+30日まで(両端を含む)ならDUE、それより後ならOKです。終了コードは、EXPIREDが1つでもあれば1、なければ0です。このコードがゲートになります。bash /root/polexc/expiry.sh /root/polexc/exceptions.txt 2026-10-01を実行して、出力を/root/polexc/05-expiry.txtに保存してください(EXPIREDが1つ、DUEが1つ、OKが1つ出ます)。
基準日を引数で受け取ることが、このステップの核心です。今日の日付をスクリプトの中で読むと、同じ台帳がある日突然違う判定になり、過去の状態をたどったり、次の四半期を先にテストしたりもできません。基準日を外から与えれば、「先月を基準にすると何件だったか」にも答えられます。日付の比較は、date -d '<날짜>' +%s(プレースホルダーは日付です)で秒に変換して、整数で比べると、桁数や月末の問題がなくなります。30日は30*86400です。このPodでは、システムの時刻を変えられません(date -sは権限がなく失敗します)。そのため、基準日の引数が、唯一のテスト方法です。
期限切れの例外を実際に取り消したら、そのワークロードが再びブロックされた
ステップ5が見つけた期限切れの項目を、実際に取り消します。まず、その項目(polexc-pay|pay-batch|...)の行を、/root/polexc/retired.txtに追記して残し、/root/polexc/exceptions.txtからは削除してください。削除した記録が残っていてはじめて、あとで「これはなぜ消えたのか」に答えられます。そのあと、bash /root/polexc/apply-exceptions.sh /root/polexc/exceptions.txt /root/polexc/scope.txtをもう一度実行して、出力を/root/polexc/06-apply.txtに保存してください(REVOKE polexc-pay/pay-batchが出る必要があります)。最後に、ステップ4と同じリクエストをpolexc-pay/pay-batchに送って、出力を標準エラー出力まで/root/polexc/06-revoked.txtに保存してください。今度は拒否される必要があります。bash /root/polexc/expiry.sh /root/polexc/exceptions.txt 2026-10-01の終了コードも、0になっている必要があります。
例外を作る手順よりも、例外を消す手順が難しく、重要です。消すほうが自動で回らなければ、例外は黙って永続化し、そのときから、そのルールは「オンになっているが、誰も信じていないルール」になります。ステップ3で台帳を原本にしておいたおかげで、ここでやることは、台帳から1行を削除して、同じスクリプトをもう一度実行することだけです。取り消しのロジックを、別に書きません。台帳をその場で修正するときは、一時ファイルに書いてから移してください。このステップを2回実行しても、retired.txtに同じ行が2回入ってはいけません。
違反を数えてみたら、例外よりも管理外のほうが多かった
/root/polexc/scan.shを書いてください。引数は、<적용범위파일>(プレースホルダーは適用範囲ファイルです)の1つです。ファイルに書かれた順序で、ネームスペースごとに1行を出力します。<namespace> <위반수> <예외수> <무단수>です(プレースホルダーは順に、ネームスペース、違反数、例外数、管理外の違反数です)。違反は、コンテナイメージが:latestで終わるDeploymentで、そのうち、そのネームスペースにpolexc.io/exempt-<이름>(プレースホルダーは名前です)ラベルがあるものが例外数、ないものが管理外の違反数です(違反がないネームスペースも、0 0 0と出力します)。bash /root/polexc/scan.sh /root/polexc/scope.txtを実行して、出力を/root/polexc/scan.txtに保存してください。polexc-pay 1 0 1、polexc-legacy 3 2 1、polexc-sandbox 1 0 1の3行が出ます。
アドミッションは、これから入ってくるものだけを見ます。すでにクラスターの中にあるものは、誰も再び検査しないため、ルールをオンにした後でも、「今いくつが違反しているか」は、別に走査しないとわかりません。そして、その数字は、必ず2つに分けてはじめて意味があります。例外でカバーされたものは期限のある負債であり、管理外の違反は、まだ誰も知らない穴です。合算してしまうと、両方とも見えなくなります。polexc-sandboxのように、強制ラベルがないネームスペースも数える必要があります。強制がオフであることは、違反がないという意味ではありません。イメージの一覧は、kubectl get deploy -o jsonをjqで走査すれば、一度に得られます。
負債を1つの画面に並べて、増えたら失敗するようにする
/root/polexc/debt.shを書いてください。引数は、<등록부> <적용범위파일> <기준일> <부채상한>(プレースホルダーは順に、台帳、適用範囲ファイル、基準日、負債の上限です)の4つで、同じディレクトリのexpiry.shとscan.shを呼び出して使います($(dirname "$0")で探します)。出力は、ちょうど6行です。EXCEPTIONS <등록부 항목 수>、EXPIRED <만료 수>、DUE <임박 수>、UNMANAGED <무단 위반 합계>(プレースホルダーは順に、台帳の項目数、期限切れの数、期限間近の数、管理外の違反の合計です)、DEBT <EXCEPTIONS + UNMANAGED>、そして最後にGATE OKまたはGATE FAILです。EXPIREDが1つでもあるか、DEBTが上限を超えたら、GATE FAILを出力して、終了コード1で終了します。そうでなければ、GATE OKに0です。bash /root/polexc/debt.sh /root/polexc/exceptions.txt /root/polexc/scope.txt 2026-10-01 5を実行して、出力を/root/polexc/debt.txtに保存してください(DEBT 5、GATE OK、終了コード0が出ます)。同じコマンドを、上限4でもう一度実行して、GATE FAILと終了コード1も、目で確認してください。
この1つの画面が、会議で答えるべき質問のすべてです。例外が何件か、今期限を過ぎているものはあるか、もうすぐ過ぎるものは何件か、そして、誰も知らない違反は何件か。数字が散らばっていると誰も見ず、1行に合算してしまうと、何を直すべきかわかりません。上限を置く理由は、「減らそう」という言葉が、ひとりでに守られることはないからです。上限があれば、負債が増える変更が、パイプラインで止まります。前のステップの2つのスクリプトを再利用することが重要です。数えるルールが2か所に書かれると、必ずどちらかが先に古くなります。expiry.shは、期限切れがあると、終了コード1で終了します。その出力を受け取って使う側が、そのコードに引っかかって死なないようにしてください。