<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>SRE on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/sre/</link>
    <description>Recent content in SRE on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Fri, 06 Mar 2026 09:02:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/sre/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>PostgreSQL PITR復旧訓練ガイド: バックアップがあるのに戻せないを防ぐ実践手順</title>
      <link>https://www.ai2core.com/posts/2026-03-06-postgresql-pitr-drill-production-guide/</link>
      <pubDate>Fri, 06 Mar 2026 09:02:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-06-postgresql-pitr-drill-production-guide/</guid>
      <description>PostgreSQLのPoint-in-Time Recoveryを本番運用で成立させるために、バックアップ設計・復旧手順・訓練設計を具体例で解説。</description>
      <content:encoded><![CDATA[<h1 id="postgresql-pitr復旧訓練ガイド-バックアップがあるのに戻せないを防ぐ実践手順">PostgreSQL PITR復旧訓練ガイド: バックアップがあるのに戻せないを防ぐ実践手順</h1>
<p>PostgreSQL運用で最も危険なのは「バックアップがある」という安心感です。実際の障害では、バックアップ自体より <strong>復旧手順の不整合</strong> で時間を失います。たとえば、WAL保管期間が足りず目標時刻に戻せない、暗号鍵が見つからず復号できない、復旧後の整合性確認が曖昧で再開判断ができない、といった問題です。</p>
<p>本記事では、PostgreSQLの Point-in-Time Recovery（PITR）を、机上ではなく本番レベルで回すための実装手順を解説します。<code>pgBackRest</code> を例にしていますが、考え方は他ツールでも共通です。</p>
<h2 id="1-pitrの前提-3つ揃わないと復旧できない">1. PITRの前提: 3つ揃わないと復旧できない</h2>
<p>PITRは次の3要素で成立します。</p>
<ol>
<li><strong>ベースバックアップ</strong>（フルまたは差分）</li>
<li><strong>WALアーカイブ</strong>（継続的）</li>
<li><strong>目標時刻情報</strong>（いつまで戻すか）</li>
</ol>
<p>どれか1つでも欠けると成立しません。特に本番で多いのは「WALが途中で消えていた」ケースです。S3保存していても、ライフサイクル設定や権限変更で欠落することがあります。</p>
<h2 id="2-まず決めるべきrtorpo">2. まず決めるべきRTO/RPO</h2>
<p>技術論の前に、業務要件を決めます。</p>
<ul>
<li><strong>RTO</strong>（復旧に許容される時間）: 例 60分</li>
<li><strong>RPO</strong>（失ってよいデータ時間）: 例 5分</li>
</ul>
<p>この2つで設計が変わります。</p>
<ul>
<li>RPO 5分以内ならWALアーカイブ遅延監視が必須</li>
<li>RTO 60分以内なら復旧訓練を定期実施し、手順を自動化する必要あり</li>
</ul>
<p>要件不明のまま「毎日バックアップ」だけ実施しても、障害時に役立たないことが多いです。</p>
<h2 id="3-推奨アーキテクチャ単一リージョンの最小構成">3. 推奨アーキテクチャ（単一リージョンの最小構成）</h2>
<ul>
<li>DBサーバ: PostgreSQL 15/16</li>
<li>バックアップツール: pgBackRest</li>
<li>保存先: S3互換ストレージ（バージョニングON）</li>
<li>監視: Prometheus + Alertmanager</li>
<li>復旧先: 別ホスト（本番と同一ネットワーク）</li>
</ul>
<p>重要なのは、<strong>本番DBと別ホストで実際に復旧できること</strong> を定期検証する点です。</p>
<h2 id="4-実装手順pgbackrest">4. 実装手順（pgBackRest）</h2>
<h3 id="41-postgresql設定">4.1 PostgreSQL設定</h3>
<p><code>postgresql.conf</code> 例:</p>
<pre tabindex="0"><code class="language-conf" data-lang="conf">wal_level = replica
archive_mode = on
archive_command = &#39;pgbackrest --stanza=main archive-push %p&#39;
max_wal_senders = 10
wal_compression = on
</code></pre><p><code>archive_command</code> は失敗時に非0を返す必要があります。ここが曖昧だとWAL欠落に気づけません。</p>
<h3 id="42-pgbackrest設定">4.2 pgBackRest設定</h3>
<p><code>/etc/pgbackrest/pgbackrest.conf</code> 例:</p>
<pre tabindex="0"><code class="language-conf" data-lang="conf">[global]
repo1-type=s3
repo1-path=/pgbackrest
repo1-s3-bucket=prod-db-backup
repo1-s3-endpoint=s3.ap-northeast-1.amazonaws.com
repo1-s3-region=ap-northeast-1
repo1-retention-full=14
start-fast=y
process-max=4
compress-type=zst

[main]
pg1-path=/var/lib/postgresql/16/main
</code></pre><p>初期化:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pgbackrest --stanza<span style="color:#f92672">=</span>main stanza-create
</span></span><span style="display:flex;"><span>pgbackrest --stanza<span style="color:#f92672">=</span>main --type<span style="color:#f92672">=</span>full backup
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="43-wal到達の監視">4.3 WAL到達の監視</h3>
<ul>
<li><code>pg_stat_archiver</code> の <code>failed_count</code></li>
<li>最終成功時刻と現在時刻の差分</li>
<li>repoの最新WALタイムスタンプ</li>
</ul>
<p>例: 10分以上WALが更新されない場合にCriticalアラート。</p>
<h2 id="5-復旧訓練drillを標準化する">5. 復旧訓練（Drill）を標準化する</h2>
<p>運用で差が出るのはここです。訓練の目的は「復旧できること」ではなく、<strong>予測可能な時間で安全に復旧できること</strong> です。</p>
<h3 id="51-月次ドリル手順テンプレート">5.1 月次ドリル手順（テンプレート）</h3>
<ol>
<li>目標時刻を決める（例: 当日 08:35:00 JST）</li>
<li>復旧専用ホストを初期化</li>
<li>バックアップ + WALからリストア</li>
<li>DB起動後、整合性チェックを実行</li>
<li>アプリのスモークテストを実行</li>
<li>RTO実測値と課題を記録</li>
</ol>
<h3 id="52-実コマンド例">5.2 実コマンド例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 復旧先でデータディレクトリを準備</span>
</span></span><span style="display:flex;"><span>systemctl stop postgresql
</span></span><span style="display:flex;"><span>rm -rf /var/lib/postgresql/16/main/*
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 指定時刻までリストア</span>
</span></span><span style="display:flex;"><span>pgbackrest --stanza<span style="color:#f92672">=</span>main <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --type<span style="color:#f92672">=</span>time <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --target<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;2026-03-06 08:35:00+09&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --delta <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  restore
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 起動</span>
</span></span><span style="display:flex;"><span>systemctl start postgresql
</span></span></code></pre></td></tr></table>
</div>
</div><p>PostgreSQL 12+では <code>recovery.conf</code> ではなく <code>postgresql.auto.conf</code> + <code>standby.signal</code> 形式に変わっている点に注意してください。</p>
<h2 id="6-復旧後の整合性チェック項目">6. 復旧後の整合性チェック項目</h2>
<p>「起動したからOK」は危険です。最低限、次を確認します。</p>
<ul>
<li>主要テーブル件数（基準値との差分）</li>
<li>直近トランザクション時刻</li>
<li>重要集計値（売上、注文、在庫など）</li>
<li>アプリの read/write スモークテスト</li>
<li>レプリカ再構築可否</li>
</ul>
<p>SQL例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> now();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">max</span>(created_at) <span style="color:#66d9ef">FROM</span> orders;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> users;
</span></span></code></pre></td></tr></table>
</div>
</div><p>件数だけでなく「業務上重要な指標」を入れるのが実務的です。</p>
<h2 id="7-失敗しがちなポイントと対策">7. 失敗しがちなポイントと対策</h2>
<h3 id="71-wal保持期間が短すぎる">7.1 WAL保持期間が短すぎる</h3>
<ul>
<li>対策: RPO/RTOに基づき、最低でも7〜14日保持</li>
<li>削除はバックアップ整合性確認後に実行</li>
</ul>
<h3 id="72-暗号鍵認証情報の保管ミス">7.2 暗号鍵・認証情報の保管ミス</h3>
<ul>
<li>対策: KMS/Secret Managerに分離保管</li>
<li>緊急時アクセス手順をRunbook化</li>
</ul>
<h3 id="73-手順が個人依存">7.3 手順が個人依存</h3>
<ul>
<li>対策: RunbookをGit管理</li>
<li>ドリル時に“当番以外”が実行して再現性を検証</li>
</ul>
<h3 id="74-復旧はできるが遅すぎる">7.4 復旧はできるが遅すぎる</h3>
<ul>
<li>対策: 差分バックアップ頻度見直し</li>
<li>復旧先マシンスペックの最低保証</li>
<li>リストア並列度（<code>process-max</code>）調整</li>
</ul>
<h2 id="8-自動化の実装例ciでドリルを回す">8. 自動化の実装例（CIでドリルを回す）</h2>
<p>本番同等データを使えない場合は、匿名化済みスナップショットで定期検証します。</p>
<ul>
<li>毎週日曜 03:00 に復旧ジョブ起動</li>
<li>復旧後にSQLチェック + APIスモーク</li>
<li>結果をSlack/Discordへ通知</li>
<li>失敗時は翌営業日のSRE定例でレビュー</li>
</ul>
<p>この「半自動ドリル」を導入すると、障害時の初動が劇的に安定します。</p>
<h2 id="9-監査対応のための証跡">9. 監査対応のための証跡</h2>
<p>監査や顧客説明で必要になるのは、次の証跡です。</p>
<ul>
<li>バックアップ成功ログ（日次）</li>
<li>WAL連続性の証跡</li>
<li>復旧ドリルの実行記録（日時、担当、RTO実測）</li>
<li>改善アクション履歴</li>
</ul>
<p>「やっています」ではなく「この月にこの結果でした」と示せる状態を作っておきましょう。</p>
<h2 id="10-現場向けチェックリスト">10. 現場向けチェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> フルバックアップ成功率 99%以上</li>
<li><input disabled="" type="checkbox"> WAL遅延アラートが有効</li>
<li><input disabled="" type="checkbox"> 目標時刻指定の復旧手順がRunbook化</li>
<li><input disabled="" type="checkbox"> 月次ドリル実施済み</li>
<li><input disabled="" type="checkbox"> 復旧後整合性チェックSQLが整備済み</li>
<li><input disabled="" type="checkbox"> KMS/鍵管理手順が文書化済み</li>
</ul>
<p>このチェックリストを満たして初めて「PITR対応」と言えます。</p>
<h2 id="まとめ">まとめ</h2>
<p>PITRは設定項目の話ではなく、<strong>復旧可能性を継続的に検証する運用</strong> です。</p>
<ul>
<li>RTO/RPOを先に決める</li>
<li>バックアップ + WAL + 監視をセットで設計する</li>
<li>月次ドリルで実測し、Runbookを改善する</li>
<li>復旧後整合性チェックまで標準化する</li>
</ul>
<p>バックアップがあることはゴールではありません。障害時に「何分で、どこまで戻せるか」を言える状態こそが、プロダクション品質です。</p>
<h2 id="付録-実運用で使えるpitrドリル手順90分版">付録: 実運用で使えるPITRドリル手順（90分版）</h2>
<p>以下は、現場でそのまま回せる最小ドリル手順です。月次で固定化すると、障害対応の心理的負荷を下げられます。</p>
<ol>
<li><strong>開始宣言（5分）</strong>
<ul>
<li>担当者、開始時刻、目標RTO/RPOをチケットに記録</li>
</ul>
</li>
<li><strong>復旧環境準備（15分）</strong>
<ul>
<li>復旧先PostgreSQLを起動</li>
<li>バックアップ世代と目標復旧時刻（例: 10:32:00 JST）を確定</li>
</ul>
</li>
<li><strong>リストア実行（25分）</strong>
<ul>
<li><code>pg_restore</code> もしくはベースバックアップ展開</li>
<li>WAL適用完了ログを保存</li>
</ul>
</li>
<li><strong>整合性検証（20分）</strong>
<ul>
<li>件数チェックSQL、外部キー整合性、直近注文IDの連続性を確認</li>
<li>APIスモーク（ログイン/一覧/作成）を実施</li>
</ul>
</li>
<li><strong>振り返り（25分）</strong>
<ul>
<li>実測RTO/RPOを記録</li>
<li>失敗ポイントをRunbookに即反映</li>
</ul>
</li>
</ol>
<p>重要なのは「成功したか」だけではなく、<strong>どこで何分消費したか</strong>を毎回残すことです。これを3回続けるだけで、復旧手順のボトルネックがかなり明確になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>PostgreSQL</category>
      <category>Backup</category>
      <category>PITR</category>
      <category>SRE</category>
      <category>Disaster Recovery</category>
    </item>
    <item>
      <title>Kubernetes環境でDBスキーマ変更を止めずに進める：ゼロダウンタイム移行の実践戦略</title>
      <link>https://www.ai2core.com/posts/2026-03-05-kubernetes-zero-downtime-db-migration-strategy/</link>
      <pubDate>Thu, 05 Mar 2026 09:16:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-05-kubernetes-zero-downtime-db-migration-strategy/</guid>
      <description>本番KubernetesでDBマイグレーションを安全に実施するためのExpand-Contract戦略、手順、ロールバック設計を具体例つきで解説。</description>
      <content:encoded><![CDATA[<h1 id="kubernetes環境でdbスキーマ変更を止めずに進めるゼロダウンタイム移行の実践戦略">Kubernetes環境でDBスキーマ変更を止めずに進める：ゼロダウンタイム移行の実践戦略</h1>
<p>「カラムを追加するだけだから大丈夫」──この油断が、本番障害の入口になります。Kubernetes のように複数バージョンの Pod が同時に存在する環境では、DB スキーマ変更はアプリ変更よりも慎重に扱う必要があります。</p>
<p>本記事では、<strong>Expand-Contract パターン</strong>を中心に、ゼロダウンタイムを目指すための具体手順を解説します。実際の運用では、DDLの速さより「互換性のある期間をどう作るか」が勝負です。</p>
<h2 id="1-なぜkubernetesでdb移行が難しいのか">1. なぜKubernetesでDB移行が難しいのか</h2>
<p>Kubernetesでは、ローリングアップデート中に新旧Podが混在します。つまり次の状態が同時に発生します。</p>
<ul>
<li>新アプリは新スキーマを期待</li>
<li>旧アプリは旧スキーマしか知らない</li>
<li>DBは1つしかない</li>
</ul>
<p>このとき破綻するのが「破壊的変更を先に適用する」ケースです。たとえば旧カラムを即削除すると、旧Podがエラーを連発します。</p>
<h2 id="2-基本戦略expand--migrate--contract">2. 基本戦略：Expand → Migrate → Contract</h2>
<p>ゼロダウンタイム移行の原則はこの3段階です。</p>
<ol>
<li><strong>Expand</strong>: 互換性を壊さない変更を先に入れる（新カラム追加など）</li>
<li><strong>Migrate</strong>: アプリを段階的に切替え、データを移行する</li>
<li><strong>Contract</strong>: 旧仕様を最終削除する（十分な監視後）</li>
</ol>
<p>この順序なら、どの時点でも旧新どちらのアプリも動作可能にできます。</p>
<h2 id="3-具体例usersfull_name-を-first_name--last_name-へ分割">3. 具体例：<code>users.full_name</code> を <code>first_name</code> / <code>last_name</code> へ分割</h2>
<h3 id="31-expand-フェーズ">3.1 Expand フェーズ</h3>
<p>まず破壊的でないDDLを適用します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> users <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> first_name text;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> users <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> last_name text;
</span></span></code></pre></td></tr></table>
</div>
</div><p>この時点で旧アプリは <code>full_name</code> を使い続けられます。新アプリは新カラムに対応した実装を持っていても、まだ必須にしません。</p>
<h3 id="32-アプリを両対応にする">3.2 アプリを「両対応」にする</h3>
<p>書き込み時は両方へ保存（dual write）し、読み込み時は新カラム優先 + 旧カラムフォールバックにします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">save_user_name</span>(user_id: str, full_name: str):
</span></span><span style="display:flex;"><span>    first, last <span style="color:#f92672">=</span> split_name(full_name)
</span></span><span style="display:flex;"><span>    db<span style="color:#f92672">.</span>execute(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        UPDATE users
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        SET full_name = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            first_name = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            last_name = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        WHERE id = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>        (full_name, first, last, user_id),
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">read_user_display_name</span>(row):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> row<span style="color:#f92672">.</span>first_name <span style="color:#f92672">and</span> row<span style="color:#f92672">.</span>last_name:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>row<span style="color:#f92672">.</span>first_name<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>row<span style="color:#f92672">.</span>last_name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> row<span style="color:#f92672">.</span>full_name
</span></span></code></pre></td></tr></table>
</div>
</div><p>この両対応期間を作るのが、ゼロダウンタイムの本質です。</p>
<h3 id="33-バックフィル既存データ移行">3.3 バックフィル（既存データ移行）</h3>
<p>大規模テーブルでは一括更新を避け、チャンク更新します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- 疑似コードイメージ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> users
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SET</span> first_name <span style="color:#f92672">=</span> split_part(full_name, <span style="color:#e6db74">&#39; &#39;</span>, <span style="color:#ae81ff">1</span>),
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> split_part(full_name, <span style="color:#e6db74">&#39; &#39;</span>, <span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">&gt;</span> :last_id
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> id <span style="color:#f92672">&lt;=</span> :last_id <span style="color:#f92672">+</span> <span style="color:#ae81ff">10000</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> (first_name <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> last_name <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>ジョブ実装時のポイント:</p>
<ul>
<li>1チャンクごとに commit</li>
<li>再開可能なチェックポイント（last_id）を保存</li>
<li>実行時間帯を制御（ピーク時間回避）</li>
</ul>
<h3 id="34-contract-フェーズ">3.4 Contract フェーズ</h3>
<p>全Podが新実装になり、フォールバックが不要と判断できたら旧カラム削除へ進みます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> users <span style="color:#66d9ef">DROP</span> <span style="color:#66d9ef">COLUMN</span> full_name;
</span></span></code></pre></td></tr></table>
</div>
</div><p>削除は必ず最後です。ここを急ぐとロールバック不能になります。</p>
<h2 id="4-マイグレーション実行方式の選び方">4. マイグレーション実行方式の選び方</h2>
<p>Kubernetesでは主に3パターンがあります。</p>
<h3 id="a-cicdで先行実行推奨">A. CI/CDで先行実行（推奨）</h3>
<p>デプロイ前に migration Job を走らせ、成功後にアプリを更新。</p>
<ul>
<li>メリット: 実行順序を固定しやすい</li>
<li>デメリット: 長時間 migration の扱いが難しい</li>
</ul>
<h3 id="b-initcontainer-実行">B. initContainer 実行</h3>
<p>Pod起動時に migration を走らせる方式。</p>
<ul>
<li>メリット: 実装が単純</li>
<li>デメリット: 複数Pod同時起動で競合しやすい</li>
</ul>
<h3 id="c-専用-migration-controller--job">C. 専用 Migration Controller / Job</h3>
<p>Argo Workflows などで明示的に管理。</p>
<ul>
<li>メリット: 大規模運用で監査しやすい</li>
<li>デメリット: 初期構築コストが高い</li>
</ul>
<p>中規模までなら A が最も事故が少ないです。</p>
<h2 id="5-alembicflyway運用の実務ポイント">5. Alembic/Flyway運用の実務ポイント</h2>
<h3 id="51-1-migration--1責務">5.1 1 migration = 1責務</h3>
<p>「追加 + データ移行 + 削除」を1ファイルに詰め込むと失敗時に戻しづらくなります。Expand/Migrate/Contract を別 migration に分けて、各段階を検証可能にしてください。</p>
<h3 id="52-lock-timeout-を設定する">5.2 lock timeout を設定する</h3>
<p>DDL はロック待ちでアプリを止めることがあります。PostgreSQL なら <code>lock_timeout</code> を設定して、危険な待機を避けます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SET</span> lock_timeout <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;5s&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> ...;
</span></span></code></pre></td></tr></table>
</div>
</div><p>失敗時は即中断し、メンテナンス時間帯に再実行する判断が可能です。</p>
<h3 id="53-破壊的変更前に計測窓を設ける">5.3 破壊的変更前に計測窓を設ける</h3>
<p>旧カラム参照がゼロになったことを、メトリクスやログで一定期間確認してから削除します。目安として 7〜14日程度の観測期間を取ると安全です。</p>
<h2 id="6-ロールバック戦略">6. ロールバック戦略</h2>
<p>ゼロダウンタイム設計では、ロールバック可能性を最初に決めます。</p>
<ul>
<li>Expand段階: ほぼ即ロールバック可能</li>
<li>Migrate段階: dual write を維持していればロールバック可能</li>
<li>Contract段階: 削除後は復元コスト高（バックアップ依存）</li>
</ul>
<p>したがって、Contract 実施前に以下を満たす必要があります。</p>
<ul>
<li>直近スナップショット取得済み</li>
<li>復元手順を演習済み</li>
<li>影響範囲（API・バッチ・BI）を棚卸し済み</li>
</ul>
<h2 id="7-監視項目と成功判定">7. 監視項目と成功判定</h2>
<p>移行中は「成功したか」より「安全に進んでいるか」を見ます。</p>
<ul>
<li>APIエラー率（4xx/5xx）</li>
<li>DBロック待ち時間</li>
<li>スロークエリ件数</li>
<li>migration ジョブ進捗（処理済み件数、残件数）</li>
<li>旧カラム参照回数</li>
</ul>
<p>成功判定の例:</p>
<ol>
<li>新旧Pod混在中のエラー率に有意な悪化なし</li>
<li>バックフィル完了率100%</li>
<li>旧カラム参照0が連続7日</li>
<li>Contract後24時間で異常なし</li>
</ol>
<h2 id="8-よくある失敗と回避策">8. よくある失敗と回避策</h2>
<h3 id="失敗1-非null制約を早く付けすぎる">失敗1: 非NULL制約を早く付けすぎる</h3>
<p>バックフィル完了前に <code>NOT NULL</code> を付けると、旧データで失敗します。まずは nullable で追加し、データ移行後に制約追加が正解です。</p>
<h3 id="失敗2-インデックス作成で書き込み停止">失敗2: インデックス作成で書き込み停止</h3>
<p>大きいテーブルで通常インデックス作成を行うとロックが重くなります。PostgreSQL では <code>CREATE INDEX CONCURRENTLY</code> を使って影響を下げます。</p>
<h3 id="失敗3-migration-の実行主体が複数">失敗3: migration の実行主体が複数</h3>
<p>同時に2つのJobが走ると競合します。Kubernetes Job は排他制御（Lease/Lock）を持たせるか、CI側で単一実行を保証してください。</p>
<h2 id="9-実運用テンプレートmigration-job-と段階リリース">9. 実運用テンプレート：Migration Job と段階リリース</h2>
<p>最後に、現場でそのまま使える最小テンプレートを示します。ポイントは「DDLを一気にやらない」「アプリ側フラグで読取切替を制御する」の2点です。</p>
<h3 id="91-migration-jobexpand専用">9.1 Migration Job（Expand専用）</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">batch/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Job</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">users-expand-20260305</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">backoffLimit</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">restartPolicy</span>: <span style="color:#ae81ff">Never</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">migrate</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">ghcr.io/example/api:1.42.0</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">command</span>: [<span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-lc&#34;</span>, <span style="color:#e6db74">&#34;alembic upgrade 20260305_expand_users_name&#34;</span>]
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">envFrom</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">secretRef</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-db-secret</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Expand 用 Job を分離しておけば、失敗時にアプリデプロイを止める判断が明確になります。<code>backoffLimit: 0</code> にして「失敗を隠さない」運用にするのも実務で有効です。</p>
<h3 id="92-段階リリース手順例">9.2 段階リリース手順（例）</h3>
<ol>
<li>Expand Job 実行（DDLのみ）</li>
<li>アプリ v1（dual write + fallback read）を10%配信</li>
<li>エラー率・ロック待ちを30分監視</li>
<li>50% → 100%へ段階拡大</li>
<li>バックフィル Job 実行</li>
<li>旧参照ゼロ確認後に Contract 実施</li>
</ol>
<p>Kubernetes では <code>kubectl rollout status deployment/api -n prod</code> を必ず監視に組み込み、配信完了判定を人間が明示的に確認する運用が安全です。</p>
<h2 id="まとめ">まとめ</h2>
<p>KubernetesでのDBマイグレーションは、DDLテクニックだけでは成功しません。重要なのは、</p>
<ul>
<li>互換性期間を意図的に作る</li>
<li>段階を分けて進める</li>
<li>ロールバック可能性を先に設計する</li>
<li>監視で「削除してよい」根拠を取る</li>
</ul>
<p>この4点です。Expand-Contract を守るだけで、移行の失敗率は目に見えて下がります。スキーマ変更は怖い作業ですが、手順化すれば再現可能な運用にできます。次回の変更から、ぜひこの流れで試してみてください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>Database</category>
      <category>Migration</category>
      <category>SRE</category>
      <category>DevOps</category>
    </item>
    <item>
      <title>PostgreSQL接続プール枯渇の実戦対処：再発防止までつなげる調査・改善プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-05-postgresql-connection-pool-exhaustion-playbook/</link>
      <pubDate>Thu, 05 Mar 2026 09:12:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-05-postgresql-connection-pool-exhaustion-playbook/</guid>
      <description>本番で頻発するPostgreSQL接続枯渇を、発生時の初動から原因切り分け、設定改善、監視強化まで具体手順で解説。</description>
      <content:encoded><![CDATA[<h1 id="postgresql接続プール枯渇の実戦対処再発防止までつなげる調査改善プレイブック">PostgreSQL接続プール枯渇の実戦対処：再発防止までつなげる調査・改善プレイブック</h1>
<p>本番障害でよくあるのが、<code>too many clients already</code> や <code>remaining connection slots are reserved</code> です。アプリ側から見ると「急にDBに繋がらない」、ユーザー側から見ると「全機能が遅い・失敗する」という最悪の体験になります。</p>
<p>厄介なのは、接続枯渇が「DBサーバー性能不足」だけで起こるわけではない点です。リーク、タイムアウト設定、長時間トランザクション、プールサイズ不整合など、複数要因が重なって起きます。</p>
<p>この記事では、接続枯渇に対して <strong>発生時の初動 → 根本原因の特定 → 恒久対策</strong> の順で、手順を実務レベルでまとめます。</p>
<h2 id="1-まず初動サービス継続を優先する">1. まず初動：サービス継続を優先する</h2>
<p>障害対応では、完璧な原因究明より「止血」が先です。以下を順番に実施します。</p>
<ol>
<li>直近リリース有無を確認（機能フラグ含む）</li>
<li>アプリの接続数・待機数・エラー率を確認</li>
<li>DB側で <code>pg_stat_activity</code> を取得</li>
<li>長時間実行クエリを必要に応じて停止</li>
<li>一時的にアプリ Pod 数を制限して雪だるま増幅を止める</li>
</ol>
<p><code>pg_stat_activity</code> の基本クエリ:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  pid,
</span></span><span style="display:flex;"><span>  usename,
</span></span><span style="display:flex;"><span>  application_name,
</span></span><span style="display:flex;"><span>  client_addr,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">state</span>,
</span></span><span style="display:flex;"><span>  wait_event_type,
</span></span><span style="display:flex;"><span>  wait_event,
</span></span><span style="display:flex;"><span>  now() <span style="color:#f92672">-</span> query_start <span style="color:#66d9ef">AS</span> query_duration,
</span></span><span style="display:flex;"><span>  now() <span style="color:#f92672">-</span> xact_start  <span style="color:#66d9ef">AS</span> xact_duration,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">left</span>(query, <span style="color:#ae81ff">120</span>) <span style="color:#66d9ef">AS</span> query_head
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_activity
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> datname <span style="color:#f92672">=</span> current_database()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> xact_start NULLS <span style="color:#66d9ef">LAST</span>, query_start NULLS <span style="color:#66d9ef">LAST</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここで見るべきは、<code>state='idle in transaction'</code> と異常に長い <code>xact_duration</code> です。これがあるとコネクションを握ったまま解放されず、枯渇の引き金になります。</p>
<h2 id="2-典型原因を4パターンで切り分ける">2. 典型原因を4パターンで切り分ける</h2>
<h3 id="パターンa-アプリ接続プールサイズが過大">パターンA: アプリ接続プールサイズが過大</h3>
<p>よくあるのが、以下のような構成です。</p>
<ul>
<li>Pod 20個</li>
<li>各Podのプール max 20</li>
<li>理論最大接続 400</li>
<li>PostgreSQL <code>max_connections=300</code></li>
</ul>
<p>この時点で設計破綻です。さらに管理接続やメンテ接続を引くと余裕ゼロになります。</p>
<p><strong>対策:</strong></p>
<ul>
<li>プールサイズ設計は「全インスタンス合計」で管理</li>
<li><code>max_connections</code> の70〜80%以内に通常運用を収める</li>
<li>ピーク時はワーカ数/Pod数で制御</li>
</ul>
<h3 id="パターンb-コネクションリーク">パターンB: コネクションリーク</h3>
<p><code>finally</code> で close していない、ORMセッションの寿命が長い、例外時に返却されない、といった実装ミスです。</p>
<p>Python（SQLAlchemy）の悪い例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>session <span style="color:#f92672">=</span> SessionLocal()
</span></span><span style="display:flex;"><span>user <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>query(User)<span style="color:#f92672">.</span>filter(User<span style="color:#f92672">.</span>id <span style="color:#f92672">==</span> user_id)<span style="color:#f92672">.</span>first()
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 例外時にcloseされない可能性</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> user
</span></span></code></pre></td></tr></table>
</div>
</div><p>改善例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> contextlib <span style="color:#f92672">import</span> contextmanager
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@contextmanager</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_session</span>():
</span></span><span style="display:flex;"><span>    session <span style="color:#f92672">=</span> SessionLocal()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">yield</span> session
</span></span><span style="display:flex;"><span>        session<span style="color:#f92672">.</span>commit()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span>:
</span></span><span style="display:flex;"><span>        session<span style="color:#f92672">.</span>rollback()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">finally</span>:
</span></span><span style="display:flex;"><span>        session<span style="color:#f92672">.</span>close()
</span></span></code></pre></td></tr></table>
</div>
</div><p>FastAPI なら dependency でセッションスコープを統一し、endpointごとに確実に返却させるのが安全です。</p>
<h3 id="パターンc-長時間トランザクション">パターンC: 長時間トランザクション</h3>
<p>バッチ処理で 1トランザクションに大量更新を詰め込むと、ロック競合と接続占有が同時発生します。</p>
<p><strong>対策:</strong></p>
<ul>
<li>バッチをチャンク分割（例: 500件単位）</li>
<li><code>statement_timeout</code>、<code>idle_in_transaction_session_timeout</code> を設定</li>
<li>長時間処理はキュー化して非同期実行</li>
</ul>
<h3 id="パターンd-プールとdbのタイムアウト不一致">パターンD: プールとDBのタイムアウト不一致</h3>
<p>アプリの接続再利用時間が長すぎると、DB側で切断済み接続を使って失敗し、リトライで更に接続圧を上げます。</p>
<p><strong>対策:</strong></p>
<ul>
<li>プールの <code>maxLifetime</code> を DB/NLB timeout より短く</li>
<li>接続取得待ち時間（acquire timeout）を短くし、早めに失敗させる</li>
<li>失敗時リトライは指数バックオフ + ジッター</li>
</ul>
<h2 id="3-具体的な設定例pgbouncer--postgresql">3. 具体的な設定例（PgBouncer + PostgreSQL）</h2>
<p>高トラフィック環境では、アプリ直結より PgBouncer を挟むのが安定します。</p>
<p><code>pgbouncer.ini</code> の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[pgbouncer]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen_addr</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">0.0.0.0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen_port</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">6432</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">auth_type</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">md5</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pool_mode</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">transaction</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">max_client_conn</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">5000</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">default_pool_size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">80</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">reserve_pool_size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">20</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server_reset_query</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">DISCARD ALL</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server_idle_timeout</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">30</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>PostgreSQL 側の最小設定例:</p>
<pre tabindex="0"><code class="language-conf" data-lang="conf">max_connections = 300
shared_buffers = 4GB
idle_in_transaction_session_timeout = 60000
statement_timeout = 30000
log_min_duration_statement = 1000
</code></pre><p><code>pool_mode=transaction</code> は接続効率が高い一方、セッション依存機能（一時テーブルや session variable）の扱いに注意が必要です。導入前に該当クエリを洗い出してください。</p>
<h2 id="4-kubernetes運用での落とし穴">4. Kubernetes運用での落とし穴</h2>
<p>Kubernetes では、HPA がスケールアウトすると同時に接続数が急増しやすいです。</p>
<h3 id="41-設計式を最初に決める">4.1 設計式を最初に決める</h3>
<p><code>最大接続見積もり = maxPods × perPodPoolMax + 管理余白</code></p>
<p>例:</p>
<ul>
<li>maxPods=30</li>
<li>perPodPoolMax=8</li>
<li>管理余白=30</li>
<li>合計 270 → <code>max_connections=320</code> なら許容</li>
</ul>
<p>この計算を IaC へコメントで残しておくと、後任が壊しにくくなります。</p>
<h3 id="42-readiness-に-db接続必須チェックを入れすぎない">4.2 readiness に DB接続必須チェックを入れすぎない</h3>
<p>Pod起動時に全Podが同時にDBへ接続テストすると、再起動時にスパイクが起きます。readiness は軽量化し、重い初期化はバックグラウンドへ逃がすのが無難です。</p>
<h2 id="5-監視項目再発防止の最低ライン">5. 監視項目：再発防止の最低ライン</h2>
<p>以下を可視化し、閾値アラートを設定します。</p>
<ul>
<li><code>numbackends / max_connections</code></li>
<li>接続待ち時間（アプリメトリクス）</li>
<li><code>idle in transaction</code> セッション数</li>
<li>95/99パーセンタイルのクエリ時間</li>
<li>DBエラー率（connection refused, timeout）</li>
</ul>
<p>推奨アラート例:</p>
<ul>
<li><code>numbackends &gt; 85%</code> が 5分継続で Warning</li>
<li><code>numbackends &gt; 92%</code> が 2分継続で Critical</li>
<li><code>idle in transaction &gt; 10</code> が 3分継続で調査開始</li>
</ul>
<h2 id="6-障害後レビューポストモーテムで必ず決めること">6. 障害後レビュー（ポストモーテム）で必ず決めること</h2>
<p>「接続が足りませんでした」で終えると再発します。次の観点を明文化します。</p>
<ol>
<li>なぜ早期検知できなかったか</li>
<li>どの設定が設計値と乖離していたか</li>
<li>コード修正・設定修正・監視修正の担当と期限</li>
<li>次回同様インシデント時の runbook 更新点</li>
</ol>
<p>運用改善は「学びをコード化する」ことです。ドキュメントだけでなく、Terraform や Helm values に反映して初めて再発率が下がります。</p>
<h2 id="7-実務向けチェックリスト">7. 実務向けチェックリスト</h2>
<p>最後に、現場で使いやすいチェックリストを置いておきます。</p>
<ul>
<li><input disabled="" type="checkbox"> <code>max_connections</code> と総プール上限の関係を計算済み</li>
<li><input disabled="" type="checkbox"> 接続リーク防止（close/return保証）が実装されている</li>
<li><input disabled="" type="checkbox"> 長時間トランザクションに timeout が設定済み</li>
<li><input disabled="" type="checkbox"> PgBouncer の pool_mode が要件に適合している</li>
<li><input disabled="" type="checkbox"> <code>numbackends</code> と接続待ち時間のアラートがある</li>
<li><input disabled="" type="checkbox"> 障害時 runbook に SQL コマンドと判断基準が書かれている</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>PostgreSQL 接続枯渇は、単なるパラメータ不足ではなく、アプリ実装・スケーリング設計・監視不足が重なって起こる複合障害です。だからこそ、</p>
<ul>
<li>初動で止血する</li>
<li>パターン別に切り分ける</li>
<li>設定と実装を同時に直す</li>
<li>監視と runbook に落とし込む</li>
</ul>
<p>この流れを徹底するだけで、同じ障害の再発率は大きく下げられます。次に障害が起きたとき、慌てず順番に潰せる状態を今日作っておくのが最善です。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>PostgreSQL</category>
      <category>Performance</category>
      <category>SRE</category>
      <category>Troubleshooting</category>
      <category>Backend</category>
    </item>
    <item>
      <title>PostgreSQL肥大化対策の実務：VACUUM/Autovacuum/Index再編成を止めずに回す運用プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-04-postgresql-vacuum-bloat-control-playbook/</link>
      <pubDate>Wed, 04 Mar 2026 09:20:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-04-postgresql-vacuum-bloat-control-playbook/</guid>
      <description>PostgreSQLのテーブル・インデックス肥大化を本番停止なしで抑えるために、Autovacuum設計、監視指標、再編成手順、障害時対応を具体例つきで整理。</description>
      <content:encoded><![CDATA[<h1 id="postgresql肥大化対策の実務vacuumautovacuumindex再編成を止めずに回す運用プレイブック">PostgreSQL肥大化対策の実務：VACUUM/Autovacuum/Index再編成を止めずに回す運用プレイブック</h1>
<p>PostgreSQL を長期運用すると、遅かれ早かれぶつかるのが bloat（テーブル/インデックス肥大化）です。CPU やメモリを増やしても、実体は不要領域の蓄積なので、根本原因を処理しない限り性能は戻りません。</p>
<p>本記事では、<strong>サービス停止なしで bloat を抑える運用</strong>を目標に、Autovacuum 設計、監視、メンテ手順を実践ベースで解説します。</p>
<h2 id="1-なぜ肥大化が起きるのか">1. なぜ肥大化が起きるのか</h2>
<p>PostgreSQL は MVCC を採用しているため、UPDATE/DELETE で古い行バージョンが即時削除されません。不要バージョンは VACUUM で回収されますが、追いつかないと肥大化します。</p>
<p>肥大化が進むと以下が起こります。</p>
<ul>
<li>同じデータ量でも I/O が増える</li>
<li>インデックス探索が遅くなる</li>
<li>キャッシュ効率が落ち、p95 レイテンシが悪化</li>
<li>自動メンテの時間がさらに伸びる（悪循環）</li>
</ul>
<p>重要なのは、<strong>「遅くなってから対処」だと回復コストが高い</strong>という点です。</p>
<h2 id="2-最初に見るべき指標">2. 最初に見るべき指標</h2>
<p>運用でまず可視化するのは次の4つです。</p>
<ol>
<li><code>n_dead_tup</code>（死んだタプル数）</li>
<li><code>last_autovacuum</code>（最後に vacuum が走った時刻）</li>
<li>テーブルサイズ・インデックスサイズ推移</li>
<li><code>age(relfrozenxid)</code>（XID 消費進行）</li>
</ol>
<p>確認クエリ例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  schemaname,
</span></span><span style="display:flex;"><span>  relname,
</span></span><span style="display:flex;"><span>  n_live_tup,
</span></span><span style="display:flex;"><span>  n_dead_tup,
</span></span><span style="display:flex;"><span>  last_autovacuum,
</span></span><span style="display:flex;"><span>  last_vacuum
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_user_tables
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> n_dead_tup <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">20</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>XID の健全性チェック:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  datname,
</span></span><span style="display:flex;"><span>  age(datfrozenxid) <span style="color:#66d9ef">AS</span> xid_age
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_database
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> xid_age <span style="color:#66d9ef">DESC</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>xid_age</code> が高いのに vacuum が遅れている場合は、緊急度が高いです。</p>
<h2 id="3-autovacuum-の基本パラメータ設計">3. Autovacuum の基本パラメータ設計</h2>
<p>デフォルト設定は小規模環境向けで、更新量が多い本番には不足しやすいです。まずは「全体設定 + ホットテーブル個別設定」に分けて調整します。</p>
<p>代表的パラメータ:</p>
<ul>
<li><code>autovacuum_max_workers</code></li>
<li><code>autovacuum_naptime</code></li>
<li><code>autovacuum_vacuum_cost_limit</code></li>
<li><code>autovacuum_vacuum_scale_factor</code></li>
<li><code>autovacuum_vacuum_threshold</code></li>
</ul>
<p>考え方:</p>
<ul>
<li>更新頻度が高いテーブルは <code>scale_factor</code> を下げる（例: 0.2 → 0.02）</li>
<li>小さなテーブルは threshold 主体、大きなテーブルは scale factor 主体</li>
<li>まず vacuum が「間に合う」状態を作る</li>
</ul>
<p>個別テーブル例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> events
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">SET</span> (
</span></span><span style="display:flex;"><span>    autovacuum_vacuum_scale_factor <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>.<span style="color:#ae81ff">01</span>,
</span></span><span style="display:flex;"><span>    autovacuum_vacuum_threshold <span style="color:#f92672">=</span> <span style="color:#ae81ff">5000</span>,
</span></span><span style="display:flex;"><span>    autovacuum_analyze_scale_factor <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>.<span style="color:#ae81ff">02</span>
</span></span><span style="display:flex;"><span>  );
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="4-インデックス肥大化の見落としに注意">4. インデックス肥大化の見落としに注意</h2>
<p>テーブル側だけ見ていて、実際のボトルネックがインデックス側というケースは非常に多いです。特に更新頻度の高い B-Tree インデックスで顕著です。</p>
<p>実務では次を定期確認します。</p>
<ul>
<li>使用頻度が低い巨大インデックス</li>
<li>重複インデックス</li>
<li>インデックスサイズ増加率（週次）</li>
</ul>
<p>重複候補を探す SQL（簡易）:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  indexrelid::regclass <span style="color:#66d9ef">AS</span> index_name,
</span></span><span style="display:flex;"><span>  indrelid::regclass <span style="color:#66d9ef">AS</span> <span style="color:#66d9ef">table_name</span>,
</span></span><span style="display:flex;"><span>  pg_get_indexdef(indexrelid)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_index
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> indisvalid <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>実際は <code>pg_stat_user_indexes</code> と組み合わせ、<code>idx_scan</code> がほぼゼロのものを優先削減します。</p>
<h2 id="5-reindex-と-pg_repack-の使い分け">5. REINDEX と pg_repack の使い分け</h2>
<p>肥大化したインデックスを戻すには <code>REINDEX</code> が基本ですが、ロック影響を避けたい場合は <code>REINDEX CONCURRENTLY</code> を選びます。</p>
<ul>
<li>影響小で安全重視: <code>REINDEX INDEX CONCURRENTLY</code></li>
<li>まとめて再編成: <code>pg_repack</code>（導入・権限管理が必要）</li>
</ul>
<p>例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">REINDEX</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_orders_created_at;
</span></span></code></pre></td></tr></table>
</div>
</div><p>注意点:</p>
<ul>
<li>ディスク空き容量を事前確認（再構築時に追加領域が必要）</li>
<li>長時間トランザクションがあると完了しない</li>
<li>実行ウィンドウを決め、監視を付ける</li>
</ul>
<h2 id="6-vacuum-が進まない時の切り分け">6. vacuum が進まない時の切り分け</h2>
<p>「Autovacuum が動いているのに改善しない」時は、次の順で確認します。</p>
<ol>
<li>長時間トランザクションが残っていないか</li>
<li>レプリカ遅延や hot_standby_feedback で cleanup が妨げられていないか</li>
<li>I/O 飽和で vacuum が極端に遅くなっていないか</li>
<li>freeze 対象の backlog が巨大化していないか</li>
</ol>
<p>長時間トランザクション確認:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> pid, usename, <span style="color:#66d9ef">state</span>, xact_start, now() <span style="color:#f92672">-</span> xact_start <span style="color:#66d9ef">AS</span> tx_age, query
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_activity
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> xact_start <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> xact_start <span style="color:#66d9ef">ASC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">20</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>tx_age</code> が長い接続は、vacuum の前進を阻害する最優先要因です。</p>
<h2 id="7-実運用で効くスケジュール設計">7. 実運用で効くスケジュール設計</h2>
<p>本番では「毎晩まとめて重い処理」より、<strong>小さく高頻度に回す</strong>方が安定します。</p>
<ul>
<li>日中: autovacuum でこまめに回収</li>
<li>深夜: 重いテーブルの <code>VACUUM (ANALYZE)</code> を計画実行</li>
<li>週次: 重要インデックスの肥大化確認</li>
<li>月次: 上位肥大化テーブルの再編成計画レビュー</li>
</ul>
<p>ANALYZE を外すとプランが古くなるため、統計更新を一体運用にします。</p>
<h2 id="8-典型インシデントと復旧手順">8. 典型インシデントと復旧手順</h2>
<h3 id="ケースa-api-レイテンシ急上昇">ケースA: API レイテンシ急上昇</h3>
<p>兆候:</p>
<ul>
<li>CPU は高くないがクエリ時間が増加</li>
<li>特定テーブルの <code>n_dead_tup</code> が急増</li>
</ul>
<p>対処:</p>
<ol>
<li>長時間トランザクションを特定</li>
<li>対象テーブルに <code>VACUUM (VERBOSE, ANALYZE)</code></li>
<li>重度ならインデックス再構築を計画</li>
</ol>
<h3 id="ケースb-ストレージ逼迫">ケースB: ストレージ逼迫</h3>
<p>兆候:</p>
<ul>
<li>disk 使用率が短期間で増加</li>
<li>UPDATE 多発テーブルが存在</li>
</ul>
<p>対処:</p>
<ol>
<li>サイズ上位テーブル・インデックスを抽出</li>
<li>不要インデックス削除</li>
<li><code>REINDEX CONCURRENTLY</code> / <code>pg_repack</code> を段階実行</li>
</ol>
<h3 id="ケースc-wraparound-警告">ケースC: wraparound 警告</h3>
<p>兆候:</p>
<ul>
<li><code>autovacuum: preventing wraparound</code> ログ</li>
</ul>
<p>対処:</p>
<ol>
<li>緊急度を最優先に切替</li>
<li>長時間 TX を停止</li>
<li>freeze 対象テーブルを優先 vacuum</li>
</ol>
<h2 id="9-導入時チェックリスト">9. 導入時チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 上位更新テーブルに個別 autovacuum パラメータがある</li>
<li><input disabled="" type="checkbox"> <code>n_dead_tup</code> と <code>last_autovacuum</code> を監視している</li>
<li><input disabled="" type="checkbox"> 長時間トランザクションのアラートがある</li>
<li><input disabled="" type="checkbox"> インデックス使用率 (<code>idx_scan</code>) を定期レビューしている</li>
<li><input disabled="" type="checkbox"> REINDEX 実行時の空き容量基準を定義している</li>
<li><input disabled="" type="checkbox"> wraparound 対応 runbook がある</li>
</ul>
<h2 id="10-30日改善プラン最短で効果を出す">10. 30日改善プラン（最短で効果を出す）</h2>
<h3 id="week-1">Week 1</h3>
<ul>
<li>現状計測（dead tuple、サイズ、xid age）</li>
<li>ホットテーブル上位10件を特定</li>
</ul>
<h3 id="week-2">Week 2</h3>
<ul>
<li>テーブルごとに autovacuum 個別設定</li>
<li>長時間 TX 監視アラート導入</li>
</ul>
<h3 id="week-3">Week 3</h3>
<ul>
<li>低利用/重複インデックス整理</li>
<li>対象インデックスを <code>REINDEX CONCURRENTLY</code></li>
</ul>
<h3 id="week-4">Week 4</h3>
<ul>
<li>実行後の p95 クエリ時間、ストレージ増加率を比較</li>
<li>設定の再チューニングと runbook 更新</li>
</ul>
<p>PostgreSQL の肥大化対策は、一発のメンテで終わる作業ではありません。<strong>観測 → 個別設定 → 段階的再編成 → 監視改善</strong>を繰り返すことで、停止なしでも安定して性能を維持できます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Performance</category>
      <category>SRE</category>
      <category>運用</category>
    </item>
    <item>
      <title>Kubernetesキャパシティ設計実践：HPA/VPA/Cluster Autoscalerを衝突させない運用術</title>
      <link>https://www.ai2core.com/posts/2026-03-03-kubernetes-hpa-vpa-capacity-tuning/</link>
      <pubDate>Tue, 03 Mar 2026 09:10:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-03-kubernetes-hpa-vpa-capacity-tuning/</guid>
      <description>Kubernetesのスケーリング機構を本番で安定運用するための設計、メトリクス選定、調整手順、障害時の確認ポイントを解説。</description>
      <content:encoded><![CDATA[<h1 id="kubernetesキャパシティ設計実践hpavpacluster-autoscalerを衝突させない運用術">Kubernetesキャパシティ設計実践：HPA/VPA/Cluster Autoscalerを衝突させない運用術</h1>
<p>Kubernetes は「自動でスケールするから安心」と思われがちですが、実運用では逆です。HPA、VPA、Cluster Autoscaler（CA）の設定が噛み合わないと、スケールアウトと再スケジューリングが衝突し、レイテンシ悪化やコスト増大を引き起こします。</p>
<p>本記事では、3つのオートスケーリング機構を同時運用する際の設計ポイントを、障害対応目線で整理します。</p>
<h2 id="1-役割分担を明確にする">1. 役割分担を明確にする</h2>
<p>まず前提として、各コンポーネントの責務を固定します。</p>
<ul>
<li>HPA: Pod 数を短期的に増減</li>
<li>VPA: Pod あたりの requests/limits を中長期で最適化</li>
<li>CA: ノード数を増減</li>
</ul>
<p>この役割分担が曖昧だと、同じ問題を複数レイヤーで同時に解こうとして不安定化します。特に Web/API ワークロードでは、<strong>HPA を主軸、VPA は recommendation 中心</strong>で始めるのが安全です。</p>
<h2 id="2-requestslimits-が崩れていると全て失敗する">2. requests/limits が崩れていると全て失敗する</h2>
<p>HPA の CPU 指標は requests 基準で計算されます。requests が不正確だと、HPA の判断もズレます。最初にやるべきは次です。</p>
<ol>
<li>過去 2 週間の実使用量を可視化</li>
<li>p95 使用量を requests の初期値に設定</li>
<li>limits は requests の 1.5〜2 倍で開始</li>
</ol>
<p>極端に低い requests は「見かけの高負荷」を作り、不要スケールを誘発します。逆に高すぎる requests は CA の過剰増設を招きます。</p>
<h2 id="3-hpa-指標選定の実践">3. HPA 指標選定の実践</h2>
<p>CPU だけで運用すると、I/O 待ちや外部 API 待ちのボトルネックを見逃します。推奨は複合指標です。</p>
<ul>
<li>CPU Utilization（基本）</li>
<li>メモリ使用率（リーク監視）</li>
<li>RPS あたりレイテンシ（SLO 接続）</li>
<li>Queue 長（非同期処理）</li>
</ul>
<p><code>autoscaling/v2</code> では複数メトリクスを扱えるため、最初から設計しておくと後で楽です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">autoscaling/v2</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">HorizontalPodAutoscaler</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-hpa</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">scaleTargetRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">minReplicas</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">maxReplicas</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">metrics</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Resource</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">resource</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">cpu</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">target</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Utilization</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">averageUtilization</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Pods</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pods</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">metric</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">http_requests_per_second</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">target</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">type</span>: <span style="color:#ae81ff">AverageValue</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">averageValue</span>: <span style="color:#e6db74">&#34;80&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="4-スケール挙動を安定化する">4. スケール挙動を安定化する</h2>
<p>HPA は設定次第で「増えすぎ・減りすぎ」を起こします。<code>behavior</code> を入れて振動を抑えます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">behavior</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">scaleUp</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">stabilizationWindowSeconds</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">policies</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Percent</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">value</span>: <span style="color:#ae81ff">100</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">periodSeconds</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">scaleDown</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">stabilizationWindowSeconds</span>: <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">policies</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Percent</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">value</span>: <span style="color:#ae81ff">20</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">periodSeconds</span>: <span style="color:#ae81ff">60</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>スケールアップは素早く、スケールダウンは慎重に、が基本です。障害時の復旧速度と平常時コストのバランスを取りやすくなります。</p>
<h2 id="5-vpa-の安全な導入順序">5. VPA の安全な導入順序</h2>
<p>VPA をいきなり <code>Auto</code> で入れると、Pod 再作成が頻発し、ピーク時間帯に影響することがあります。次の順序を推奨します。</p>
<ol>
<li><code>Off</code> で recommendation だけ収集</li>
<li>2〜4週間データ蓄積</li>
<li>非クリティカルなバッチ系から <code>Auto</code> 開始</li>
<li>API 系は <code>Initial</code> または手動反映を継続</li>
</ol>
<p>この段階導入にすると、VPA が既存 SLO を壊すリスクを抑えられます。</p>
<h2 id="6-hpa-と-vpa-の衝突回避">6. HPA と VPA の衝突回避</h2>
<p>同一 Deployment で CPU/Memory の両方を HPA と VPA が同時に強く制御すると、調整ループが競合します。現場では次の運用が現実的です。</p>
<ul>
<li>HPA: レプリカ数を制御（主）</li>
<li>VPA: requests 最適化（従）</li>
<li>クリティカルサービスは VPA recommendation を人間レビューして反映</li>
</ul>
<p>また、メモリで OOM が起こるサービスは、HPA より先にアプリ側リーク調査を優先します。スケールで隠すと後で必ず再発します。</p>
<h2 id="7-cluster-autoscaler-の盲点">7. Cluster Autoscaler の盲点</h2>
<p>CA は Pending Pod を見てノードを増やしますが、以下の条件で期待通り動きません。</p>
<ul>
<li>PDB が厳しすぎて eviction できない</li>
<li>NodeSelector/taint 制約で配置先がない</li>
<li>requests が過大で BinPacking 不可能</li>
</ul>
<p>スケールしない時の確認手順を runbook 化しておくと、夜間障害で迷いません。</p>
<ol>
<li>Pending Pod の events を確認</li>
<li>CA logs で scale-up decision を確認</li>
<li>node group の上限値と quota を確認</li>
<li>Pod 制約（affinity/taint/tolerations）を確認</li>
</ol>
<h2 id="8-典型障害シナリオと対処">8. 典型障害シナリオと対処</h2>
<h3 id="シナリオa-突発トラフィックで-5xx-増加">シナリオA: 突発トラフィックで 5xx 増加</h3>
<ul>
<li>兆候: CPU 90%超、pod 起動待ち</li>
<li>対処: HPA minReplicas を一時引き上げ、イメージ pull 最適化、readiness 調整</li>
<li>恒久策: 予測ピーク前の scheduled scaling を導入</li>
</ul>
<h3 id="シナリオb-コスト急増">シナリオB: コスト急増</h3>
<ul>
<li>兆候: ノード数だけ増えて利用率低い</li>
<li>対処: requests 見直し、scaleDown stabilization 調整、CA consolidate 有効化</li>
<li>恒久策: ワークロード別の node pool 分離</li>
</ul>
<h3 id="シナリオc-断続的なタイムアウト">シナリオC: 断続的なタイムアウト</h3>
<ul>
<li>兆候: CPU 余裕あり、レイテンシ悪化</li>
<li>対処: 外部依存（DB/Redis/API）をトレースで確認</li>
<li>恒久策: HPA 指標に queue 長・レイテンシを追加</li>
</ul>
<h2 id="9-実運用のメトリクスセット">9. 実運用のメトリクスセット</h2>
<p>運用ダッシュボードには、最低限次を置きます。</p>
<ul>
<li>HPA current/desired replicas</li>
<li>Pod 起動時間（image pull + readiness）</li>
<li>Pod Pending 数と理由</li>
<li>Node 利用率（CPU/Memory）</li>
<li>CA scale up/down イベント数</li>
<li>5xx 率と p95 latency</li>
</ul>
<p>このセットがあると「スケーラが原因か、アプリが原因か」を 5 分以内で切り分けられます。</p>
<h2 id="10-段階的チューニング手順4週間">10. 段階的チューニング手順（4週間）</h2>
<h3 id="week-1-ベースライン計測">Week 1: ベースライン計測</h3>
<ul>
<li>現行 requests/limits と実使用量を比較</li>
<li>HPA 閾値を仮設定（CPU 60%）</li>
</ul>
<h3 id="week-2-振動抑制">Week 2: 振動抑制</h3>
<ul>
<li>behavior 追加</li>
<li>scaleDown window を 300s 前後で調整</li>
</ul>
<h3 id="week-3-指標追加">Week 3: 指標追加</h3>
<ul>
<li>RPS/queue 指標を導入</li>
<li>アラートを SLO 連動へ変更</li>
</ul>
<h3 id="week-4-コスト最適化">Week 4: コスト最適化</h3>
<ul>
<li>idle 時の minReplicas 見直し</li>
<li>node pool の統廃合</li>
</ul>
<p>この 4 週間を回すだけでも、無駄スケールと障害時復旧の両方が改善します。</p>
<h2 id="11-導入チェックリスト">11. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> requests/limits が実使用量に基づいている</li>
<li><input disabled="" type="checkbox"> HPA に <code>behavior</code> を設定済み</li>
<li><input disabled="" type="checkbox"> VPA は recommendation 期間を経て適用している</li>
<li><input disabled="" type="checkbox"> CA の上限値・quota・制約を定期点検している</li>
<li><input disabled="" type="checkbox"> SLO 指標とスケーリング指標を接続している</li>
<li><input disabled="" type="checkbox"> 典型障害シナリオの runbook がある</li>
</ul>
<p>Kubernetes のスケーリングは、機能を有効にするだけでは安定しません。役割分担、指標設計、調整ループの制御、そして runbook をセットで整えることで、初めて「自動化が味方になる」状態を作れます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>HPA</category>
      <category>VPA</category>
      <category>SRE</category>
      <category>Platform Engineering</category>
    </item>
    <item>
      <title>OpenTelemetry実践導入ガイド：ログ・メトリクス・トレース統合を90日で定着させる</title>
      <link>https://www.ai2core.com/posts/2026-03-03-opentelemetry-practical-observability-guide/</link>
      <pubDate>Tue, 03 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-03-opentelemetry-practical-observability-guide/</guid>
      <description>OpenTelemetryを現場導入する際の設計、サンプリング、Collector構成、SLO連携、運用改善の具体手順を解説。</description>
      <content:encoded><![CDATA[<h1 id="opentelemetry実践導入ガイドログメトリクストレース統合を90日で定着させる">OpenTelemetry実践導入ガイド：ログ・メトリクス・トレース統合を90日で定着させる</h1>
<p>「監視は入れているのに障害原因の特定が遅い」。この状態は、たいていデータが足りないのではなく、<strong>データが分断されている</strong>ことが原因です。メトリクスは見える、ログは別画面、トレースは導入途中、という構成だと、オンコールは毎回同じ調査を手作業で繰り返すことになります。</p>
<p>OpenTelemetry（OTel）はこの分断を減らすための共通規格です。ただし、導入に失敗するチームも少なくありません。理由は単純で、「計測の追加」だけやって「運用設計」を後回しにするからです。</p>
<p>本記事では、OpenTelemetry を 90 日で現場定着させるための、実務寄りの導入手順を紹介します。</p>
<h2 id="1-まず決めるべき運用目標">1. まず決めるべき運用目標</h2>
<p>OTel を入れる前に、次の問いに答えます。</p>
<ul>
<li>どの障害をどれだけ早く見つけたいか</li>
<li>どのサービスの MTTR をどれだけ下げたいか</li>
<li>どのチームがトリアージ責任を持つか</li>
</ul>
<p>たとえば「API 5xx の原因調査を 60 分 → 15 分に短縮する」と明文化すると、必要な計測が決まります。逆に目標がないと、span を増やす作業が目的化して終わります。</p>
<h2 id="2-参照アーキテクチャ">2. 参照アーキテクチャ</h2>
<p>本番で扱いやすい最小構成は次です。</p>
<ol>
<li>アプリケーションに OTel SDK を導入</li>
<li>エージェント/サイドカー経由で OTel Collector に送信</li>
<li>Collector で加工・サンプリング・ルーティング</li>
<li>Prometheus / Loki / Tempo（または商用基盤）へ出力</li>
</ol>
<p>Collector を中継に置く理由は、アプリ側の再デプロイなしでルール変更できるからです。運用現場ではここが非常に効きます。</p>
<h2 id="3-サービス命名規則を最初に固定する">3. サービス命名規則を最初に固定する</h2>
<p>命名規則を後で直すと、ダッシュボードとアラートが壊れます。以下は最低限のルール例です。</p>
<ul>
<li><code>service.name</code>: <code>domain-service-env</code>（例: <code>billing-api-prod</code>）</li>
<li><code>deployment.environment</code>: <code>prod|stg|dev</code></li>
<li><code>service.version</code>: Git SHA または semver</li>
<li><code>cloud.region</code>: 実リージョン名</li>
</ul>
<p>この 4 つが揃うと、障害時に「どの環境・どのバージョン」が悪いか一気に絞れます。</p>
<h2 id="4-pythonサービス計測の実装例">4. Pythonサービス計測の実装例</h2>
<p>FastAPI を例に、最小導入手順を示します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install opentelemetry-distro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  opentelemetry-exporter-otlp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  opentelemetry-instrumentation-fastapi <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  opentelemetry-instrumentation-requests
</span></span></code></pre></td></tr></table>
</div>
</div><p>起動時に auto-instrumentation を有効化します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export OTEL_SERVICE_NAME<span style="color:#f92672">=</span>billing-api-prod
</span></span><span style="display:flex;"><span>export OTEL_EXPORTER_OTLP_ENDPOINT<span style="color:#f92672">=</span>http://otel-collector:4318
</span></span><span style="display:flex;"><span>export OTEL_EXPORTER_OTLP_PROTOCOL<span style="color:#f92672">=</span>http/protobuf
</span></span><span style="display:flex;"><span>export OTEL_TRACES_SAMPLER<span style="color:#f92672">=</span>parentbased_traceidratio
</span></span><span style="display:flex;"><span>export OTEL_TRACES_SAMPLER_ARG<span style="color:#f92672">=</span>0.1
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>opentelemetry-instrument uvicorn app.main:app --host 0.0.0.0 --port <span style="color:#ae81ff">8080</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>次に、業務的に重要な処理へ custom span を追加します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry <span style="color:#f92672">import</span> trace
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>tracer <span style="color:#f92672">=</span> trace<span style="color:#f92672">.</span>get_tracer(__name__)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">charge_customer</span>(order_id: str, customer_id: str, amount: int):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> tracer<span style="color:#f92672">.</span>start_as_current_span(<span style="color:#e6db74">&#34;billing.charge&#34;</span>) <span style="color:#66d9ef">as</span> span:
</span></span><span style="display:flex;"><span>        span<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;order.id&#34;</span>, order_id)
</span></span><span style="display:flex;"><span>        span<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;customer.id&#34;</span>, customer_id)
</span></span><span style="display:flex;"><span>        span<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;payment.amount&#34;</span>, amount)
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># 決済処理</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>HTTP 自動計測だけでは業務上のボトルネックが見えません。注文IDや課金金額など、<strong>調査に必要な属性</strong>を入れることが重要です。</p>
<h2 id="5-collector設定の実践パターン">5. Collector設定の実践パターン</h2>
<p>Collector の典型構成は次です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">receivers</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">otlp</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">protocols</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">grpc</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">http</span>:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">processors</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">memory_limiter</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">check_interval</span>: <span style="color:#ae81ff">1s</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">limit_mib</span>: <span style="color:#ae81ff">512</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">batch</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">send_batch_size</span>: <span style="color:#ae81ff">1024</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">5s</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">resource</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">attributes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">deployment.environment</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">value</span>: <span style="color:#ae81ff">prod</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">action</span>: <span style="color:#ae81ff">upsert</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">exporters</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">otlp/tempo</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">endpoint</span>: <span style="color:#ae81ff">tempo:4317</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">tls</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insecure</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">prometheusremotewrite</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">endpoint</span>: <span style="color:#ae81ff">http://mimir:9009/api/v1/push</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pipelines</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">traces</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">receivers</span>: [<span style="color:#ae81ff">otlp]</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">processors</span>: [<span style="color:#ae81ff">memory_limiter, batch, resource]</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">exporters</span>: [<span style="color:#ae81ff">otlp/tempo]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metrics</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">receivers</span>: [<span style="color:#ae81ff">otlp]</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">processors</span>: [<span style="color:#ae81ff">memory_limiter, batch, resource]</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">exporters</span>: [<span style="color:#ae81ff">prometheusremotewrite]</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは <code>memory_limiter</code> と <code>batch</code> を必ず入れることです。高負荷時の Collector 落ちを防げます。</p>
<h2 id="6-サンプリング戦略">6. サンプリング戦略</h2>
<p>全トレースを保存すると費用が跳ねます。まずは次の段階運用が現実的です。</p>
<ul>
<li>フェーズ1: 10% head sampling</li>
<li>フェーズ2: エラーは 100%、正常は 5%</li>
<li>フェーズ3: 高価値APIのみ 20%、その他 1%</li>
</ul>
<p>さらに Collector で tail sampling を使うと、異常リクエストを優先的に残せます。導入初期は head sampling だけでも十分ですが、障害解析を重視するなら tail sampling への移行を計画に含めます。</p>
<h2 id="7-ログとの相関を必ず作る">7. ログとの相関を必ず作る</h2>
<p>トレースだけ可視化しても、最終的にはログを見ます。ログに trace_id と span_id を埋め込み、画面から相互ジャンプできるようにします。</p>
<p>Python の標準 logging なら、フォーマッタに <code>trace_id</code> を追加するだけで改善します。アプリ基盤に共通 formatter を置いて強制するのが現実的です。</p>
<h2 id="8-sloアラート設計の接続">8. SLO/アラート設計の接続</h2>
<p>OTel 導入の価値は、可視化だけでなく SLO 運用に繋がる点です。例:</p>
<ul>
<li>SLI: <code>http.server.duration</code> の p95</li>
<li>SLI: <code>http.server.request.count</code> に対する 5xx 率</li>
<li>エラーバジェット消費率が閾値超過でページ</li>
</ul>
<p>ここで重要なのは、アラート本文に「関連トレースのリンク」を入れることです。オンコールが即座に原因調査へ入れます。</p>
<h2 id="9-90日導入ロードマップ">9. 90日導入ロードマップ</h2>
<h3 id="day-1-14-土台づくり">Day 1-14: 土台づくり</h3>
<ul>
<li>命名規則決定</li>
<li>Collector を冗長構成で配置</li>
<li>主要 2 サービスへ SDK 導入</li>
</ul>
<h3 id="day-15-45-運用化">Day 15-45: 運用化</h3>
<ul>
<li>主要 API の custom span 追加</li>
<li>ダッシュボード標準化</li>
<li>エラー系アラートにトレースリンクを追加</li>
</ul>
<h3 id="day-46-90-最適化">Day 46-90: 最適化</h3>
<ul>
<li>サンプリング改善（費用最適化）</li>
<li>ノイズアラート削減</li>
<li>週次レビューで MTTR 変化を追跡</li>
</ul>
<p>この 90 日で「導入しただけ」から「使って解決できる」状態に変わります。</p>
<h2 id="10-よくある失敗と対策">10. よくある失敗と対策</h2>
<h3 id="失敗1-span-を増やしすぎて遅くなる">失敗1: span を増やしすぎて遅くなる</h3>
<p>対策: 重要フローに限定し、属性も最小から始める。</p>
<h3 id="失敗2-pii-を属性に入れてしまう">失敗2: PII を属性に入れてしまう</h3>
<p>対策: Collector でマスキング、アプリ側で禁止 lint を導入。</p>
<h3 id="失敗3-ダッシュボードがチームごとにバラバラ">失敗3: ダッシュボードがチームごとにバラバラ</h3>
<p>対策: プラットフォーム側で共通テンプレートを配布し、必須指標を統一。</p>
<h2 id="11-導入チェックリスト">11. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> <code>service.name</code> / <code>deployment.environment</code> / <code>service.version</code> が揃っている</li>
<li><input disabled="" type="checkbox"> Collector に <code>memory_limiter</code> と <code>batch</code> が入っている</li>
<li><input disabled="" type="checkbox"> トレースとログが <code>trace_id</code> で相関できる</li>
<li><input disabled="" type="checkbox"> エラー率アラートからトレースへ遷移できる</li>
<li><input disabled="" type="checkbox"> サンプリング率と月次コストをレビューしている</li>
<li><input disabled="" type="checkbox"> 主要障害のポストモーテムで観測改善が反映される</li>
</ul>
<p>OpenTelemetry は魔法のツールではありません。しかし、計測規約と運用フローをセットで設計すると、障害対応の速度と質は確実に上がります。まずは 2 サービスで勝ち筋を作り、そこから全体展開するのが最短ルートです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>OpenTelemetry</category>
      <category>Observability</category>
      <category>SRE</category>
      <category>Grafana</category>
      <category>Prometheus</category>
    </item>
    <item>
      <title>Terraformドリフト検知プレイブック：本番事故を防ぐCI設計と運用手順</title>
      <link>https://www.ai2core.com/posts/2026-03-03-terraform-drift-detection-playbook/</link>
      <pubDate>Tue, 03 Mar 2026 09:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-03-terraform-drift-detection-playbook/</guid>
      <description>Terraformのドリフト検知を実運用に載せるための設計指針、GitHub Actions実装例、誤検知の減らし方、復旧フローまで具体的に解説。</description>
      <content:encoded><![CDATA[<h1 id="terraformドリフト検知プレイブック本番事故を防ぐci設計と運用手順">Terraformドリフト検知プレイブック：本番事故を防ぐCI設計と運用手順</h1>
<p>Terraform を導入していても、運用が進むほど「実環境がいつの間にかコードとズレる」問題にぶつかります。いわゆるドリフトです。最初は小さな差分でも、放置すると本番変更時に予期せぬ差分が混ざり、障害やリリース遅延の原因になります。</p>
<p>本記事では、Terraform ドリフト検知を単なる <code>terraform plan</code> 実行で終わらせず、<strong>継続運用できる仕組み</strong>として実装するための具体策をまとめます。対象は AWS を例にしますが、考え方は他クラウドでも共通です。</p>
<h2 id="1-ドリフト検知で最初に決めるべきこと">1. ドリフト検知で最初に決めるべきこと</h2>
<p>多くのチームが失敗するのは、実装前に運用設計を決めないことです。まず以下を決めます。</p>
<ol>
<li>どの環境をいつ検知するか（prod は毎日、stg は平日など）</li>
<li>検知結果をどこに通知するか（Slack/Discord/Issue）</li>
<li>誰がいつまでに対応するか（当番制、SLA）</li>
<li>「意図した手動変更」をどう扱うか（例外ラベル、期限付き）</li>
</ol>
<p>ここを決めずに CI だけ作ると、通知がノイズ化して無視されます。ドリフト検知は技術課題より運用課題です。</p>
<h2 id="2-リポジトリ構成と-state-分離">2. リポジトリ構成と state 分離</h2>
<p>最小限、次のような構成を推奨します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>infra/
</span></span><span style="display:flex;"><span>  modules/
</span></span><span style="display:flex;"><span>    vpc/
</span></span><span style="display:flex;"><span>    ecs/
</span></span><span style="display:flex;"><span>    rds/
</span></span><span style="display:flex;"><span>  envs/
</span></span><span style="display:flex;"><span>    prod/
</span></span><span style="display:flex;"><span>      main.tf
</span></span><span style="display:flex;"><span>      backend.hcl
</span></span><span style="display:flex;"><span>      variables.tf
</span></span><span style="display:flex;"><span>    stg/
</span></span><span style="display:flex;"><span>      main.tf
</span></span><span style="display:flex;"><span>      backend.hcl
</span></span><span style="display:flex;"><span>.github/
</span></span><span style="display:flex;"><span>  workflows/
</span></span><span style="display:flex;"><span>    terraform-drift.yml
</span></span></code></pre></td></tr></table>
</div>
</div><p>環境ごとに backend と state を分けることが重要です。ドリフト検知ジョブが state を誤って参照すると、存在しない差分が出ます。S3 backend + DynamoDB lock を使う場合は、<code>bucket/key/region/table</code> の整合性を必ず固定化します。</p>
<h2 id="3-ci-での検知フローgithub-actions">3. CI での検知フロー（GitHub Actions）</h2>
<p>以下は、毎朝実行してドリフトを検知する最小構成です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">terraform-drift</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">schedule</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">cron</span>: <span style="color:#e6db74">&#34;0 22 * * *&#34;</span> <span style="color:#75715e"># JST 07:00</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">drift-prod</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">defaults</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">run</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">working-directory</span>: <span style="color:#ae81ff">infra/envs/prod</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">id-token</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">contents</span>: <span style="color:#ae81ff">read</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">hashicorp/setup-terraform@v3</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">terraform_version</span>: <span style="color:#ae81ff">1.9.8</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configure AWS credentials (OIDC)</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">aws-actions/configure-aws-credentials@v4</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">role-to-assume</span>: <span style="color:#ae81ff">arn:aws:iam::123456789012:role/github-terraform-readonly</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">aws-region</span>: <span style="color:#ae81ff">ap-northeast-1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Init</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">terraform init -backend-config=backend.hcl</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Drift check</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">terraform plan -detailed-exitcode -out=tfplan</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>-detailed-exitcode</code> は実務で必須です。終了コードの意味は以下です。</p>
<ul>
<li>0: 差分なし</li>
<li>1: エラー</li>
<li>2: 差分あり（ドリフト含む）</li>
</ul>
<p>これを使えば、差分ありと失敗を明確に分けられます。</p>
<h2 id="4-終了コードを正しく扱う">4. 終了コードを正しく扱う</h2>
<p>Actions で単純に <code>terraform plan</code> を実行すると、終了コード 2 が failure 扱いになるため、実際には「検知成功」なのに赤く見えます。以下のように分岐して扱います。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>set +e
</span></span><span style="display:flex;"><span>terraform plan -detailed-exitcode -out<span style="color:#f92672">=</span>tfplan
</span></span><span style="display:flex;"><span>code<span style="color:#f92672">=</span>$?
</span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$code<span style="color:#e6db74">&#34;</span> -eq <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;DRIFT=false&#34;</span> &gt;&gt; $GITHUB_ENV
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$code<span style="color:#e6db74">&#34;</span> -eq <span style="color:#ae81ff">2</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;DRIFT=true&#34;</span> &gt;&gt; $GITHUB_ENV
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Terraform plan failed&#34;</span>
</span></span><span style="display:flex;"><span>  exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この実装にすると、ジョブ自体は成功させたまま「ドリフト有無」を後段に渡せます。</p>
<h2 id="5-通知設計差分サマリを人間が読める形にする">5. 通知設計：差分サマリを人間が読める形にする</h2>
<p>ドリフト検知で最も大事なのは、通知が読みやすいことです。plan 全文を貼ると誰も読みません。次の 3 つだけ通知します。</p>
<ol>
<li>どの環境で</li>
<li>何が（resource type + name）</li>
<li>どう変わるか（create/update/delete）</li>
</ol>
<p>例: <code>terraform show -json tfplan</code> を <code>jq</code> で要約</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>terraform show -json tfplan &gt; plan.json
</span></span><span style="display:flex;"><span>jq -r <span style="color:#e6db74">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  .resource_changes[] |
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;- \(.type).\(.name): \(.change.actions | join(&#34;,&#34;))&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;</span> plan.json | head -n <span style="color:#ae81ff">50</span> &gt; summary.txt
</span></span></code></pre></td></tr></table>
</div>
</div><p>通知文は長すぎると切れるため、冒頭 50 件だけに制限し、全文は artifacts に添付する運用が実践的です。</p>
<h2 id="6-誤検知を減らす具体策">6. 誤検知を減らす具体策</h2>
<p>ドリフト検知の信頼性を下げる典型例は次です。</p>
<ul>
<li><code>timestamp</code> 的な値を管理対象にしている</li>
<li>provider バージョン差で毎回微差分が出る</li>
<li>autoscaling 系設定が外部コントローラで更新される</li>
</ul>
<p>対策として、<code>lifecycle { ignore_changes = [...] }</code> を乱用せず、<strong>変化が許容される属性だけ</strong>を限定的に無視します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_ecs_service&#34; &#34;app&#34;</span> {
</span></span><span style="display:flex;"><span>  name            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;app&#34;</span>
</span></span><span style="display:flex;"><span>  cluster         <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_ecs_cluster</span>.<span style="color:#66d9ef">main</span>.<span style="color:#66d9ef">id</span>
</span></span><span style="display:flex;"><span>  task_definition <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_ecs_task_definition</span>.<span style="color:#66d9ef">app</span>.<span style="color:#66d9ef">arn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">lifecycle</span> {
</span></span><span style="display:flex;"><span>    ignore_changes <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">desired_count</span>
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>ignore_changes</code> は便利ですが、広げすぎると本当に危険なドリフトを見逃します。四半期ごとに見直しを入れてください。</p>
<h2 id="7-復旧フロー検知後に迷わない手順">7. 復旧フロー：検知後に迷わない手順</h2>
<p>ドリフトを検知したら、毎回議論しないように Runbook 化します。</p>
<h3 id="ステップa-変更の意図確認">ステップA: 変更の意図確認</h3>
<ul>
<li>直近の障害対応や緊急作業がなかったか</li>
<li>コンソール手動変更のチケットがあるか</li>
</ul>
<h3 id="ステップb-差分分類">ステップB: 差分分類</h3>
<ul>
<li>許容（意図あり）</li>
<li>要コード反映（意図あり、IaC未反映）</li>
<li>要即時修正（意図なし）</li>
</ul>
<h3 id="ステップc-実行方針">ステップC: 実行方針</h3>
<ul>
<li>IaC 正とする場合: コード更新 → PR → apply</li>
<li>実環境正としない場合: 手動変更を戻す、または Terraform apply で整合</li>
</ul>
<p>このフローを当番が 30 分以内に回せるようにすると、検知の価値が跳ね上がります。</p>
<h2 id="8-iam-最小権限の実例">8. IAM 最小権限の実例</h2>
<p>ドリフト検知専用ロールは read-only を原則にします。<code>plan</code> だけなら多くのサービスで読み取り権限で足ります。apply 権限を同じロールに入れると、漏えい時のリスクが大きくなります。</p>
<ul>
<li><code>github-terraform-readonly</code>: drift check 用</li>
<li><code>github-terraform-apply</code>: manual approval 後に限定実行</li>
</ul>
<p>この分離は監査対応でも説明しやすく、セキュリティレビューで通りやすいです。</p>
<h2 id="9-週次レポートで改善を回す">9. 週次レポートで改善を回す</h2>
<p>検知を入れたら終わりではありません。週次で次を見ます。</p>
<ul>
<li>ドリフト検知件数</li>
<li>うち誤検知件数</li>
<li>MTTR（検知から解消まで）</li>
<li>原因カテゴリ（手動運用、自動スケール、設定ミス）</li>
</ul>
<p>誤検知率が 30% を超えるなら通知設計か ignore ポリシーが過剰です。逆に検知ゼロが長すぎる場合は、ジョブ自体が死んでいる可能性もあります。</p>
<h2 id="10-導入チェックリスト">10. 導入チェックリスト</h2>
<p>最後に、導入時のチェックリストを置いておきます。</p>
<ul>
<li><input disabled="" type="checkbox"> env ごとに backend/state 分離済み</li>
<li><input disabled="" type="checkbox"> <code>terraform plan -detailed-exitcode</code> を採用</li>
<li><input disabled="" type="checkbox"> 終了コード 2 を正常系として処理</li>
<li><input disabled="" type="checkbox"> 通知は summary + artifacts の二段構成</li>
<li><input disabled="" type="checkbox"> read-only ロールで drift check を実行</li>
<li><input disabled="" type="checkbox"> 復旧 Runbook を 1 ページ化</li>
<li><input disabled="" type="checkbox"> 週次で誤検知率と MTTR をレビュー</li>
</ul>
<p>Terraform ドリフト検知は、単なる監視ではなく「IaC の信頼性を維持する運用基盤」です。最初から完璧を狙うより、通知品質と対応フローを小さく回して改善する方が、結果的に強い運用になります。</p>
<h2 id="11-すぐ使える運用コマンド当番向け">11. すぐ使える運用コマンド（当番向け）</h2>
<p>最後に、当番が迷わず実行できる最小コマンドをまとめます。どれも「まず状況把握」を優先する順序です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1) 直近のドリフトジョブ確認（GitHub CLI）</span>
</span></span><span style="display:flex;"><span>gh run list --workflow terraform-drift.yml --limit <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2) 失敗/差分あり実行のログ確認</span>
</span></span><span style="display:flex;"><span>gh run view &lt;run-id&gt; --log
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3) 対象環境で再現確認（ローカル）</span>
</span></span><span style="display:flex;"><span>cd infra/envs/prod
</span></span><span style="display:flex;"><span>terraform init -backend-config<span style="color:#f92672">=</span>backend.hcl
</span></span><span style="display:flex;"><span>terraform plan -detailed-exitcode
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 4) 差分要約を作成</span>
</span></span><span style="display:flex;"><span>terraform plan -out<span style="color:#f92672">=</span>tfplan
</span></span><span style="display:flex;"><span>terraform show -json tfplan | jq -r <span style="color:#e6db74">&#39;.resource_changes[] | &#34;- \(.type).\(.name): \(.change.actions | join(&#34;,&#34;))&#34;&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この 4 ステップを運用手順書の先頭に置くだけで、一次対応の速度と品質が安定します。特に「誰が見ても同じ順序で確認できる」状態を作ることが、夜間障害時の認知負荷を大きく下げます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Terraform</category>
      <category>IaC</category>
      <category>DevOps</category>
      <category>SRE</category>
      <category>CI/CD</category>
    </item>
    <item>
      <title>Kubernetes障害対応ランブック実践編：15分で切り分け、60分で復旧するための現場手順</title>
      <link>https://www.ai2core.com/posts/2026-03-01-kubernetes-incident-response-runbook/</link>
      <pubDate>Sun, 01 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-01-kubernetes-incident-response-runbook/</guid>
      <description>Kubernetes本番障害を短時間で切り分け・復旧するための実践ランブック。初動、調査コマンド、復旧判断、再発防止まで具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="kubernetes障害対応ランブック実践編15分で切り分け60分で復旧するための現場手順">Kubernetes障害対応ランブック実践編：15分で切り分け、60分で復旧するための現場手順</h1>
<p>Kubernetes で障害が起きたとき、技術力より先に問われるのは「順序」です。順序が崩れると、正しいコマンドを打っていても復旧が遅れます。逆に、判断フレームが決まっていれば、難しい障害でも被害を最小化できます。</p>
<p>本記事では、実運用で使える障害対応ランブックを、<strong>初動 15 分・復旧 60 分</strong>を目安に具体化します。対象は EKS/GKE/AKS を含む一般的な Kubernetes 本番環境です。</p>
<h2 id="1-最初の5分人と情報の交通整理">1. 最初の5分：人と情報の交通整理</h2>
<p>まずやるべきは技術調査ではなく、交通整理です。</p>
<ul>
<li>インシデントの指揮者（IC）を1人決める</li>
<li>ログ担当、メトリクス担当、アプリ担当を分ける</li>
<li>Slack/Discord の専用チャンネルを作る</li>
<li>5分ごとの時系列ログ（タイムライン）を残す</li>
</ul>
<p>この段階で「誰が何を見ているか」が曖昧だと、同じ調査を3人で繰り返し、誰も復旧判断をしない状態になります。ICは手を動かさず、意思決定に集中するのが基本です。</p>
<h2 id="2-515分影響範囲の特定">2. 5〜15分：影響範囲の特定</h2>
<p>次に「どのユーザーに、どの機能で、何%の影響があるか」を確定します。</p>
<h3 id="2-1-まず全体の健康状態">2-1. まず全体の健康状態</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl get nodes
</span></span><span style="display:flex;"><span>kubectl get pods -A --field-selector<span style="color:#f92672">=</span>status.phase!<span style="color:#f92672">=</span>Running
</span></span><span style="display:flex;"><span>kubectl get events -A --sort-by<span style="color:#f92672">=</span>.lastTimestamp | tail -n <span style="color:#ae81ff">50</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>見るポイント:</p>
<ul>
<li>Node が <code>NotReady</code> になっていないか</li>
<li>特定 namespace だけで <code>CrashLoopBackOff</code> が増えていないか</li>
<li>直近イベントに <code>FailedScheduling</code> や <code>Back-off restarting failed container</code> がないか</li>
</ul>
<h3 id="2-2-slosliから影響を数値化">2-2. SLO/SLIから影響を数値化</h3>
<p>「遅い気がする」ではなく、以下を数値で押さえます。</p>
<ul>
<li>エラー率（5xx / total）</li>
<li>p95 / p99 レイテンシ</li>
<li>リクエスト成功率</li>
<li>影響時間（何時何分から）</li>
</ul>
<p>復旧優先順位は「売上影響」「決済影響」「ログイン影響」で並べると迷いにくいです。</p>
<h2 id="3-典型障害ごとの切り分けテンプレート">3. 典型障害ごとの切り分けテンプレート</h2>
<h3 id="3-1-podが再起動ループする">3-1. Podが再起動ループする</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl describe pod &lt;pod-name&gt; -n &lt;ns&gt;
</span></span><span style="display:flex;"><span>kubectl logs &lt;pod-name&gt; -n &lt;ns&gt; --previous
</span></span></code></pre></td></tr></table>
</div>
</div><p>判断軸:</p>
<ol>
<li><strong>アプリ起因</strong>（例: マイグレーション失敗、環境変数不足）</li>
<li><strong>リソース起因</strong>（OOMKill, CPU スロットリング）</li>
<li><strong>依存先起因</strong>（DB接続失敗、外部APIタイムアウト）</li>
</ol>
<p>OOMKill が見えたら、即座に requests/limits の見直し候補です。緊急回避として replicas を増やすより、まずメモリリークや一時的負荷スパイクの切り分けを優先します。</p>
<h3 id="3-2-podは生きているのに503が増える">3-2. Podは生きているのに503が増える</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl get endpoints -n &lt;ns&gt;
</span></span><span style="display:flex;"><span>kubectl describe svc &lt;service&gt; -n &lt;ns&gt;
</span></span><span style="display:flex;"><span>kubectl get deploy &lt;deploy&gt; -n &lt;ns&gt; -o yaml | grep -n <span style="color:#e6db74">&#34;readinessProbe\|livenessProbe&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>よくある原因:</p>
<ul>
<li>readinessProbe が厳しすぎて endpoint から外れ続ける</li>
<li>依存先劣化で app は起動しているが実処理が詰まる</li>
<li>HPA は増えているが、DB 接続プールが上限で飽和</li>
</ul>
<p>このケースでは「Pod数が多いのにエラーが減らない」ので、アプリ外（DB、キュー、外部API）を疑うのが早いです。</p>
<h3 id="3-3-特定ノードでのみ障害">3-3. 特定ノードでのみ障害</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl describe node &lt;node&gt;
</span></span><span style="display:flex;"><span>kubectl top node
</span></span><span style="display:flex;"><span>kubectl get pods -A -o wide | grep &lt;node&gt;
</span></span></code></pre></td></tr></table>
</div>
</div><p>ノード隔離が必要なら、以下で新規スケジュールを止めます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl cordon &lt;node&gt;
</span></span><span style="display:flex;"><span>kubectl drain &lt;node&gt; --ignore-daemonsets --delete-emptydir-data
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>drain</code> は影響が大きい操作なので、IC の明示承認を挟むのが安全です。</p>
<h2 id="4-復旧アクションの優先順位実務向け">4. 復旧アクションの優先順位（実務向け）</h2>
<p>障害中は「恒久対策」を始めないこと。順番は固定します。</p>
<ol>
<li><strong>被害抑制</strong>: feature flag で重い機能を止める</li>
<li><strong>暫定復旧</strong>: 直前の安定リビジョンへロールバック</li>
<li><strong>性能確保</strong>: replicas/HPA 一時調整、キュー消化速度調整</li>
<li><strong>恒久対策</strong>: 根本修正は障害収束後にPRで実施</li>
</ol>
<h3 id="4-1-ロールバックの型を決めておく">4-1. ロールバックの型を決めておく</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl rollout history deployment/&lt;deploy&gt; -n &lt;ns&gt;
</span></span><span style="display:flex;"><span>kubectl rollout undo deployment/&lt;deploy&gt; -n &lt;ns&gt; --to-revision<span style="color:#f92672">=</span>&lt;rev&gt;
</span></span><span style="display:flex;"><span>kubectl rollout status deployment/&lt;deploy&gt; -n &lt;ns&gt;
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは「戻す判断に迷わない基準」を事前に定義しておくことです。</p>
<ul>
<li>5分間で 5xx &gt; 2% 継続</li>
<li>p95 が通常比 2倍超を 10分継続</li>
<li>ログイン/決済失敗が監視閾値超え</li>
</ul>
<p>基準がないと、誰も責任を取りたくなくて引き延ばしになります。</p>
<h2 id="5-事後対応postmortemで必ず残すべきもの">5. 事後対応（Postmortem）で必ず残すべきもの</h2>
<p>障害対応の価値は、復旧速度だけではありません。再発率を下げて初めて完了です。</p>
<p>最低限残す項目:</p>
<ul>
<li>発生時刻、検知時刻、復旧時刻</li>
<li>ユーザー影響（件数、売上影響）</li>
<li>技術的原因（直接原因 / 背景要因）</li>
<li>うまくいった対応、失敗した判断</li>
<li>再発防止のアクション（担当者・期限付き）</li>
</ul>
<h3 id="5-1-アクションを監視に落とす">5-1. アクションを「監視」に落とす</h3>
<p>「次は気をつける」は効果ゼロです。必ず機械化します。</p>
<p>例:</p>
<ul>
<li><code>CrashLoopBackOff &gt; N件</code> を5分継続で即アラート</li>
<li>DB接続待ち時間の p95 をダッシュボード化</li>
<li>リリース直後30分は自動で高感度監視モード</li>
</ul>
<h2 id="6-そのまま使える障害対応チェックリスト">6. そのまま使える障害対応チェックリスト</h2>
<h3 id="初動チェック015分">初動チェック（0〜15分）</h3>
<ul>
<li><input disabled="" type="checkbox"> IC決定、連絡チャンネル作成</li>
<li><input disabled="" type="checkbox"> 影響範囲（機能・ユーザー・売上）を数値化</li>
<li><input disabled="" type="checkbox"> Node / Pod / Event の全体確認</li>
<li><input disabled="" type="checkbox"> 直近デプロイ有無の確認</li>
</ul>
<h3 id="復旧チェック1560分">復旧チェック（15〜60分）</h3>
<ul>
<li><input disabled="" type="checkbox"> 被害抑制（feature flag / トラフィック制御）</li>
<li><input disabled="" type="checkbox"> ロールバック判断基準を満たすか確認</li>
<li><input disabled="" type="checkbox"> 復旧後 15 分の再監視（再発確認）</li>
<li><input disabled="" type="checkbox"> ユーザー向けステータス更新</li>
</ul>
<h3 id="事後チェック24時間以内">事後チェック（24時間以内）</h3>
<ul>
<li><input disabled="" type="checkbox"> Postmortem 作成</li>
<li><input disabled="" type="checkbox"> 再発防止チケット発行</li>
<li><input disabled="" type="checkbox"> 監視ルール・Runbook 更新</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>Kubernetes 障害対応は、個人技で勝つゲームではありません。<strong>誰でも同じ順序で動ける運用設計</strong>が最も効きます。特に「初動の交通整理」「ロールバック基準の明文化」「事後の監視自動化」の3点は、復旧時間と再発率を同時に改善します。</p>
<p>次の障害が来たときに迷わないよう、今日のうちにこのランブックを自チーム用に1ページ化しておくことを強くおすすめします。</p>
<h2 id="7-障害を早く終わらせるための役割分担テンプレート">7. 障害を早く終わらせるための役割分担テンプレート</h2>
<p>実際の現場では、技術調査よりコミュニケーションで詰まることが多いです。そこで、あらかじめ役割を固定しておくと復旧が速くなります。</p>
<ul>
<li><strong>IC（Incident Commander）</strong>: 優先順位決定、Go/No-Go判断、対外連絡承認</li>
<li><strong>Ops Lead</strong>: クラスタ状態確認、ロールバック実行、インフラ変更の安全確認</li>
<li><strong>App Lead</strong>: アプリログ解析、機能フラグ切り替え、暫定パッチ判断</li>
<li><strong>Comms</strong>: 社内・顧客向けステータス更新、問い合わせ窓口統一</li>
</ul>
<p>この分担があると、「調査担当が勝手に危険操作を実施する」「連絡が多重化して混乱する」といった二次被害を防げます。</p>
<h2 id="8-よくあるアンチパターンと回避策">8. よくあるアンチパターンと回避策</h2>
<h3 id="アンチパターン1-まず再デプロイしてしまう">アンチパターン1: まず再デプロイしてしまう</h3>
<p>原因が不明なまま再デプロイすると、証拠ログを失い再現不能になります。最低限 <code>events</code> と <code>previous logs</code> を保存してから操作します。</p>
<h3 id="アンチパターン2-たぶん直ったでクローズする">アンチパターン2: 「たぶん直った」でクローズする</h3>
<p>復旧後 15 分の監視を省くと、同じ障害が波状的に再発します。復旧宣言は「指標が閾値内で安定した」ことを条件にします。</p>
<h3 id="アンチパターン3-障害後の改善が担当者依存">アンチパターン3: 障害後の改善が担当者依存</h3>
<p>改善をドキュメント化せず担当者の記憶に任せると、同じ夜間障害を繰り返します。Runbook への反映までをインシデント完了条件に含めるべきです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>SRE</category>
      <category>Incident Response</category>
      <category>Runbook</category>
    </item>
    <item>
      <title>FastAPI本番運用ハードニング完全ガイド：セキュリティ・性能・障害対応を実装で固める</title>
      <link>https://www.ai2core.com/posts/2026-02-28-fastapi-production-hardening-guide/</link>
      <pubDate>Sat, 28 Feb 2026 09:15:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-28-fastapi-production-hardening-guide/</guid>
      <description>FastAPIを本番運用する際に必要なセキュリティ、性能最適化、観測性、デプロイ手順を具体的に解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi本番運用ハードニング完全ガイドセキュリティ性能障害対応を実装で固める">FastAPI本番運用ハードニング完全ガイド：セキュリティ・性能・障害対応を実装で固める</h1>
<p>FastAPI は開発速度が高く、PoC から本番まで一気に進めやすいフレームワークです。しかし、早く作れることと安全に運用できることは別問題です。実際の障害は、コードの正しさよりも運用の隙から発生します。</p>
<p>本記事では、FastAPI を本番で安心して運用するためのハードニング手順を、実装可能な形でまとめます。対象は「すでにAPIが動いているが、運用強度を上げたい」チームです。</p>
<h2 id="1-入口防御tlsヘッダレート制限">1. 入口防御：TLS、ヘッダ、レート制限</h2>
<h3 id="tls終端とforwardedヘッダ">TLS終端とForwardedヘッダ</h3>
<p>ロードバランサ配下で動かす場合、<code>X-Forwarded-For</code> と <code>X-Forwarded-Proto</code> の扱いを明確にします。誤るとクライアントIPが取れず、監査も制限も機能しません。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> FastAPI
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> starlette.middleware.trustedhost <span style="color:#f92672">import</span> TrustedHostMiddleware
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> FastAPI()
</span></span><span style="display:flex;"><span>app<span style="color:#f92672">.</span>add_middleware(TrustedHostMiddleware, allowed_hosts<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;api.example.com&#34;</span>, <span style="color:#e6db74">&#34;*.example.com&#34;</span>])
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>allowed_hosts</code> をワイルドにしすぎると Host Header Injection の温床になります。</p>
<h3 id="セキュリティヘッダ">セキュリティヘッダ</h3>
<p>最低限次を返します。</p>
<ul>
<li><code>Strict-Transport-Security</code></li>
<li><code>X-Content-Type-Options: nosniff</code></li>
<li><code>X-Frame-Options: DENY</code></li>
<li><code>Referrer-Policy</code></li>
</ul>
<p>APIでも無関係ではありません。管理画面やドキュメントUIを守る意味があります。</p>
<h3 id="レート制限">レート制限</h3>
<p>ブルートフォースと突発負荷に備え、IPまたはAPIキー単位でレート制限を設定します。</p>
<ul>
<li>認証系: 5 req/min</li>
<li>通常API: 60 req/min</li>
<li>高負荷検索: 20 req/min</li>
</ul>
<p>Redis バックエンド方式にして、アプリ再起動でカウンタが失われないようにします。</p>
<h2 id="2-認証認可の落とし穴を塞ぐ">2. 認証・認可の落とし穴を塞ぐ</h2>
<h3 id="jwt検証の必須項目">JWT検証の必須項目</h3>
<p><code>exp</code> だけ見て通す実装は危険です。少なくとも次を検証します。</p>
<ul>
<li><code>iss</code>（発行者）</li>
<li><code>aud</code>（想定利用先）</li>
<li><code>nbf</code>（有効開始）</li>
<li><code>kid</code> に基づく鍵ローテーション</li>
</ul>
<h3 id="認可はエンドポイント単位ではなくリソース単位">認可は「エンドポイント単位」ではなく「リソース単位」</h3>
<p><code>/users/{id}</code> のアクセス時に、path パラメータの <code>id</code> とトークンの主体を照合しない事故は頻発します。FastAPI の dependency で統一的に実施します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">authorize_user_resource</span>(current_user, target_user_id: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> (current_user<span style="color:#f92672">.</span>is_admin <span style="color:#f92672">or</span> current_user<span style="color:#f92672">.</span>user_id <span style="color:#f92672">==</span> target_user_id):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> HTTPException(status_code<span style="color:#f92672">=</span><span style="color:#ae81ff">403</span>, detail<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;forbidden&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="3-入出力の安全化pydanticだけでは不十分">3. 入出力の安全化：Pydanticだけでは不十分</h2>
<p>Pydantic は型安全に強いですが、ビジネス制約は別で実装が必要です。</p>
<ul>
<li>文字列長上限</li>
<li>許可文字セット</li>
<li>SQL/NoSQLインジェクションの危険文字</li>
<li>HTML/Markdown サニタイズ</li>
</ul>
<p>特に検索APIやエクスポートAPIは、クエリ文字列が巨大化しやすく DoS の入口になります。<code>max_length</code> を必ず定義してください。</p>
<h2 id="4-性能ハードニングワーカdbタイムアウト">4. 性能ハードニング：ワーカ・DB・タイムアウト</h2>
<h3 id="uvicorngunicorn-構成">Uvicorn/Gunicorn 構成</h3>
<p>CPUコア数に応じて worker を決めます。目安は <code>workers = 2 * core + 1</code> から開始し、実測で調整。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>gunicorn app.main:app -k uvicorn.workers.UvicornWorker --workers <span style="color:#ae81ff">5</span> --bind 0.0.0.0:8000 --timeout <span style="color:#ae81ff">30</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="db接続プール">DB接続プール</h3>
<p><code>asyncpg</code> や SQLAlchemy async engine のプール上限を設定しないと、ピーク時に接続飽和します。</p>
<ul>
<li>min: 5</li>
<li>max: 30（DB性能と相談）</li>
<li>pool timeout: 5s</li>
</ul>
<h3 id="タイムアウト戦略">タイムアウト戦略</h3>
<p>上流・下流の timeout を揃えないと、雪崩障害が発生します。</p>
<ul>
<li>外部API呼び出し: connect 1s / read 3s</li>
<li>DBクエリ: statement timeout 2s（重処理は別キュー）</li>
<li>API全体: 10s で fail fast</li>
</ul>
<h2 id="5-例外設計と障害時の挙動">5. 例外設計と障害時の挙動</h2>
<p>本番障害では「500が出ること」より「500の意味が不明」なことが問題です。エラーレスポンス形式を固定し、trace_id を必ず返します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;error&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;code&#34;</span>: <span style="color:#e6db74">&#34;INTERNAL_ERROR&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;unexpected error&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trace_id&#34;</span>: <span style="color:#e6db74">&#34;8f3d...&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>内部例外はそのまま返さず、ログ側に stack trace を記録。ユーザーには安全な文言のみ返却します。</p>
<h2 id="6-可観測性ログメトリクストレース">6. 可観測性：ログ・メトリクス・トレース</h2>
<h3 id="構造化ログ">構造化ログ</h3>
<p>JSON ログを標準化し、次を必須項目にします。</p>
<ul>
<li>timestamp</li>
<li>level</li>
<li>service</li>
<li>trace_id</li>
<li>user_id（可能なら）</li>
<li>endpoint</li>
<li>latency_ms</li>
</ul>
<h3 id="メトリクス">メトリクス</h3>
<p>最低限:</p>
<ul>
<li>RPS</li>
<li>エラー率（4xx/5xx）</li>
<li>P50/P95/P99 latency</li>
<li>DB遅延</li>
<li>外部API失敗率</li>
</ul>
<h3 id="トレース">トレース</h3>
<p>OpenTelemetry で endpoint → service → DB をつなぐと、障害切り分けが劇的に速くなります。</p>
<h2 id="7-デプロイ戦略壊さずに出す">7. デプロイ戦略：壊さずに出す</h2>
<p>推奨は Blue/Green か Canary。FastAPI 単体の問題より、周辺設定差異が事故の原因になります。</p>
<p>リリース前チェックリスト:</p>
<ol>
<li>DB migration の後方互換性</li>
<li>依存ライブラリ脆弱性スキャン</li>
<li>load test（代表3API）</li>
<li>rollback 手順の実行確認</li>
<li>feature flag で段階有効化</li>
</ol>
<h2 id="8-運用で効くインシデント訓練">8. 運用で効くインシデント訓練</h2>
<p>月1回、次の擬似障害を実施すると運用強度が上がります。</p>
<ul>
<li>DB遅延 3秒化</li>
<li>外部API 30% 失敗</li>
<li>メモリリーク発生</li>
<li>JWT鍵ローテーション失敗</li>
</ul>
<p>重要なのは、復旧時間だけでなく「誰が何を見て判断したか」を記録することです。Runbook の更新まで含めて初めて訓練が完結します。</p>
<h2 id="9-すぐ使える最小ハードニングチェック">9. すぐ使える最小ハードニングチェック</h2>
<ul>
<li><input disabled="" type="checkbox"> Host ヘッダ制限</li>
<li><input disabled="" type="checkbox"> JWT <code>iss/aud/exp/nbf</code> 検証</li>
<li><input disabled="" type="checkbox"> 全エンドポイントに認可 dependency</li>
<li><input disabled="" type="checkbox"> 外部API timeout/retry/circuit breaker</li>
<li><input disabled="" type="checkbox"> JSON 構造化ログ + trace_id</li>
<li><input disabled="" type="checkbox"> P95 latency 監視とアラート</li>
<li><input disabled="" type="checkbox"> rollback 手順が5分で実行可能</li>
</ul>
<p>この 7 項目が揃うだけで、障害時の被害規模は大きく下がります。</p>
<h2 id="まとめ">まとめ</h2>
<p>FastAPI は高速開発の武器ですが、本番運用では「早く作る」より「安全に壊れる」設計が重要です。入口防御、認証認可、性能制御、観測性、リリース運用をセットで整備すれば、チームは安心して機能開発に集中できます。</p>
<p>もし何から始めるか迷うなら、まずは trace_id 付きの構造化ログと timeout 統一から着手してください。最小の投資で、運用の見通しが一気に良くなります。</p>
<h2 id="10-セキュアな開発フローを維持するためのci設定">10. セキュアな開発フローを維持するためのCI設定</h2>
<p>本番ハードニングはコードだけでなく、CI フローで担保する必要があります。推奨するジョブは次の通りです。</p>
<ol>
<li>依存脆弱性スキャン（pip-audit / osv-scanner）</li>
<li>SAST（bandit など）</li>
<li>型チェック（mypy）</li>
<li>負荷テストのスモーク（k6）</li>
<li>OpenAPI 差分チェック（破壊的変更検出）</li>
</ol>
<p>特に OpenAPI 差分チェックは有効です。意図しないレスポンス変更を早期に検知でき、フロントエンド障害を防げます。</p>
<h2 id="11-バックアップと復旧を設計に含める">11. バックアップと復旧を設計に含める</h2>
<p>API 運用は「壊れない」ではなく「壊れても戻せる」が現実的です。最低限次を決めておきます。</p>
<ul>
<li>DB バックアップ頻度（例: 15分ごと増分、日次フル）</li>
<li>復旧目標（RTO/RPO）</li>
<li>復旧手順の担当と実行順</li>
</ul>
<p>復旧訓練をしていないバックアップは、存在しないのと同じです。四半期に一度は検証環境でリストア演習を行ってください。</p>
<h2 id="12-監査対応を見据えたログ保全">12. 監査対応を見据えたログ保全</h2>
<p>B2B API では監査要件が後から増えることが多いです。最初から次を満たす設計にしておくと後で困りません。</p>
<ul>
<li>監査ログとアプリログを分離</li>
<li>重要操作（権限変更、削除、課金操作）の証跡保存</li>
<li>ログ保持期間の明確化（例: 180日）</li>
<li>改ざん検知（WORM ストレージや署名）</li>
</ul>
<p>「誰が、いつ、何をしたか」を追えることは、障害解析だけでなく法務リスク低減にも直結します。</p>
<h2 id="最終まとめ">最終まとめ</h2>
<p>FastAPI の本番運用は、フレームワーク知識だけでは足りません。セキュリティ、性能、可観測性、復旧性を一体で設計することが重要です。チェックリスト化し、CI と運用手順へ落とし込むことで、安定した開発速度を維持できます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>Python</category>
      <category>Security</category>
      <category>SRE</category>
    </item>
    <item>
      <title>Kubernetesコスト最適化の実務：FinOps視点で無駄を可視化し、性能を落とさず削減する方法</title>
      <link>https://www.ai2core.com/posts/2026-02-27-kubernetes-finops-cost-optimization/</link>
      <pubDate>Fri, 27 Feb 2026 18:30:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-27-kubernetes-finops-cost-optimization/</guid>
      <description>Kubernetes環境で発生しがちな無駄コストを、計測・改善・運用ルールの3段階で削減する具体的な実践ガイド。</description>
      <content:encoded><![CDATA[<h1 id="kubernetesコスト最適化の実務finops視点で無駄を可視化し性能を落とさず削減する方法">Kubernetesコスト最適化の実務：FinOps視点で無駄を可視化し、性能を落とさず削減する方法</h1>
<p>Kubernetes を導入した直後は「自動で効率化される」と期待されがちですが、実際には逆です。適切な運用ルールがないと、クラウド請求は静かに膨らみ続けます。</p>
<p>典型例は次の通りです。</p>
<ul>
<li>リクエスト/リミットが過大でノードが常時スカスカ</li>
<li>夜間トラフィック減少時も同じ台数を維持</li>
<li>開発環境が週末ずっと起動</li>
<li>HPA が CPU しか見ておらず、キュー滞留を無視</li>
<li>過剰なストレージクラス（IOPS課金）を全環境で使用</li>
</ul>
<p>本記事では、FinOps の考え方を用いて、Kubernetesコストを「見える化→改善→定着」の順に進める方法を解説します。</p>
<h2 id="コスト削減の前に定義すべき指標">コスト削減の前に定義すべき指標</h2>
<p>削減施策が失敗する理由は、目標が「安くする」しかないからです。最低でも次の 4 指標を定義します。</p>
<ol>
<li><strong>Unit Cost</strong>: 1リクエストあたりコスト / 1ジョブあたりコスト</li>
<li><strong>Utilization</strong>: CPU・Memoryの実効使用率</li>
<li><strong>Reliability</strong>: SLO 達成率（削減で品質を落とさない）</li>
<li><strong>Waste Ratio</strong>: 予約/実使用の差分率</li>
</ol>
<p>この4つを同時に追うと、単なるコストカットでなく「健全な最適化」になります。</p>
<h2 id="ステップ1可視化基盤を整える">ステップ1：可視化基盤を整える</h2>
<h3 id="1-1-kubecost-または-opencost-の導入">1-1. Kubecost または OpenCost の導入</h3>
<p>EKS/GKE/AKS いずれでも、namespace / deployment / label 単位で費用を把握できる状態にします。重要なのは <code>team</code>, <code>service</code>, <code>env</code> ラベルを強制することです。</p>
<p>推奨ラベル:</p>
<ul>
<li><code>cost-center</code></li>
<li><code>owner</code></li>
<li><code>environment</code>（prod/stg/dev）</li>
<li><code>criticality</code></li>
</ul>
<p>ラベルがないと請求分析ができず、改善責任が曖昧になります。</p>
<h3 id="1-2-観測ダッシュボード">1-2. 観測ダッシュボード</h3>
<p>最初のダッシュボードは次を必須にします。</p>
<ul>
<li>Namespace別コスト（当日/前日比）</li>
<li>Pod別 request/usage ギャップ</li>
<li>ノード空き率（CPU、Memory）</li>
<li>時間帯別トラフィックとレプリカ数</li>
</ul>
<p>ここまでで「どこが高いか」は見えます。次は「なぜ高いか」を潰します。</p>
<h2 id="ステップ2最も効果が高い改善施策">ステップ2：最も効果が高い改善施策</h2>
<h3 id="2-1-right-sizing最優先">2-1. Right Sizing（最優先）</h3>
<p>多くの環境で最大効果が出るのは request/limit の見直しです。VPA recommendation を参考に、まずは read-only で 2 週間観測します。</p>
<p>例:</p>
<ul>
<li>現在: <code>requests.cpu=1000m</code>, 実使用 P95 が 220m</li>
<li>改善後: <code>requests.cpu=300m</code></li>
</ul>
<p>この1変更だけでノード数が 20〜35% 下がるケースがあります。</p>
<h3 id="2-2-cluster-autoscaler--karpenter-最適化">2-2. Cluster Autoscaler / Karpenter 最適化</h3>
<p>オートスケーラーを入れていても、設定不備で縮退しないことが多いです。次をチェックします。</p>
<ul>
<li>scale down unneeded time が長すぎないか</li>
<li>pod disruption budget が硬すぎないか</li>
<li>daemonset が過剰にリソースを固定していないか</li>
</ul>
<p>Karpenter なら Spot + On-Demand の比率制御を導入し、重要ワークロードのみ On-Demand 固定にします。</p>
<h3 id="2-3-ワークロードの時間制御">2-3. ワークロードの時間制御</h3>
<p>開発・検証環境はスケジュール停止が有効です。平日 9:00-20:00 だけ稼働し、夜間/休日は scale to zero。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 例: CronJobで夜間停止</span>
</span></span><span style="display:flex;"><span>kubectl scale deployment api-staging --replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> -n staging
</span></span></code></pre></td></tr></table>
</div>
</div><p>単純ですが、月額で大きく効きます。</p>
<h3 id="2-4-hpa指標の改善">2-4. HPA指標の改善</h3>
<p>CPU のみでスケールすると、I/O 待ちやキュー詰まりを見逃します。Prometheus Adapter を使い、次の指標を追加します。</p>
<ul>
<li>queue length</li>
<li>request latency P95</li>
<li>in-flight request</li>
</ul>
<p>結果として「必要な時だけ増やす」が実現し、過剰常時稼働を減らせます。</p>
<h2 id="ステップ3ストレージネットワークの見直し">ステップ3：ストレージ/ネットワークの見直し</h2>
<h3 id="ストレージ">ストレージ</h3>
<p>高IOPSディスクを全環境に使うのは典型的な浪費です。ワークロード特性でクラス分離します。</p>
<ul>
<li>DB本番: 高IOPS</li>
<li>バッチ/開発: 標準クラス</li>
</ul>
<p>スナップショット保持期間も要確認です。無制限保持は請求を圧迫します。</p>
<h3 id="ネットワーク">ネットワーク</h3>
<p>クロスAZ通信量は見落とされがちです。チャットAPIや内部gRPCが高頻度だと、トラフィック課金が無視できません。依存サービスを同一AZに寄せる設計を検討します。</p>
<h2 id="ガードレールを仕組み化する">ガードレールを仕組み化する</h2>
<p>最適化は一度で終わりません。再び膨らむので、CI/CD に制約を組み込みます。</p>
<ul>
<li>OPA/Gatekeeper で「requests未設定」を拒否</li>
<li>ラベル欠落 (<code>owner</code>, <code>cost-center</code>) の manifest を reject</li>
<li>予算超過 namespace の新規デプロイを警告</li>
</ul>
<p>さらに、週次で「Top Waste 10」を自動配信し、チームごとに改善オーナーを固定します。</p>
<h2 id="実例3か月で-32-削減した手順">実例：3か月で 32% 削減した手順</h2>
<p>ある SaaS 環境（5クラスタ）で実施した順序は次の通りです。</p>
<ol>
<li>2週間の使用実績を収集</li>
<li>上位20 deployment の rightsizing</li>
<li>staging/dev の時間停止</li>
<li>Spot 比率を 15% → 45% へ引き上げ</li>
<li>高コストPVの棚卸し</li>
</ol>
<p>結果:</p>
<ul>
<li>月次コスト: -32%</li>
<li>P95 レイテンシ: ほぼ維持</li>
<li>インシデント増加: なし</li>
</ul>
<p>ポイントは、SLO を維持しながら段階実施したことです。</p>
<h2 id="よくある反論への回答">よくある反論への回答</h2>
<p><strong>Q. 削減すると障害が増えるのでは？</strong>
A. いきなり下げると危険です。P95実績を基準に 10〜15% ずつ下げ、SLO と同時監視すれば安全に進められます。</p>
<p><strong>Q. チームが協力しない</strong>
A. コストを「共通責任」にすると進みません。namespaceごとに owner を明示し、改善期限を決めると動きます。</p>
<p><strong>Q. どこから始めるべき？</strong>
A. Right Sizing と夜間停止です。最短で効果が出ます。</p>
<h2 id="まとめ">まとめ</h2>
<p>Kubernetes コスト最適化は、節約テクニックの寄せ集めではありません。計測、改善、運用ガードレールをセットで回す FinOps プロセスです。</p>
<p>まずは「誰の、どのワークロードが、なぜ高いか」を可視化し、Right Sizing から着手してください。性能を落とさずにコストを下げる道筋は、思っているより現実的です。</p>
<h2 id="実装テンプレート無駄を自動検出する">実装テンプレート：無駄を自動検出する</h2>
<p>可視化だけだと改善は進みません。週次で自動的に「無駄候補」を抽出してチームへ配信する仕組みを入れると、改善が継続します。</p>
<p>抽出ルール例:</p>
<ul>
<li>7日平均で CPU 使用率 &lt; 15% かつ request &gt; 500m</li>
<li>夜間（0:00-6:00）にトラフィックほぼゼロなのに replicas &gt; 1</li>
<li>メモリ使用率 P95 &lt; 40% が 14 日継続</li>
</ul>
<p>このルールで候補を Slack/Discord に自動投稿し、オーナーと期限を付けるだけで改善率が上がります。</p>
<h2 id="具体的なyaml見直し例">具体的なYAML見直し例</h2>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;250m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;512Mi&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;1Gi&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">autoscaling</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">minReplicas</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">maxReplicas</span>: <span style="color:#ae81ff">8</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">targetCPUUtilizationPercentage</span>: <span style="color:#ae81ff">65</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは「requests を現実に寄せる」「limits を安全マージンに置く」「HPA で不足分だけ増やす」の3点です。</p>
<h2 id="ガバナンスコストを技術負債化させない">ガバナンス：コストを技術負債化させない</h2>
<p>削減施策は放置すると必ず戻ります。そこで、リリースゲートに FinOps 観点を追加します。</p>
<ul>
<li>新規サービスは cost label 必須</li>
<li>request 未設定の Pod はデプロイ拒否</li>
<li>予算超過時は自動で注意喚起</li>
</ul>
<p>さらに月次レビューで「削減額」だけでなく「SLO維持率」も一緒に報告することで、コスト最適化が品質改善と対立しない文化を作れます。</p>
<h2 id="まとめ実行順の再確認">まとめ（実行順の再確認）</h2>
<ol>
<li>まず可視化（誰が何にいくら使っているか）</li>
<li>次に Right Sizing と時間停止</li>
<li>最後にガードレールをCI/CDに組み込む</li>
</ol>
<p>この順番を守れば、短期の削減成果と中長期の運用安定を両立できます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>FinOps</category>
      <category>Cloud</category>
      <category>SRE</category>
    </item>
  </channel>
</rss>
