インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

K8s開発者プレイブック|自力解決の境界

公開

第17回で、リージョンが失われたときに使う数値と、その数値を満たす復旧の経路を決めました。これで、本連載が扱う設計書は17冊そろいました。

ただし、この17冊には共通点があります。読む相手が、すべて運用チームだということです。設計書は決定を記録する文書で、決定した本人たちが読むことを前提にしています。一方、その決定に従って毎日デプロイするのは開発チームです。決定が開発チームの手順に現れていなければ、その決定は運用されていません。

目次
  1. 17回ぶんの設計を、使う人が読む1つの文書に変換する
    1. 呼び出しの総量を下げる、3つ目の経路
    2. プラットフォームを作った直後は、速度も安定性も下がる
  2. オンボーディングの完了は、権限を渡した時点ではない
    1. 時計は3つあり、起点が違う
    2. 完了条件は観測値で書く
    3. 測る値は3つに絞る
  3. 払い出しは「何を渡すか」ではなく「何を渡さないか」で決まる
    1. 1人の開発者が動き出すのに要る5点セット
    2. Namespaceは「作る」のではなく「揃える」
    3. 申請に残すのは2種類だけ
    4. 渡さないものを、渡さない理由とセットで書く
  4. 初回デプロイのウォークスルー ― 最初の1日に踏む罠
    1. ウォークスルーは、1つのサンプルサービスで書き切る
    2. 手元の道具が古い
    3. requestsを書かないと、そもそも作成が拒否される
    4. 書いた値と、動いている値が違う
    5. 同期されるまで、書いたものは動いていない
  5. セルフサービスの範囲は、第5回で閉じた権限の裏返し
    1. 開発者が本番で見られるものの一覧
    2. 権限を閉じたなら、閉じたうえで自力解決させる設計義務が生じる
    3. 環境ごとに範囲を変える
  6. 症状別の手引きに書くのは、原因ではなく境界
    1. 表の列は5つ
    2. CrashLoopBackOff ― 終了コードから始める
    3. OOMKilled ― 宛先が分岐する症状
    4. ImagePullBackOff ― 境界が最もはっきりしている症状
    5. Pending ― 2つを分ける
    6. 起動はするが、応答しない
    7. 手引きに書いてはいけない3つ
  7. 問い合わせと機能要望は、同じ窓口で受けて別々に処理する
    1. 受け口は1本、分類は3つ
    2. 要望は、第10回の3つの問いに変換して受ける
    3. FAQは書くものではなく、溜まったものから作る
    4. 計画作業の依頼文を、毎回ゼロから書かない
  8. 【実務テンプレート】開発者オンボーディング・プレイブック
  9. 全18回の地図 ― どこに戻ればよいか
    1. 判断に迷ったとき、どの回に戻るか
    2. 連載を通した3つの同型

17回ぶんの設計を、使う人が読む1つの文書に変換する

本記事は、次の3つの立場を採ります。

  1. オンボーディングの完了は、権限を渡した時点ではない。完了条件は観測値で書きます。渡したかどうかは、渡した側にしか見えない事実です
  2. プレイブックに新しい判断は書かない。過去17回の決定を、開発者の手順に転記します。ここで新しく決め始めたら、それは過去回のどこかが未決だという合図です
  3. トラブルシューティングの手引きに書くのは、原因ではなく境界。症状ごとに、どこまでが自分で直せる範囲かを書きます

先に線を引いておきます。第10回で、インターフェースの設計は設計書、その使い方のガイドはプレイブックだと書きました設計書を書く相手は運用チーム、ガイドを書く相手は開発チームです。したがって本記事は、開発者向けの入門記事ではありません。開発者に渡す文書を、プラットフォームエンジニアがどう設計するかの記事です。

扱わないものも明示します。Kubernetesの技術教育は扱いません。本記事が前提にするのは、PodやDeploymentの概念を既に知っている開発者が、この組織の基盤を使い始めるときに必要な手順です。プラットフォームチームの内部運用プロセスも扱いません。オンコールの組み方やポストモーテムの実施ルールは第16回の管轄で、本記事が書くのは開発者から見た窓口の側だけです。

呼び出しの総量を下げる、3つ目の経路

第16回で、呼び出しの総量そのものを下げる経路は3つあると書きましたバグを減らす(第9回のCIとテスト)。アラートを減らす(第15回)。開発チームが自力で解決できる範囲を広げる。3つ目が本記事です。第15回でも第16回でも、運用チームだけでは呼び出し量を下げられないと2度書きました。その最後の1本を、ここで実装します。

目標値は第16回で決まっています。1日のチケットが5件未満、1シフトあたりのページが2件未満。本記事の成果は、この数値への寄与で測ります。

プラットフォームを作った直後は、速度も安定性も下がる

内部開発者プラットフォームは、もう例外的な取り組みではありません。DORAの調査では、組織の90%が内部開発者プラットフォームの利用を報告し、76%が専任のプラットフォームチームを置いています。

同じ調査が、もう1つのことを報告しています。内部開発者プラットフォームの利用は、個人の生産性・チームのパフォーマンス・組織全体のパフォーマンスを改善する一方で、変更の安定性とスループットを下げうる。理由は単純で、プラットフォームは層を1つ増やすからです。層が増えれば、引き継ぎと依存も増えます。

ここから本記事の立場が決まります。オンボーディングは、この谷を浅くする作業です。谷をなくす作業ではありません。そしてDORAが生産性の改善に結びつけている因子は開発者の独立性、つまり支援チームに頼らずにタスクを完了できることで、チームと個人の両レベルで5%の生産性改善が観測されています。独立性は、渡した権限の量ではなく、自力で完了できた作業の割合です。この定義が、そのまま次章の完了条件になります。

第1回で、Kubernetesの運用コストを上回る投資対効果がなければ採用しないという条件を置きました。層を1つ増やすという判断も、同じ計算の中にあります。

オンボーディングの完了は、権限を渡した時点ではない

本記事で最初に決める設計判断です。完了条件を「権限を渡したこと」に置くと、渡した後の詰まりが誰の問題でもなくなります。運用チームは払い出しを終えており、開発チームはまだ動けていない。この状態に責任者がいません。

時計は3つあり、起点が違う

第17回で、RTOとRPOを設計するときは時計の起点を先に定義すると決めました。同じ規律をここでも使います。

時計起点終点既にある約束
払い出しの時計依頼を受け取った時刻Namespaceと権限が使える状態になった時刻第2回のSLA:2営業日
オンボーディングの時計開発者にアカウントと文書を渡した時刻その開発者自身の操作で、初回デプロイが成功した時刻なし(本記事で決める
問い合わせの時計問い合わせを受け取った時刻一次応答第2回のSLA:4時間

1つ目と2つ目は別の時計です。2営業日で払い出しても、開発者が2週間デプロイできないなら、第1回で掲げたビジネス目標は達成されていません。第1回では、新環境の払い出しに2週間かかっている状態を短縮し、待機コストを削減するという目標の書き方を示しました。その目標を検証できるのは、2つ目の時計だけです。1つ目は運用チームの作業時間しか測っていません。

払い出し・オンボーディング・問い合わせの3つの時計を縦に3段で並べ、それぞれの起点と終点が異なることを示した図。払い出しの時計が止まった後もオンボーディングの時計は動き続けることを赤い破線で強調している
払い出しの時計が止まっても、開発者の時計は止まっていません。3つを同じ「リードタイム」という語でまとめると、どれも改善されなくなります。

完了条件は観測値で書く

本連載で4回目の同型です。第7回でバックアップは復元できて初めて完了と書き第8回でIaCの完了を差分が空になることと定め第13回で移行の完了を切り戻し期限の経過に置き、第14回で更新の完了を観測期間の終了としました。オンボーディングも同じ形にします。

完了条件:新規開発者が、運用チームの操作を1度も挟まずに、本番相当環境への初回デプロイを成功させたこと。

この定義には理由が3つあります。

  1. 「1度も挟まずに」が独立性の定義と一致します。途中で運用チームが1回でも代行したなら、その手順は開発者が実行できない手順です。代行は親切ですが、記録には残りません
  2. 終点は本番ではなくStgに置きます。本番は第9回で設計したPromotionと承認の対象なので、完了条件に含めると承認待ちの時間が混ざり、測っている対象が「承認の速さ」に変わります
  3. 成功の判定を人が行いません。同期が完了し、Podが Ready になり、ヘルスチェックが通った状態を機械が判定します。人が「たぶん動いています」と言う状態を完了にしません

測る値は3つに絞る

多く測ると、測らなくなります。3つです。

測る値定義使い道
初回デプロイまでの営業日数アカウント発行から、Stgへの初回デプロイ成功まで第10回の実務テンプレートに空欄として残した「実績◯営業日」を埋める値。IDP導入の再検討トリガーの入力にもなります
初回デプロイまでの問い合わせ件数同じ期間に受けた質問の数手引きの穴の位置を示します。件数そのものより、後述する7段階のどこで詰まったかの分布を見ます
自力解決率参加から最初の四半期見直しまでの間に、開発者が運用チームを呼ばずに閉じた事象の割合手引きの効果測定。第16回の「1日のチケット5件未満」に効く唯一の指標です

3つ目の観測期間を、30日のような独自の値ではなく第2回で定めた四半期の見直しに揃えているのは、新しい周期を増やさないためです。第5回で例外の有効期限を四半期の見直しに合わせたのと同じ理由で、見直しの場がない周期で測った数値は、誰も見ないまま溜まります。

そして、目標値を先に書かないでください。「3営業日以内」のような数値に根拠はありません。最初の数人ぶんは測るだけにして、目標を置くのは中央値が動かなくなってからにします。第17回で、訓練でしか埋まらない工程は「未計測」と書いて提出してよいと決めたのと同じ扱いです。数値ではなく、数値の決め方を渡します。

払い出しは「何を渡すか」ではなく「何を渡さないか」で決まる

第10回と同型の章です。本記事に新しい判断はありません。過去回の決定を、払い出しの手順に転記します。第10回で「線が引けないと感じるなら、それは過去回のどれかが未決だ」と書いたのと同じ診断が、ここでも使えます。

1人の開発者が動き出すのに要る5点セット

Namespaceを作るだけでは動きません。5つ揃って、開発者はようやく最初のコマンドを打てます。

渡すもの中身決めた回
1. クラウド側の権限クラスタの資格情報を取得する権限(container.clusters.getこれを含む最小権限のロールは container.clusterViewer)と、ログを読む権限。ログについてはロール名を渡すだけでは足りません。自分のNamespaceだけに絞り込んだ検索条件を、URLの形で1つ渡します第5回・第15回
2. クラスタ内の権限Namespace内のロールバインディング。グループ経由で付与し、個人に直接付けません第5回
3. Namespace次のH3の7項目が揃った状態第4回・第5回
4. リポジトリvaluesを書く場所、マージ先のブランチ、レビュー担当第8回・第10回
5. レジストリの読み取りイメージを取得するサービスアカウントへの roles/artifactregistry.reader第9回

2番目について、もう少し具体的に書きます。開発者に渡すロールを、その場で組み立てないでください1種類に固定し、例外は申請にします。第5回で確認したとおり、編集権限に相当する既定のロールは、そのNamespaceのSecretを読み書きでき、任意のServiceAccountとしてPodを実行できます。そのまま渡すと、第5回で設計した層1と層4が最初から無効です。渡すロールから外すものは3つ。Secretの読み取り、コンテナ内でのコマンド実行、ポートフォワード。この3つを外した状態が、本記事でいう「開発者の既定のロール」です。

そして付与はグループ経由に固定します。個人に直接付けると、離任時に外れず、四半期の棚卸し(第5回)でしか検出できません。グループ経由なら、外す操作は除外1回で終わります。払い出しの手順書に「個人に直接付与しない」と1行書いておくのは、後の棚卸しの工数を先に削る作業です。

5番目を忘れると、開発者は ImagePullBackOff で止まります。そして後述するとおり、この症状のうち権限が原因のものは開発者側では直せません。払い出しの不備が、開発者にとって自力解決不能な症状として現れる典型例です。

Namespaceは「作る」のではなく「揃える」

コマンド1つで作ったNamespaceは、本連載の設計を1つも満たしていません。払い出しとは、次が揃った状態を作ることです。

揃えるもの決めた回揃っていないと開発者に起きること
ResourceQuota第4回総量の制限がなく、他チームの資源を奪います
LimitRange第4回requestsを書き忘れたPodがBestEffortになり、退避時に最初に停止します(第11回)
NetworkPolicy(既定拒否と必須の例外)第5回通信が想定外に通ります。逆に例外を入れ忘れると、メタデータサーバへの経路が塞がり認証が失敗します
Pod Security Standards のラベル第5回Podのセキュリティ水準が定まりません
必須ラベル第4回コストの按分が成立しません
ServiceAccountとWorkload Identityのバインド第5回クラウドのAPIを呼べません
監視とログの宛先第15回開発者が自分のログを見つけられません

この7項目に手作業が1つでも残っているなら、それが自動化の対象です。第8回でTerraformのモジュール分割を、第9回でGitOpsを設計しました。払い出しは、その2つの適用先として最も分かりやすい対象です。第10回でIDPの要否を「払い出しのSLAが守れているか」で判定すると決めたのも、この作業量が判定材料になるからです。

申請に残すのは2種類だけ

自動で渡すものと、申請にするものを分けます。判定基準は1つです。その要求を断ることがあるか。断ることがないなら、申請は待ち時間を生む儀式にすぎません。

種類扱い
断ることがない標準構成のNamespace、既定のロールバインディング、レジストリの読み取り自動で渡します。申請にしません
断ることがある(1)増枠Quotaの引き上げ、マシンタイプの指定、GPU申請にします。判断材料は第4回の按分と予算
断ることがある(2)例外ポリシーの例外、本番での特権的な操作、外部公開申請にします。第5回で決めたとおり、例外には期限を付けます(既定3ヶ月)

第2回の実務テンプレートに残した「例外の申請先」の空欄は、ここで埋まります申請先は後述する受付窓口、承認者は第2回のRACIで決めた説明責任者です。

渡さないものを、渡さない理由とセットで書く

プレイブックには「できないこと」の節を置きます。理由を書かずに禁止だけを書くと、開発者は抜け道を探します。第5回で否定には代替を添えると決め、第9回で緊急の抜け道が恒久化すると書いたのと同じ構造です。

渡さないもの理由(過去回の決定)代わりに使える手段
本番でのコンテナ内コマンド実行とポートフォワード第5回。コマンド実行があればコンテナ内でシークレットを読め、ポートフォワードがあればNetworkPolicyで遮断した内部サービスに到達できます。層1と層3の設計が1行で無効化されますログ・イベント・メトリクス・トレースと、アプリの管理用エンドポイント。再現はStgで行います
ノードへのアクセス第3回で選んだ運用モード。Autopilotでは、そもそも基盤となるノードへのアクセスが禁止されていますノード側の事象は、後述する症状表の宛先に従って運用チームへ
Chartのテンプレート本体第10回。開発者が書くのはvaluesまで要望として出します(後述の様式)
本番への直接適用第9回。適用の経路はパイプラインに一本化しますPRとパイプライン。緊急時は第16回の手順
クラスタ全体に及ぶリソースの作成第5回。CRDやClusterRoleは運用チーム申請(例外の枠)

「渡さない」で終わる行を1つも作らないでください。代替のない禁止は、抜け道を探させるだけです。

最後に1行、離任と異動のときに権限を返す手順を同じ文書に置きます。渡す手順だけを書いた文書では、権限が積み上がります。本記事が書くのは開発者側の行動(返却の申告先と、返す対象の一覧)までで、剥奪の実行と棚卸しは第5回の管轄です。第5回で四半期の棚卸しを設計しましたが、棚卸しで検出するより、離任時に外すほうが安く、確実です。グループ経由で付与していれば、外す操作はグループからの除外1回で終わります。

初回デプロイのウォークスルー ― 最初の1日に踏む罠

ここで書くのは手順そのものではなく、手順書に何を書き込むと詰まらないかです。手順の中身は組織ごとに違いますが、詰まる場所はほぼ共通しています。

ウォークスルーは、1つのサンプルサービスで書き切る

第10回で「仕様書は読まれない」と書きました。読まなくても作業が進む場合、文書は開かれません。抽象的な手順書も同じ理由で読まれません。実在するサンプルサービスを1つ用意し、それを最後までデプロイする一本道として書いてください。分岐は書きません。分岐は次章以降の症状表に置きます。

段階は7つです。各段階に所要時間の目安ここで詰まったら見る場所を1行ずつ添えます。

段階開発者がやること
1. 認証クラウドにログインし、認証プラグインを導入する
2. 資格情報クラスタの資格情報を取得する
3. 確認自分のNamespaceに対して、読めること・書けることを確認する
4. valuesサンプルのvaluesをコピーし、必須項目を埋める
5. PR変更をPRにする。ここから先は第9回のパイプラインが動く
6. 同期マージ後、同期を待つ
7. 確認Podが Ready になり、ヘルスチェックが通ったことを自分で確認する

段階4の「必須項目」には、第2回の実務テンプレートで開発チームへの要求として並べた4項目を入れます。requestsの設定、標準出力へのJSON形式のログ(第15回)、Graceful Shutdownの実装、Readiness Probeの実装(この2つは第11回)。第2回のテンプレートでは、これらに移行期限を書く欄を設けました。ただし新規サービスに期限の概念はありません。開始時点で必須です。第15回で「規約を書いただけで守られることはない」と書き、実効性を担保する3経路の1つにオンボーディングを挙げたのは、この形にするためです。既存サービスには期限を、新規サービスにはチェック項目を。同じ要求でも、渡し方が違います。

手元の道具が古い

最初の3段階で詰まる原因は、ほぼこの2つです。どちらも本人の環境の問題なので、運用チームに聞いても解決しません。手引きの最初に置いてください。

詰まる原因内容確認方法
認証プラグインがないGKEクラスタとの通信には gke-gcloud-auth-plugin が必要です。gcloud CLI は Kubernetes 1.26 のリリースに先立って、この導入を要求するようになりました。入っていないと、資格情報を取得したあとの操作で認証が通りませんプラグインのバージョンを表示する
クライアントのバージョンが離れているkubectl がサポートされるのは、APIサーバの前後1マイナーバージョンの範囲です。離れていると、動くこともあれば予期しない失敗をすることもありますクライアントのバージョンを表示し、クラスタのバージョンと比べる

第14回で、マイナーバージョンは年に3回上がる前提で更新計画を立てましたクラスタが上がるということは、開発者の手元の道具も定期的に古くなるということです。更新の告知に、手元の道具の更新を1行入れる根拠がここにあります。

requestsを書かないと、そもそも作成が拒否される

Kubernetesの公式仕様です。CPUやメモリにResourceQuotaが設定されたNamespaceでは、新しいPodはrequestsまたはlimitsを指定しなければ、受け付けが拒否されることがあります。公式の表現では、そのNamespaceのすべての新規Podに対して、そのリソースの指定を強制します。

そして、LimitRangeがあれば既定値が注入されるため、この拒否は起きません。公式も、計算リソースの要求を書かないPodに既定を強制する目的でLimitRangeを使えると案内しています。開発者がこの罠に遭遇するかどうかは、運用チームがLimitRangeを置いたかどうかで決まります。前章で「Namespaceは揃える」と書いた項目の1つが、そのまま開発者の体験を左右します。

もう1つ、Quotaの変更は既に作成済みのリソースに遡及しません。「昨日まで動いていたのに、今日デプロイしたら拒否された」という問い合わせの説明がこれです。動いているPodは、Quotaを変えても動き続けます。

書いた値と、動いている値が違う

Autopilotでは、開発者が書いたrequestsがそのまま動くとは限りません。公式が明記している挙動です。

  • requestsを指定しない場合、既定値が適用されます。汎用・Balanced・Scale-Out のコンピュートクラスで CPU 0.5 vCPU、メモリ 2 GiB、エフェメラルストレージ 1 GiB。DaemonSetのコンテナは CPU 50 mCPU、メモリ 100 MiB、エフェメラルストレージ 100 MiB
  • 最小値を下回る指定は、ワークロードの構成が自動的に変更されて引き上げられます
  • CPUとメモリの比率が許容範囲を外れると、小さいほうが自動的に引き上げられます(汎用は 1:1 から 1:6.5、Balanced は 1:1 から 1:8、Scale-Out はちょうど 1:4)
  • requestsがlimitsより大きい場合、requestsがlimitsの値に設定されます

「メモリだけ増やしたつもりが、CPUの費用も増えた」という問い合わせの正体がこれです。第11回で比率の制限に触れ、第4回でAutopilotの課金がPodの要求リソースに対するものだと確認しました本記事が書くのは症状と戻り先だけです。値の決め方は第11回、費用への影響は第4回に返してください。

同期されるまで、書いたものは動いていない

マージした直後にPodの一覧を見ても、まだ変わっていません。第17回で確認したとおり、Argo CDの照合間隔は既定で120秒、これにジッターが60秒加わります。手引きには待ち時間の目安を書き、同期の状態をどこで見るかを1行で示してください。

そして段階7には、後始末の行を入れてください。動作確認のために作った検証用のPod、開いたままのポートフォワード、一時的に借りた権限。これらを消す操作を手順の最後に書いておかないと、確認のたびに残骸が積み上がります。Namespaceの総量はResourceQuotaで決まっているので(第4回)、残骸は次のデプロイの失敗として現れます。「昨日は通ったのに今日は資源不足で止まる」の一部は、自分が消し忘れたものが原因です。手順書の最後の行を空けたまま配ると、この因果は開発者からは見えません。

加えて、GitOpsを使っている環境で、稼働中のリソースを直接編集する操作は無意味です。自動同期が有効なら差し戻され、無効なら次の同期で消えます。第17回で「復旧手順の最初は自動同期の停止、最後は再開」と書いた理由と同じ構造です。開発者が本番を直接触ることを設計上ありえなくしておくと、第16回のインシデント対応の前提も守られます。対応中に、誰かが手で変えた差分を探す時間が消えます。

セルフサービスの範囲は、第5回で閉じた権限の裏返し

ここが本記事で2番目に重要な導出です。開発者が自力で調べられる範囲は、善意で決めるものではありません。過去回の権限設計から機械的に導かれます。

開発者が本番で見られるものの一覧

第5回で、コンテナ内でのコマンド実行とポートフォワードを開発者のロールに含めないでくださいと書きました。層1でSecretの読み取りを外し、層3で通信を絞った設計が、この2つを1行渡した時点で両方とも無効化されるからです。あの決定が、本番の一次調査に使える道具を確定させています。

手段本番Dev / Stg根拠
Podの一覧と詳細第5回
コンテナのログ(現在と、直前に終了した分第5回。ただし本番のログ閲覧は機微データの出力有無で判断します
イベント保持は既定で1時間(第15回)
メトリクスとダッシュボード第15回の3階層
トレース第15回。Trace IDで横断します
コンテナ内でのコマンド実行不可第5回。渡すと層1と層3が無効化されます
ポートフォワード不可同上
ノードへのアクセス不可不可第3回。Autopilotでは禁止されています

この表が、次章の症状表の前提条件です。手引きの各行は、この表にある道具だけで書けなければなりません。

権限を閉じたなら、閉じたうえで自力解決させる設計義務が生じる

本章の主張です。権限を閉じることと、閉じたうえで自力解決させることは、セットで設計しなければ成立しません。閉じただけなら、すべての症状が問い合わせに変換されます。第16回で下げようとした呼び出しの総量が、権限設計によって増えることになります。

したがって、運用チーム側に3つの義務が発生します。

  1. ログが読めること。第15回で確認したとおり、サイドカーでログを収集する構成にすると、そのログは kubectl logs では読めなくなります(kubeletが管理していないため、と公式は説明しています)。こちらの都合で、開発者に渡した一次調査の手段を壊さないでください
  2. 昨日の失敗を調べられること。イベントの保持は既定で1時間です。OOMKilled、スケジュール失敗、イメージ取得の失敗といった理由はイベントにしか出ないことが多く、翌朝には消えています。イベントをログとして残すかどうかは費用との交換で、残さないと決めるなら、手引きから「イベントを見る」という行を消し、代わりに何を見るかを書く義務が生じます
  3. アプリケーション側に代替の経路を用意すること。コンテナ内でのコマンド実行の代わりに、管理用のエンドポイントで内部状態を返します。最小の3項目を決めておいてください。読み込んだ設定の識別子(値そのものではなくハッシュ)、依存先への到達可否、動いているビルドの識別子。この3つがあれば、コンテナに入って確認したくなる場面の大半は外から判定できます

3番目で値そのものを返さないのは、第5回でシークレットの露出経路を閉じた判断と整合させるためです。そしてこれはアプリケーション側への設計要求なので、第2回のRACIで開発チームの責任として合意しておく必要があります。プレイブックに書いただけでは実装されません。

環境ごとに範囲を変える

Dev/Stgで渡し、本番で渡さない。この差は事故ではなく設計です。ただし差があること自体を手引きに明記しなければ、開発者は「Stgでできたことが本番でできない」を障害として報告します。

ここで1つ確認が要ります。第5回の記述は、環境を限定していません。環境で差を付けるかどうかは、本記事で決める判断です。そして、Dev/Stgで渡してよい条件は1つに絞れます。その環境に、本番の資格情報が存在しないこと。第3回で環境を分離し、第5回でシークレットの置き場所を設計したのは、この条件を成立させるためでもあります。本番とStgで同じ資格情報を共有しているなら、Stgでも渡してはいけません。環境の名前ではなく、そこに何が置いてあるかで決まります。

そして、差があるという事実が、Stgの価値を決めます。本番でコンテナに入れないなら、再現はStgで行うのが唯一の経路です。第3回で環境を分離した目的の1つが、ここで現れます。第12回のカオステストで成果物を「手順の不足」と定めたのも同じ形で、本番でできないことを、できる場所で先に確かめておくという設計になっています。

この構造には、運用上の帰結が1つあります。Stgが本番と大きく違っていると、再現の経路そのものが失われます。本番でしか起きない事象は、本番でしか調べられません。そして本番では道具を渡していません。「Stgでは再現しない」という報告が続くなら、それは開発者の調査能力の問題ではなく、環境差の問題です。第3回の環境分離の設計に戻してください。

症状別の手引きに書くのは、原因ではなく境界

症状と原因の対応表は、技術記事にいくらでもあります。それを社内文書に書き写す作業に価値はありません。プラットフォームチームが書く価値があるのは、自組織の権限と責任の境界が入った表です。同じ症状でも、直せる人が違います。

表の列は5つ

内容なぜ必要か
症状開発者が画面で見る文字列そのもの検索できる語であること。解釈を挟みません
最初に見る場所1箇所だけ書く3箇所書くと、どれも見られません
自分で直せる場合該当する条件と、直し方ここが空欄の行は、手引きに載せる意味がありません
自分では直せない場合の宛先窓口と、連絡時に貼るもの「運用チームに連絡」だけの行を書かないためです
戻る文書その制約を決めた設計書手引きは判断の入口で、根拠の置き場ではありません

4列目の「連絡時に貼るもの」を具体化してください。第16回のランブックで最低限の5項目を固定したのと同じ発想で、一次応答4時間という約束の中身を、往復1回で終わらせるための設計です。貼るものが決まっていないと、最初の返信が「詳細を教えてください」になり、4時間の約束は形式だけになります。症状の文字列、発生時刻、Namespace、対象の名前、実行したコマンドとその結果。この5点で足ります。

そして、この表の各行が実際に使えるかどうかは、書いた時点では分かりません。第12回でカオステストの成果物を「手順の不足」と定めました。障害を注入したときに、開発者がこの表だけで判定まで到達できるか。手引きの検証は、第12回の枠組みでそのまま行えます専用の仕組みを新設する必要はありません。

CrashLoopBackOff ― 終了コードから始める

最初に伝えるのは、これがエラーの名前ではなく状態の名前だということです。コンテナが起動と終了を繰り返しており、kubeletが再起動の間隔を空けている状態を指します。

公式に確定している挙動が3つあります。再起動の遅延は指数的に伸び(10秒、20秒、40秒と続きます)、300秒で頭打ちになります。そしてコンテナが10分間問題なく動作すると、kubeletは再起動のバックオフをリセットします。なお、初期値と上限を1秒と60秒に変えるアルファ機能がKubernetes 1.33に入っています。秒数は将来変わりうるため、手引きには秒数ではなく「間隔が伸びていく」という性質を書いてください。

切り分けは終了コードから始めます。

終了コード意味誰が直すか
0正常終了。常駐しないコマンドを起動している、ワークキューが空で終了している、設定ファイルがない(ConfigMapの未アタッチ、空、キーの不一致)開発。ただしConfigMapの取り違えは両方ありえます
128コマンドが無効、または実行ファイルが欠落開発(イメージまたはコマンドの指定)
137SIGKILL。終了理由が OOMKilled ならメモリ超過(次のH3へ)分岐します
その他の非ゼロアプリケーション内部のエラー。設定の構文誤り、環境変数の不正、起動コマンドの誤指定、可変タグによる予期しないイメージ更新、依存先への到達不能、認証の失敗、Workload Identityの権限不足分岐します

直前の実行のログを見る操作を、手引きの一等地に置いてください。再起動後のコンテナのログには、落ちた理由が残っていません。kubectl logs に直前の終了分を指定するオプションを付けるだけの話ですが、これを知らないと調査が始まりません。

Liveness Probeの失敗による再起動は、性質が違います。ノード側のログに「liveness probeに失敗したため再起動する」という記録が残るため、判定できます。この場合、アプリケーションは壊れていない可能性があります。起動に時間がかかっているだけかもしれません。Probeのパス、ポート、タイムアウト、初期遅延の設計は第11回の管轄なので手引きは「Probeの失敗である」と判定させるところで止め、値の見直しは第11回に返します。

OOMKilled ― 宛先が分岐する症状

同じ OOMKilled でも2種類あり、直せる人が違います。この分岐を書けるかどうかが、手引きの質を決めます。

種類何が起きたか誰が直すか
コンテナレベルそのコンテナが自分のメモリのlimitに達しました。OOMKilled として記録され、restartPolicyに従って再起動します開発。値の見直し(第11回)またはメモリリークの修正
ノードレベルノード全体のメモリが枯渇し、システム全体のOOM Killerが全プロセスを評価して終了させました。公式は、ノードの圧力による退避の仕組みが十分に働かなかった可能性を示すと説明しています運用。ノードのサイズ、退避の閾値、集約度の問題

判定材料も公式にあります。ノード側のカーネルログで、コンテナレベルのものには memory cgroup または memcg の記載があり、システムレベルのものにはcgroupへの言及がありません。ただし前章の表のとおり、ノードには開発者は入れません。したがって手引きには、コンテナレベルであることを確認する条件(自分のlimitに達しているか)を先に書き、それに当てはまらない場合は運用チームへ、という順序で書きます。

もう1つ、公式が挙げている落とし穴があります。cgroup v1では、OOM Killerがメインプロセス以外の子プロセスを終了させることがあり、Kubernetesはメインプロセスしか監視しないため、この部分的なOOMは検知されないことがあります。cgroup v2は、すべてのタスクが終了するか継続するかの二択になり、挙動が予測可能です。「メモリ不足のはずなのに OOMKilled と表示されない」という問い合わせの説明がこれです。

そして、手引きに書く注意が1つあります。公式が明示しています。メモリ使用率のメトリクスだけを見ていると、OOMの引き金になる急峻なスパイクを見逃します。第15回のダッシュボードで平均値だけを見せている場合、この症状は説明できませんダッシュボードの設計が、トラブルシューティングの可否を決めている例です。

ImagePullBackOff ― 境界が最もはっきりしている症状

定義から確認します。ImagePullBackOff は、kubeletがイメージの取得に失敗したものの、最大5分まで間隔を伸ばしながら再試行を続けている状態です。ErrImagePull は、回復しない一般的な失敗を指します。前者は待てば直る可能性があり、後者はありません。この違いを書いておくと、無用な再デプロイが減ります。

そして、エラーメッセージの文字列で宛先が決まります。

メッセージ原因誰が直すか
イメージが見つからないイメージ名やタグの誤り、削除済み開発。タグを直します
403 Forbiddenレジストリへの権限不足運用。サービスアカウントに roles/artifactregistry.reader を付与します
401 Unauthorizedトークン取得の失敗運用。サービスアカウントの有効性を確認します
名前解決の失敗DNSの問題(メタデータサーバへの経路を含む)運用
接続のタイムアウトネットワークの到達性(ファイアウォール、限定公開のアクセス)運用。第3回のネットワーク設計に戻ります

前章で「5点セットの5番目を忘れると開発者はここで止まる」と書いた回収がここです。払い出しの不備は、403として開発者の画面に出ます。そしてタグの誤りは、第9回で「タグは動く、ダイジェストは動かない」と決めた話に返ります。本番でダイジェスト参照にしていれば、この症状の一部は最初から発生しません。

CrashLoopBackOff、OOMKilled、ImagePullBackOffの3つの症状を横帯で表し、開発チームが直せる範囲と運用チームでないと直せない範囲を赤い破線で分けた図。境界の入る位置が症状ごとに異なることを示している
同じ症状名でも、宛先は1つに決まりません。手引きに書くべきなのは原因の一覧ではなく、この縦線がどこに入るかです。

Pending ― 2つを分ける

Pending は、作成は受け付けられたがスケジュールされていない状態です。開発者にとっての分岐は2つあります。

判定内容誰が直すか
そもそも作成が拒否されているResourceQuotaによる拒否。Podが作られていないので、Podの一覧に現れません。理由は上位のコントローラのイベントに出ます開発(増枠が必要なら申請)
作られたが載る場所がない資源不足、ノードセレクタ、アフィニティ、Taint分岐します。要求が過大なら開発、容量が足りないなら運用(第4回のスケーリング)

「Podが一覧に出てこない」という問い合わせの正体が1つ目です。Deploymentのイベントを見る、という行を手引きに入れておくだけで、この往復は消えます。Podがないことは、Podを探しても分かりません。

起動はするが、応答しない

最も問い合わせが長引く症状です。見る場所を、順番付きで固定します。

  1. Podは Ready か。Readinessが通っていなければ、そもそも経路に載っていません(第11回)
  2. Serviceの宛先に、そのPodが入っているか。セレクタとポートの一致を確認します。EndpointSliceの中身を見れば、載っているかどうかが分かります
  3. 経路のどこで切れているか。第15回のTrace IDで横断します。IDが伝播していなければ、調査はここで止まります
  4. 通信が許可されているか。第5回で既定拒否にしたため、新しい宛先は明示的に開ける必要があります

4番目は、開発者が自力で判定できない典型です。遮断されている場合、アプリケーションからは接続のタイムアウトにしか見えません。手引きにはこう書きます。「通信制御による遮断の疑いがある場合、自分では確認できないので、宛先と発生時刻を添えて連絡する」。渡していない権限を前提にした確認手順を書かないでください。実行できない手順は、書いた側の自己満足で終わります。

手引きに書いてはいけない3つ

書いてはいけないもの理由代わりに書くもの
コマンドの丸暗記を求める行第16回で「ランブックに残してよいのは分岐と判断だけ。決定的なコマンド列になったら、それは自動化の仕様書」と決めました判断(何を見て、どちらへ進むか)。決まりきった手順はスクリプトや対話型の手引きに寄せます
「運用チームに連絡」だけの行連絡された側が最初にやるのは情報の要求で、往復が1回増えます連絡時に貼るもの5点(症状の文字列、発生時刻、Namespace、対象、実行したコマンドと結果)
製品名や機能名だけを書いた行開発者は自分の画面でその名前を見つけられません。第6回で選定した道具の名前は、選んだ側にしか意味が通じませんどの画面から何回の操作で到達するか。なおクラウドのコンソールには対話型のトラブルシューティング手引きがあり、クラッシュループのPodはその対象に含まれます。自作の前に、既にあるものへの導線を書きます

問い合わせと機能要望は、同じ窓口で受けて別々に処理する

DORAは、プラットフォームの運用が失敗する型をいくつか挙げています。その1つがチケット運用の罠、つまり受け身の運用に終始する状態です。窓口の設計は、それを避けるための設計です。

受け口は1本、分類は3つ

窓口を分けると、開発者は「どちらに出すか」で迷い、結果として個人に直接聞きに行きます。個人宛の依頼は記録に残らず、第16回で測ると決めた負荷の数値にも現れません。見えない負荷は減らせません。

受け口は1本にします。そこで3つに分類します。

分類判定条件行き先約束
障害本番の利用者に影響が出ている、または出る見込みがある第16回の深刻度判定へ第16回のSeverity定義に従います
使い方手引きに書いてあるか、書いてあるべきことFAQへ第2回の一次応答4時間
要望今はできないことを、できるようにしてほしい様式に転記して受付一次応答は4時間。可否の回答は別

分類は受けた側が行います。開発者に分類させると、深刻度の判定が申告になります。第16回で、深刻度を「呼び出しの強さ」で定義して申告に依存しない形にしたのと同じ理由です。

そして分類そのものを記録してください。3つの比率が、この設計の効果を示す唯一の数字です。「使い方」の比率が下がらないなら、手引きが届いていません。「要望」の比率が上がるのは、悪い兆候ではありません。自力で動けるようになった開発者は、使い方ではなく仕様の話をするようになります。比率の変化を、件数の増減より先に見てください。総件数は開発チームの数に比例して増えるため、それだけを見ていると改善が見えません。

要望は、第10回の3つの問いに変換して受ける

第10回で、valuesの公開を求められたときの3つの問いを決めました。その値を環境ごとに変える必要が本当にあるか。間違えたときに何が起きるか。それは運用チームが決めるべき値ではないか。

要望の様式を、この3つの問いに開発者が答える形にします。第10回はこの問いを運用チームの判断基準として書きましたが、ここでは開発者が記入する欄になります。様式が問いに対応していれば、判断の材料が最初から揃い、往復が減ります。

可否は即答しません。一次応答4時間は「受け取った」ことの通知です。可否は、判断材料が揃う既存の場(月次の定例など)で回答すると決めておきます。即答すると、その場で断れるものだけを断る運用になり、判断が担当者ごとにばらつきます。第6回でADRに決定の経緯を残させたのと同じ理由です

そして却下の書き方です。第10回で「公開を断った要望の記録」の欄を作り、第6回でADRの却下理由に「時間が経てば変わる/変わらない」の分類を持たせました。要望の却下にも同じ2分類を使います。

  • 変わらない理由での却下(設計上の制約、規制要件):根拠を1行で示し、再申請の余地がないことを明示します
  • 変わる理由での却下(優先度、工数):再検討の条件を測定できる形で書きます。「同じ要望が四半期に3件を超えたら再検討する」のように、開発者側から見て到達可能な条件にします

「未決」と書ける欄を用意してください。第6回でADRを空欄にしないと決め、第10回で判定に迷う項目を未決と書けるようにしたのと同じです。判断していないことを、記録できる状態にします。返事をしないことと、未決だと伝えることは別です。

FAQは書くものではなく、溜まったものから作る

先回りして書いたFAQは、実際の質問と一致しません。手順は3つです。

  1. 受け口に来た「使い方」を、月次で数えます
  2. 昇格の閾値は件数で置きますが、その件数は決め方から導きます。同じ質問に1回答えるのに要する時間と件数の積が、FAQを1本書いて保守する時間を超えたときが昇格の条件です。根拠のない「3件」を既定にしないでください
  3. FAQが増え続けているなら、それは手引きの不足ではなく、手順そのものの不備です

3番目が重要です。FAQの肥大は、成果ではなく症状です。第16回で「原因不明で閉じたページが減らないと、ページの総数は減らない」と書いたのと同じ構造で、FAQで対処し続けている項目は、ウォークスルーか払い出しに戻して直すべきものです。項目数の上限を決めておき、上限に達したら追加ではなく「どれを手順の修正で消せるか」を検討する運用にしてください。

計画作業の依頼文を、毎回ゼロから書かない

第14回で、更新の告知について書きました。開発チームが知りたいのは更新日そのものではなく、「自分のアプリケーションに何が起きるか」と「自分が何をいつまでにやるか」です。この2つを毎回ゼロから書くと、告知が遅れます。定型の説明をプレイブックに置き、告知では日付と依頼事項だけを差し替えます。プレイブックに置くのは、更新時に開発チームが行う確認の項目です。動作確認の範囲、非推奨APIへの対応状況、そして前述した手元の道具の更新。

第17回からも同じ形の宿題を受けていますDR訓練の当日に開発チームへ依頼する項目です。第17回で、訓練の完了条件は「復元できたこと」ではなく「業務上の整合が確認できたこと」と決め、その中身を3項目に具体化しました。直前に登録したデータが存在するか(RPOの実測)、採番に重複や欠番がないか、外部システムとの突き合わせが取れるか。このうち、業務仕様を知らなければ判定できないものは、開発チームにしか確認できません。依頼の様式は更新の告知と同じにします。定型はプレイブック、都度差し替えるのは日付と対象だけです。

【実務テンプレート】開発者オンボーディング・プレイブック

これまでの17回のテンプレートとは、宛先が違います。記入するのは運用チームですが、読むのは開発者です。冒頭にその旨を1行入れてください。読み手が違う文書に、内部向けの略語や社内政治の経緯を書かないためです。

セクション0: この文書の前提(過去回からの転記)

所有権モデル(第2回): [A フルオープン / B 共通Helm Chart / C IDP]
運用モード(第3回): [Autopilot / Standard / 混在]
Namespace払い出しのSLA(第2回): ____営業日
問い合わせの一次応答(第2回): ____時間
本番で開発者に渡さないと決めた権限(第5回): ______________________________
ログの収集方式(第15回): [標準出力のみ / サイドカーあり]
 → サイドカーありの場合、開発者がログを読む経路: ______________________
イベントをログとして保持するか(第15回): [する(保持____日) / しない]
 → しない場合、手引きから「イベントを見る」の行を削除したか: [はい / いいえ]


セクション1: 利用開始の5点セット(払い出しチェックリスト)

1. クラウド側の権限: [付与済 / 未] / ロール______________________
 ログ検索のURL(Namespaceで絞り込み済み): ______________________
2. クラスタ内の権限: [付与済 / 未] / 付与単位 [グループ / 個人]
 ※ 個人に直接付与している場合、棚卸しの担当と周期: ______ / ____ヶ月
3. Namespace: 名称______________
 □ ResourceQuota □ LimitRange □ NetworkPolicy □ PSSのラベル
 □ 必須ラベル □ ServiceAccountのバインド □ 監視とログの宛先
 ※ 上記のうち手作業で行っている項目: ______________________________
4. リポジトリ: 場所______________ / マージ先______ / レビュー担当______
5. レジストリの読み取り: [付与済 / 未] / 対象サービスアカウント____________


セクション2: 自動で渡すもの / 申請にするもの

自動で渡す(断ることがないもの): ______________________________
申請(1)増枠: 申請先______ / 承認者(第2回のRACI)______ / 回答期限____営業日
申請(2)例外: 申請先______ / 承認者______ / 有効期限____ヶ月(既定3ヶ月・第5回)
離任・異動時の返却: 申告先______ / 返す対象の一覧______________________


セクション3: 初回デプロイのウォークスルー(7段階)

サンプルサービス名: ______________ / 置き場所: ______________
 段階1 認証: 所要____分 / 詰まったら見る場所______________
 段階2 資格情報: 所要____分 / 同______________
 段階3 権限の確認: 所要____分 / 同______________
 段階4 valuesの記入: 所要____分 / 必須項目______________
  ※ 新規サービスの必須(第15回): □ 標準出力へのJSONログ □ Graceful Shutdown □ Readiness Probe
 段階5 PR: レビュー担当______ / 想定リードタイム____時間
 段階6 同期: 待ち時間の目安____秒 / 同期の状態を見る場所______________
 段階7 動作確認: 確認項目______________ / 後始末で消すもの______________


セクション4: 症状別トラブルシューティング表

症状ごとに1行。最初から全部を埋めないでください。自組織で実際に発生した上位3件から書き始め、残りは発生時に追記します。空欄が並ぶ表は、表ごと読まれなくなります。

症状(画面に出る文字列): ______________________________
 最初に見る場所(1箇所だけ): ______________________________
 自分で直せる条件と直し方: ______________________________
 直せない場合の宛先: ______ / 連絡時に貼るもの: ______________________
 戻る社内文書: ______________________________


セクション5: 渡していない権限と、その理由

本番で使えないもの: ______________________________
 理由(1行): ______________________________
 代わりの手段: ______________________________
 その手段が使える場所: [Dev / Stg]


セクション6: 問い合わせ・要望フロー

受け口: ______________(1本に統一する)
分類する人: [受けた側](申告にしない)
要望の様式(第10回の3つの問い)
 ① 環境ごとに変える必要が本当にあるか: ______________________________
 ② 間違えたときに何が起きるか: ______________________________
 ③ 運用チームが決めるべき値ではないか: ______________________________
 判定: [採用 / 却下(変わらない理由) / 却下(変わる理由・再検討の条件______) / 未決]
 可否を回答する場: ______________ / 頻度____ヶ月ごと
FAQ: 昇格の閾値____件 / 項目数の上限____件 / 棚卸しの周期____ヶ月


セクション7: 計画作業のときに開発チームへ依頼すること(定型)

バージョンアップ時(第14回): 依頼項目______________________________
 期限の書式: ______________
DR訓練時(第17回): 動作確認の担当範囲______________________________
 確認項目: □ 直前に登録したデータの存在 □ 採番の重複と欠番 □ 外部システムとの突き合わせ
 ※ 都度差し替えるのは日付と対象のみ


セクション8: 観測値(オンボーディングの完了判定)

完了条件: 運用チームの操作を挟まず、Stgへの初回デプロイが成功したこと
 ① 初回デプロイまでの営業日数: ____営業日
  → 第10回のテンプレートの「実績____営業日」に転記する
 ② 初回デプロイまでの問い合わせ件数: ____件 / 最も多かった段階: 段階____
 ③ 自力解決率: ____%(測定期間は第2回の四半期見直しに揃える)
見直し: ____ヶ月ごと / 次回____/____/____

全18回の地図 ― どこに戻ればよいか

本連載は、1つの設計書要素を1つの記事として18回に分けてきました。最後に、実務で判断に迷ったときにどこへ戻ればよいかの索引を置きます。前章の症状表の5列目を、連載全体に拡張したものです。

判断に迷ったとき、どの回に戻るか

状況戻る回
そもそもKubernetesでよいのかを問われた第1回
「誰がやるのか」で揉めた第2回
環境やネットワークの構造を変える必要が出た第3回
費用が想定を超えた第4回
権限と通信の設計を見直す第5回
道具の選定をやり直す第6回
データの置き場所と日常のバックアップ第7回
コードとリポジトリの構成第8回
届ける経路と供給網第9回
開発者に渡すインターフェース第10回
Podが落ちる、スケールしない第11回
壊して確かめる第12回
既存システムからの切り替え第13回
バージョンアップ第14回
何を測るか、何で人を起こすか第15回
鳴った後に人がどう動くか第16回
リージョンが落ちた第17回
開発者が詰まっている第18回(本記事)

連載を通した3つの同型

18回を通して、同じ構造が繰り返し現れました。テーマは違っても、設計の形が同じものです。

同型現れた回内容
戻せなくなる時点がある第13回・第14回・第16回・第17回切り戻しの期限、更新の不可逆点、シークレットの破棄、DRの発動。どの設計にも「ここを越えたら戻れない」点があり、それを先に特定します
完了条件は観測値で書く第7回・第8回・第13回・第14回・第18回「作業が終わった」ではなく「観測できる状態になった」で完了を定義します
運開分離は4回に分けて実装される第2回・第5回・第8回・第9回責任の定義、権限の実装、コードの分離、経路の一本化。1箇所では完結しません

もう1つ、この地図の使い方があります。18冊を同時には書けません。実務では、全部を書き終える前にクラスタが動き始めます。そのとき何から書くかは、依存関係で決まります。第1回と第2回が書けていなければ、以降のすべてが未決の上に積み上がります。第2回で誰が決めるかが定まっていないと、第10回で公開・非公開の線が引けません。第5回で権限を閉じていないと、本記事のセルフサービスの範囲が決まりません。本記事で「どこかの回に戻ってください」と何度も書いたのは、この依存の向きが一方向だからです。

逆に、後回しにできるものもあります。第12回のカオステスト、第17回のDRは、稼働してから書いても手遅れになりません。ただし第16回のインシデント対応だけは例外で、稼働した日から必要になります。本番が動き始めた時点で、障害は発生しうるからです。順番の判断基準は「その設計がないまま稼働したとき、何が起きるか」です。

本連載で作った18の設計書は、書いた時点では何も守っていません。守り始めるのは、その決定が誰かの手順に現れたときです。第18回のプレイブックは、その変換の最後の1本でした。プレイブックに書けなかった決定は、実際には運用されていない決定だと考えてください。設計書を配ることではなく、手順に変換することが、プラットフォームエンジニアの仕事の終わりです。

前の記事