<?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>Tech on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/categories/tech/</link>
    <description>Recent content in Tech on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 07 Mar 2026 13:00:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/categories/tech/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>GitHub Actionsでモノレポを安全に自動リリースする設計: 変更検知・段階配布・失敗復旧</title>
      <link>https://www.ai2core.com/posts/2026-03-07-github-actions-monorepo-release-automation-guide/</link>
      <pubDate>Sat, 07 Mar 2026 13:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-07-github-actions-monorepo-release-automation-guide/</guid>
      <description>モノレポ運用でGitHub Actionsを使い、変更検知から段階リリース、ロールバック、監査まで実装する具体手順を解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actionsでモノレポを安全に自動リリースする設計-変更検知段階配布失敗復旧">GitHub Actionsでモノレポを安全に自動リリースする設計: 変更検知・段階配布・失敗復旧</h1>
<p>モノレポのCI/CDは、単一リポジトリだから楽になる一方で、リリース設計を誤ると一気に難しくなります。</p>
<ul>
<li>1つの変更で全サービスを再デプロイしてしまう</li>
<li>並列ジョブが増えてキュー渋滞する</li>
<li>どのコミットがどのサービスへ反映されたか追跡できない</li>
<li>一部失敗時のロールバックが曖昧</li>
</ul>
<p>本記事では、GitHub Actionsでモノレポを運用しているチーム向けに、実務で耐えるリリース自動化の構成を具体的に説明します。</p>
<h2 id="1-モノレポcicdで先に決める設計原則">1. モノレポCI/CDで先に決める設計原則</h2>
<p>最初に次の原則を明文化します。</p>
<ol>
<li>変更のないサービスはデプロイしない</li>
<li>リリース対象は機械的に決定する</li>
<li>本番反映は段階的（canary/割合配布）</li>
<li>失敗時の復旧手順を自動化する</li>
<li>監査ログ（誰が何をいつ）を残す</li>
</ol>
<p>この5つがないと、運用が属人化し、障害時対応が遅れます。</p>
<h2 id="2-変更検知をワークフローの入口に置く">2. 変更検知をワークフローの入口に置く</h2>
<p>モノレポでは「どのディレクトリが変わったか」を最初に判定し、対象サービスだけを処理します。</p>
<h3 id="21-changed-filesの例">2.1 changed-filesの例</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><span 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></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">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">detect</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">outputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">matrix</span>: <span style="color:#ae81ff">${{ steps.set-matrix.outputs.matrix }}</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 style="color:#f92672">id</span>: <span style="color:#ae81ff">changed</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">tj-actions/changed-files@v45</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">files_yaml</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            api:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              - services/api/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            web:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              - services/web/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            worker:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              - services/worker/**</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">set-matrix</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          python .github/scripts/build_matrix.py &#39;${{ toJson(steps.changed.outputs) }}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここでmatrixを作り、後続ジョブを <code>fromJson</code> で動的展開します。</p>
<h2 id="3-reusable-workflowで共通化する">3. reusable workflowで共通化する</h2>
<p>各サービスごとにworkflowを複製すると、保守コストが急上昇します。<code>workflow_call</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></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:#75715e"># .github/workflows/release-service.yml</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">workflow_call</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</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">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">string</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">string</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">release</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">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 style="color:#f92672">run</span>: <span style="color:#ae81ff">./scripts/release.sh ${{ inputs.service }} ${{ inputs.environment }}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>呼び出し側はサービス名だけ渡せばよくなり、ガバナンスが効きます。</p>
<h2 id="4-同時実行制御concurrencyで事故防止">4. 同時実行制御（concurrency）で事故防止</h2>
<p>同一サービスへの並列デプロイは事故のもとです。<code>concurrency</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></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">concurrency</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">group</span>: <span style="color:#ae81ff">release-${{ github.ref }}-${{ matrix.service }}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cancel-in-progress</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>cancel-in-progress</code> は本番では通常 <code>false</code> が安全です。途中キャンセルで半端状態を作らないためです。</p>
<h2 id="5-環境ごとの保護ルールを使う">5. 環境ごとの保護ルールを使う</h2>
<p>GitHub Environmentsを使えば、本番前レビューやシークレット分離を標準機能で実装できます。</p>
<ul>
<li>staging: 自動デプロイ</li>
<li>production: 承認必須</li>
<li>緊急時のみ管理者がoverride</li>
</ul>
<p>加えて、OIDCでクラウド認証し、長期鍵（固定アクセスキー）を排除します。</p>
<h2 id="6-段階リリースcanaryを組み込む">6. 段階リリース（Canary）を組み込む</h2>
<p>本番へ一気に100%展開は避け、段階配布をワークフローに組み込みます。</p>
<p>例:</p>
<ol>
<li>10%トラフィックへ5分</li>
<li>エラーレート確認</li>
<li>問題なければ50%</li>
<li>最終的に100%</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></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>deploy --service api --traffic <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>check_slo api <span style="color:#f92672">||</span> rollback api
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>deploy --service api --traffic <span style="color:#ae81ff">50</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>check_slo api <span style="color:#f92672">||</span> rollback api
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>deploy --service api --traffic <span style="color:#ae81ff">100</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>check_slo</code> はp95レイテンシ・5xx率・ビジネスKPIを含めるのが理想です。</p>
<h2 id="7-失敗復旧を手順ではなく機能にする">7. 失敗復旧を「手順」ではなく「機能」にする</h2>
<p>運用で強いチームは、ロールバックを手作業Runbookではなくワークフロー化しています。</p>
<h3 id="71-rollback-workflow例">7.1 rollback workflow例</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></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">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</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">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">target_sha</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</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">rollback</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">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 style="color:#f92672">run</span>: <span style="color:#ae81ff">./scripts/rollback.sh ${{ github.event.inputs.service }} ${{ github.event.inputs.target_sha }}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>「事故時はこのボタンを押す」で復旧できる状態を目指してください。</p>
<h2 id="8-リリース証跡の自動生成">8. リリース証跡の自動生成</h2>
<p>監査対応やトラブル解析で必須になるのが証跡です。</p>
<p>最低限、以下をアーティファクト化します。</p>
<ul>
<li>対象サービス一覧</li>
<li>対象コミットSHA</li>
<li>実行者</li>
<li>実行時刻</li>
<li>結果（成功/失敗）</li>
<li>ロールバック有無</li>
</ul>
<p>さらにSlack/Discord通知で、サービス単位の反映結果を即時共有すると現場が安定します。</p>
<h2 id="9-コスト最適化も同時に行う">9. コスト最適化も同時に行う</h2>
<p>モノレポCIは規模が大きくなるほどコストが効いてきます。</p>
<ul>
<li>キャッシュ（依存/ビルド成果物）を徹底</li>
<li>変更のないサービスはskip</li>
<li>夜間バッチと競合しない時間帯に重いjobを寄せる</li>
<li>自己ホストランナーはautoscaling前提で運用</li>
</ul>
<p>「全サービス毎回build」は最初は簡単ですが、半年後に必ず負債化します。</p>
<h2 id="10-導入チェックリスト">10. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 変更検知のmatrix生成がある</li>
<li><input disabled="" type="checkbox"> reusable workflowに統一されている</li>
<li><input disabled="" type="checkbox"> concurrency制御がサービス単位で設定済み</li>
<li><input disabled="" type="checkbox"> production環境に承認ルールあり</li>
<li><input disabled="" type="checkbox"> canary + 自動SLO判定がある</li>
<li><input disabled="" type="checkbox"> rollback workflowが存在する</li>
<li><input disabled="" type="checkbox"> リリース証跡を保存している</li>
<li><input disabled="" type="checkbox"> 通知連携（Slack/Discord）が整備されている</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>GitHub Actionsでのモノレポ自動リリースは、単にworkflowを書く作業ではありません。<strong>変更検知・段階配布・復旧自動化・監査</strong> を含む運用設計です。</p>
<ul>
<li>デプロイ対象を最小化する</li>
<li>本番反映は必ず段階化する</li>
<li>失敗時の復旧を自動化する</li>
<li>証跡を残して再現性を確保する</li>
</ul>
<p>この設計を最初に固めると、サービス数が増えても運用は破綻しません。まずは「変更検知 + reusable workflow + rollback workflow」の3点から着手するのが実装コストと効果のバランスが良いです。</p>
<h2 id="付録-失敗しにくい運用設計テンプレート">付録: 失敗しにくい運用設計テンプレート</h2>
<p>最後に、運用へ落とし込むときに有効なテンプレートを示します。実際にはこの3点をチーム標準にするだけで、リリース事故率が下がります。</p>
<ul>
<li><strong>PRテンプレートに「影響サービス」欄を必須化</strong>
<ul>
<li><code>service-a, service-b</code> のように宣言させ、変更検知結果と突合する</li>
</ul>
</li>
<li><strong>デプロイ承認時の確認項目を固定化</strong>
<ul>
<li>監視グラフURL、ロールバック手順URL、オンコール担当を毎回入力</li>
</ul>
</li>
<li><strong>失敗後レビューのフォーマット統一</strong>
<ul>
<li>失敗原因、検知時刻、復旧時刻、再発防止策を必ず残す</li>
</ul>
</li>
</ul>
<p>特にモノレポでは「誰がどこに責任を持つか」が曖昧になりやすいです。workflowの技術実装だけでなく、<strong>承認と振り返りの運用ルール</strong>をセットにして初めて、自動リリースは安定運用に乗ります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>Monorepo</category>
      <category>CI/CD</category>
      <category>Release</category>
      <category>DevOps</category>
    </item>
    <item>
      <title>FastAPI &#43; SQLAlchemy性能改善プレイブック: 遅いAPIを計測ベースで高速化する</title>
      <link>https://www.ai2core.com/posts/2026-03-07-fastapi-sqlalchemy-performance-tuning-playbook/</link>
      <pubDate>Sat, 07 Mar 2026 11:10:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-07-fastapi-sqlalchemy-performance-tuning-playbook/</guid>
      <description>FastAPIとSQLAlchemyのAPI性能を、N&#43;1解消・クエリ最適化・接続プール設定・負荷検証まで含めて具体的に改善する実践手順を解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi--sqlalchemy性能改善プレイブック-遅いapiを計測ベースで高速化する">FastAPI + SQLAlchemy性能改善プレイブック: 遅いAPIを計測ベースで高速化する</h1>
<p>FastAPIの初期実装は非常に快適です。しかし運用フェーズに入ると、次のような症状が出てきます。</p>
<ul>
<li>一覧APIのレスポンスが急に遅くなる</li>
<li>同時接続が増えるとp95が跳ねる</li>
<li>CPUは余っているのにタイムアウトが増える</li>
<li>DB接続数が上限に張り付く</li>
</ul>
<p>こうした問題の多くは「Pythonが遅い」のではなく、<strong>SQLAlchemyの使い方とDBアクセス設計</strong> に起因します。</p>
<p>本記事では、FastAPI + SQLAlchemy + PostgreSQL構成を前提に、実際の改善手順を計測ベースで整理します。</p>
<h2 id="1-最初に測るべき指標">1. 最初に測るべき指標</h2>
<p>最適化は、体感ではなく数値で進めます。最低限、以下を可視化します。</p>
<ul>
<li>APIのp50/p95/p99レイテンシ</li>
<li>エンドポイント別SQL発行回数</li>
<li>1リクエストあたりのDB滞在時間</li>
<li>connection pool待ち時間</li>
<li>slow query件数（200ms以上など）</li>
</ul>
<p>OpenTelemetryやNew Relicを使っているなら、アプリspanとDB spanを必ず紐付けてください。これだけでボトルネック特定速度が上がります。</p>
<h2 id="2-n1問題を最優先で潰す">2. N+1問題を最優先で潰す</h2>
<p>最も頻出するのがN+1です。例えばユーザー一覧でプロフィールを参照すると、ユーザー数分の追加クエリが発行されます。</p>
<h3 id="21-悪い例">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><span 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></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>users <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>query(User)<span style="color:#f92672">.</span>limit(<span style="color:#ae81ff">100</span>)<span style="color:#f92672">.</span>all()
</span></span><span style="display:flex;"><span>result <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> u <span style="color:#f92672">in</span> users:
</span></span><span style="display:flex;"><span>    result<span style="color:#f92672">.</span>append({
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;id&#34;</span>: u<span style="color:#f92672">.</span>id,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;name&#34;</span>: u<span style="color:#f92672">.</span>name,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;profile&#34;</span>: u<span style="color:#f92672">.</span>profile<span style="color:#f92672">.</span>bio,
</span></span><span style="display:flex;"><span>    })
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="22-改善例joinedloadselectinload">2.2 改善例（joinedload/selectinload）</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></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> sqlalchemy.orm <span style="color:#f92672">import</span> selectinload
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>users <span style="color:#f92672">=</span> (
</span></span><span style="display:flex;"><span>    session<span style="color:#f92672">.</span>query(User)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>options(selectinload(User<span style="color:#f92672">.</span>profile))
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>limit(<span style="color:#ae81ff">100</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>all()
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>joinedload</code> と <code>selectinload</code> はデータ量で使い分けます。</p>
<ul>
<li>1対1/少量: <code>joinedload</code></li>
<li>1対多/件数多め: <code>selectinload</code></li>
</ul>
<p>闇雲に <code>joinedload</code> を増やすと行爆発が起きるため、EXPLAINで確認しながら適用します。</p>
<h2 id="3-sqlalchemy-2xスタイルへ揃える">3. SQLAlchemy 2.xスタイルへ揃える</h2>
<p>旧query APIと新APIが混在すると可読性と最適化精度が落ちます。2.xスタイルへ統一しましょう。</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></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> sqlalchemy <span style="color:#f92672">import</span> select
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>stmt <span style="color:#f92672">=</span> (
</span></span><span style="display:flex;"><span>    select(Order)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>where(Order<span style="color:#f92672">.</span>status <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;paid&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>order_by(Order<span style="color:#f92672">.</span>created_at<span style="color:#f92672">.</span>desc())
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>limit(<span style="color:#ae81ff">50</span>)
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>orders <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>execute(stmt)<span style="color:#f92672">.</span>scalars()<span style="color:#f92672">.</span>all()
</span></span></code></pre></td></tr></table>
</div>
</div><p>この形式は、<code>EXPLAIN</code> の追跡や再利用がしやすく、レビュー品質も上がります。</p>
<h2 id="4-必要な列だけ取る過剰フェッチの削減">4. 必要な列だけ取る（過剰フェッチの削減）</h2>
<p>ORMは便利ですが、何も考えずモデル全体を取ると不要データまで転送されます。特にJSONカラムやTEXTが重い場合、ここが効きます。</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-python" data-lang="python"><span style="display:flex;"><span>stmt <span style="color:#f92672">=</span> select(User<span style="color:#f92672">.</span>id, User<span style="color:#f92672">.</span>name, User<span style="color:#f92672">.</span>email)<span style="color:#f92672">.</span>where(User<span style="color:#f92672">.</span>active<span style="color:#f92672">.</span>is_(<span style="color:#66d9ef">True</span>))
</span></span><span style="display:flex;"><span>rows <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>execute(stmt)<span style="color:#f92672">.</span>all()
</span></span></code></pre></td></tr></table>
</div>
</div><p>一覧APIはDTO用の軽量SELECTを使い、詳細APIでのみ重いカラムを取得する設計が安定します。</p>
<h2 id="5-接続プール設定を環境に合わせる">5. 接続プール設定を環境に合わせる</h2>
<p><code>pool_size</code> を適当に増やすだけでは逆効果です。PostgreSQL側上限とアプリ台数を合わせて設計します。</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></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>engine <span style="color:#f92672">=</span> create_engine(
</span></span><span style="display:flex;"><span>    DB_URL,
</span></span><span style="display:flex;"><span>    pool_size<span style="color:#f92672">=</span><span style="color:#ae81ff">20</span>,
</span></span><span style="display:flex;"><span>    max_overflow<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>    pool_timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>    pool_recycle<span style="color:#f92672">=</span><span style="color:#ae81ff">1800</span>,
</span></span><span style="display:flex;"><span>    pool_pre_ping<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p>設計の目安:</p>
<ul>
<li>DB max_connections = 300</li>
<li>API Pod = 6</li>
<li>1 Podあたりpool_size 20</li>
</ul>
<p>この時点で理論最大120接続。バッチや管理接続も見込み、余白を残すのが安全です。</p>
<h2 id="6-トランザクション境界を短くする">6. トランザクション境界を短くする</h2>
<p>長いトランザクションはロック競合とスループット低下を招きます。</p>
<p>悪い例:</p>
<ol>
<li>DB更新</li>
<li>外部API呼び出し</li>
<li>メール送信</li>
<li>commit</li>
</ol>
<p>この順は危険です。外部I/Oをトランザクション外へ逃がします。</p>
<p>改善例:</p>
<ol>
<li>DB更新 + commit</li>
<li>外部通知は非同期ジョブで実行</li>
</ol>
<p>これだけで同時処理性能が目に見えて改善します。</p>
<h2 id="7-インデックス設計をapi単位で見直す">7. インデックス設計をAPI単位で見直す</h2>
<p>「インデックスはある」だけでは不足です。実際のWHERE + ORDER BYに合っているかが重要です。</p>
<p>例: 注文履歴API</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> id, total_amount, created_at
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> user_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span> <span style="color:#66d9ef">AND</span> status <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;paid&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> created_at <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">50</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>この場合、次の複合indexが有効です。</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">CREATE</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_orders_user_status_created_at_desc
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> orders (user_id, status, created_at <span style="color:#66d9ef">DESC</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>単独indexを乱立させるより、アクセスパターンに合わせた複合indexを厳選した方が効きます。</p>
<h2 id="8-キャッシュ導入は遅い理由の解決後に行う">8. キャッシュ導入は「遅い理由の解決後」に行う</h2>
<p>キャッシュは万能ではありません。N+1やスロークエリを放置したまま載せると、整合性事故の温床になります。</p>
<p>導入順序:</p>
<ol>
<li>SQL最適化</li>
<li>接続プール調整</li>
<li>必要なエンドポイントに限定してRedis cache</li>
</ol>
<p>キャッシュキーは <code>resource:id:version</code> 形式にし、更新時の無効化戦略を先に定義してください。</p>
<h2 id="9-負荷試験シナリオk6例">9. 負荷試験シナリオ（k6例）</h2>
<p>最適化の成果は負荷試験で確認します。最低3シナリオが必要です。</p>
<ul>
<li>steady: 通常トラフィック</li>
<li>burst: 短時間ピーク</li>
<li>soak: 長時間連続実行（リーク検知）</li>
</ul>
<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></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-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">http</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;k6/http&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">check</span>, <span style="color:#a6e22e">sleep</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;k6&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">stages</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;2m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">50</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;5m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">50</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;1m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">200</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;2m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span> },
</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">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">res</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;https://api.example.com/orders?limit=50&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">check</span>(<span style="color:#a6e22e">res</span>, { <span style="color:#e6db74">&#39;status 200&#39;</span><span style="color:#f92672">:</span> (<span style="color:#a6e22e">r</span>) =&gt; <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">200</span> });
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">sleep</span>(<span style="color:#ae81ff">1</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>改善前後で <code>p95</code>, SQL回数, DB CPU を比較し、定量で判断します。</p>
<h2 id="10-本番での改善手順テンプレート">10. 本番での改善手順テンプレート</h2>
<ol>
<li>ボトルネックendpointの特定</li>
<li>SQLログとEXPLAIN ANALYZE取得</li>
<li>N+1解消</li>
<li>必要列取得へ変更</li>
<li>インデックス追加（CONCURRENTLY）</li>
<li>pool設定調整</li>
<li>負荷試験再実施</li>
<li>段階リリース（10%→50%→100%）</li>
</ol>
<p>この手順を運用チーム全体でテンプレ化すると、パフォーマンス問題への対応速度が上がります。</p>
<h2 id="まとめ">まとめ</h2>
<p>FastAPI + SQLAlchemyの性能改善は、派手なテクニックより <strong>計測→原因分離→小さく改善</strong> の積み重ねが効きます。</p>
<ul>
<li>N+1解消</li>
<li>過剰フェッチ削減</li>
<li>接続プール最適化</li>
<li>インデックスの再設計</li>
<li>負荷試験で再検証</li>
</ul>
<p>この5点を回せば、遅いAPIは高確率で改善できます。まずは「1リクエストあたりのSQL発行数」を可視化するところから始めるのが最短です。</p>
<h2 id="付録-改善施策の優先順位最短で効く順">付録: 改善施策の優先順位（最短で効く順）</h2>
<p>時間が限られる現場では、まず「SQL発行回数削減 → インデックス最適化 → connection pool調整」の順で着手すると効果が出やすいです。特に、N+1解消だけでp95が半減するケースは珍しくありません。改善後は必ず同一負荷条件で再計測し、数字で効果を残しておくと、次の改善投資判断が通りやすくなります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>SQLAlchemy</category>
      <category>PostgreSQL</category>
      <category>Python</category>
      <category>Performance</category>
    </item>
    <item>
      <title>Terraform→OpenTofu移行実践ガイド: 既存IaCを止めずに移行するエンタープライズ手順</title>
      <link>https://www.ai2core.com/posts/2026-03-07-terraform-opentofu-migration-enterprise-playbook/</link>
      <pubDate>Sat, 07 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-07-terraform-opentofu-migration-enterprise-playbook/</guid>
      <description>Terraform資産をOpenTofuへ安全に移行するための、互換性確認、state管理、CI更新、段階的切替の実践手順を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="terraformopentofu移行実践ガイド-既存iacを止めずに移行するエンタープライズ手順">Terraform→OpenTofu移行実践ガイド: 既存IaCを止めずに移行するエンタープライズ手順</h1>
<p>Terraformのライセンス変更以降、OpenTofuへ移行したいという相談は確実に増えています。とはいえ現場の本音は「理屈は分かるが、stateが壊れたら終わる」「本番を止めずに移行できるのか不安」です。</p>
<p>結論から言うと、移行は十分可能です。ただし「CLIを置き換えるだけ」で済むケースは限定的で、実際は <strong>providerバージョン整合・state lock・CI/CD・運用Runbook</strong> までまとめて整える必要があります。</p>
<p>本記事では、既にTerraformを本番運用しているチーム向けに、OpenTofuへ段階移行する実践手順をまとめます。</p>
<h2 id="1-移行方針を先に決める">1. 移行方針を先に決める</h2>
<p>最初に決めるべきは「一気に切り替えるか」「ワークスペース単位で段階移行するか」です。実務では次の方針が安全です。</p>
<ol>
<li>低リスク環境（dev/sandbox）から先行</li>
<li>本番は最終フェーズで移行</li>
<li>旧TerraformとOpenTofuを一定期間並行運用</li>
<li>ロールバック手順を文書化してから実施</li>
</ol>
<p>この順序を守るだけで、移行事故の大半を避けられます。</p>
<h2 id="2-互換性の棚卸し最重要">2. 互換性の棚卸し（最重要）</h2>
<p>まずは現状のIaC資産を棚卸しします。</p>
<ul>
<li>Terraformバージョン（例: 1.5.x / 1.6.x）</li>
<li>使用provider（AWS/Azure/GCP/Kubernetes等）</li>
<li>backend（S3 + DynamoDB lock、Terraform Cloud、GCSなど）</li>
<li>moduleの参照方式（registry / git / local）</li>
<li>CI実行環境（GitHub Actions, GitLab CI, Jenkins）</li>
</ul>
<h3 id="21-依存を固定化してから移行する">2.1 依存を固定化してから移行する</h3>
<p><code>.terraform.lock.hcl</code> を必ずコミットし、providerを固定します。移行時にproviderまで同時更新すると、差分原因の切り分けが困難になります。</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></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">terraform</span> {
</span></span><span style="display:flex;"><span>  required_version <span style="color:#f92672">=</span> &#34;&gt;<span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>.<span style="color:#ae81ff">6</span>.<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">required_providers</span> {
</span></span><span style="display:flex;"><span>    aws <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      source  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hashicorp/aws&#34;</span>
</span></span><span style="display:flex;"><span>      version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 5.40&#34;</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>移行フェーズでは「ツール差分」と「provider差分」を分離してください。</p>
<h2 id="3-ローカル検証の基本手順">3. ローカル検証の基本手順</h2>
<p>OpenTofuを導入したら、既存プロジェクトで次を実行します。</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 現状確認</span>
</span></span><span style="display:flex;"><span>terraform version
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># OpenTofu側確認</span>
</span></span><span style="display:flex;"><span>tofu version
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 初期化（既存backendを利用）</span>
</span></span><span style="display:flex;"><span>tofu init -upgrade<span style="color:#f92672">=</span>false
</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> tofu plan -out<span style="color:#f92672">=</span>tfplan
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>plan</code> が完全一致しない場合、いきなり適用してはいけません。主な原因は以下です。</p>
<ul>
<li>providerの暗黙更新</li>
<li>data sourceの評価タイミング差</li>
<li>標準関数・型変換の微差</li>
<li>module内部のversion制約</li>
</ul>
<p>差分は「構文差分」より「実行時評価差分」で出ることが多い点に注意してください。</p>
<h2 id="4-stateを守る設計壊したら復旧が重い">4. Stateを守る設計（壊したら復旧が重い）</h2>
<p>IaC移行の本質的リスクはstateです。ここを守るために、以下を移行前チェックリストに組み込みます。</p>
<ul>
<li>state backendのバックアップ取得</li>
<li>lock機構が有効か確認（DynamoDB等）</li>
<li><code>apply</code> 権限を移行担当者に限定</li>
<li>並列実行ジョブを停止（同時apply禁止）</li>
</ul>
<h3 id="41-s3-backendの例">4.1 S3 backendの例</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></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">terraform</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">backend</span> <span style="color:#e6db74">&#34;s3&#34;</span> {
</span></span><span style="display:flex;"><span>    bucket         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prod-iac-state&#34;</span>
</span></span><span style="display:flex;"><span>    key            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;network/prod.tfstate&#34;</span>
</span></span><span style="display:flex;"><span>    region         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ap-northeast-1&#34;</span>
</span></span><span style="display:flex;"><span>    dynamodb_table <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-locks&#34;</span>
</span></span><span style="display:flex;"><span>    encrypt        <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</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>OpenTofuでもこのbackend構成は継続利用できます。重要なのは「誰がいつlockするか」を運用で統制することです。</p>
<h2 id="5-cicdをopentofu対応に置換">5. CI/CDをOpenTofu対応に置換</h2>
<p>CLI切替はローカルだけでは不十分です。実運用ではCIがapply主体です。</p>
<h3 id="51-github-actions移行例">5.1 GitHub Actions移行例</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><span 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></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">iac-plan</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">pull_request</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">paths</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;infra/**&#34;</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">plan</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</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 style="color:#f92672">uses</span>: <span style="color:#ae81ff">opentofu/setup-opentofu@v1</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">tofu_version</span>: <span style="color:#ae81ff">1.8.5</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tofu init -input=false</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tofu validate</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tofu plan -input=false -no-color</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは、<code>tofu_version</code> を固定し、突然のツール更新を防ぐことです。</p>
<h3 id="52-planの品質ゲート">5.2 Planの品質ゲート</h3>
<ul>
<li><code>tofu fmt -check</code></li>
<li><code>tofu validate</code></li>
<li><code>tflint</code></li>
<li><code>checkov</code> や <code>tfsec</code> によるセキュリティ検査</li>
</ul>
<p>この4点をPRゲート化すると、移行直後の品質低下を防げます。</p>
<h2 id="6-段階移行の実践フロー">6. 段階移行の実践フロー</h2>
<p>本番で安全に移行するなら、次の順番が現実的です。</p>
<ol>
<li>dev workspaceをOpenTofuに切替</li>
<li>1週間運用し、差分・事故を記録</li>
<li>stg workspaceを切替</li>
<li>本番はメンテウィンドウで切替</li>
<li>切替後48時間は変更凍結（緊急以外apply禁止）</li>
</ol>
<p>移行で一番危険なのは「切替当日に通常の機能変更を混ぜる」ことです。必ず分離してください。</p>
<h2 id="7-よくある障害と対策">7. よくある障害と対策</h2>
<h3 id="71-plan-差分が毎回揺れる">7.1 <code>plan</code> 差分が毎回揺れる</h3>
<p>原因:</p>
<ul>
<li>data sourceに非決定的値（timestamp等）が混在</li>
<li>providerバージョン不統一</li>
</ul>
<p>対策:</p>
<ul>
<li>変動値をlocalsに隔離</li>
<li>provider lockを全環境で統一</li>
</ul>
<h3 id="72-import済み資源が再作成扱いになる">7.2 import済み資源が再作成扱いになる</h3>
<p>原因:</p>
<ul>
<li>module path変更</li>
<li>resource address変更（<code>count</code>→<code>for_each</code>）</li>
</ul>
<p>対策:</p>
<ul>
<li><code>moved</code> ブロックを使い論理移動を明示</li>
<li>必要時のみ <code>state mv</code> を計画的に実施</li>
</ul>
<h3 id="73-lock解放漏れでapplyが詰まる">7.3 lock解放漏れでapplyが詰まる</h3>
<p>原因:</p>
<ul>
<li>CI異常終了</li>
<li>手動中断</li>
</ul>
<p>対策:</p>
<ul>
<li>lock timeoutを定義</li>
<li>強制unlock手順をRunbook化</li>
<li>Slack通知でlock継続を可視化</li>
</ul>
<h2 id="8-運用runbookに必ず入れる項目">8. 運用Runbookに必ず入れる項目</h2>
<ul>
<li>OpenTofuバージョン更新手順</li>
<li>backend障害時の復旧手順</li>
<li>lock競合時の対応フロー</li>
<li>監査ログ（誰がいつapplyしたか）</li>
<li>ロールバック条件（差分件数・重大リスク判定）</li>
</ul>
<p>「ツール移行成功」は、CLIが動くことではなく、運用が回ることです。</p>
<h2 id="9-具体的な移行チェックリスト">9. 具体的な移行チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> <code>.terraform.lock.hcl</code> を全リポジトリで固定</li>
<li><input disabled="" type="checkbox"> OpenTofu版CIテンプレート作成</li>
<li><input disabled="" type="checkbox"> dev/stg/prodの順に移行計画作成</li>
<li><input disabled="" type="checkbox"> stateバックアップの自動化</li>
<li><input disabled="" type="checkbox"> lock競合監視の通知導入</li>
<li><input disabled="" type="checkbox"> apply実行権限を限定</li>
<li><input disabled="" type="checkbox"> 切替後の変更凍結ルールを明文化</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>TerraformからOpenTofuへの移行は、単純なCLI置換ではなく <strong>IaC運用基盤の再整備</strong> です。</p>
<ul>
<li>差分原因を分離して検証する</li>
<li>state保護を最優先に設計する</li>
<li>CIとRunbookをセットで更新する</li>
<li>段階移行でリスクを局所化する</li>
</ul>
<p>この4点を守れば、既存システムを止めずに移行できます。まずはdev環境で「同一plan再現性」を検証し、そこから本番へ広げるのが最短かつ安全です。</p>
<h2 id="付録-切替当日の実行チェック時系列">付録: 切替当日の実行チェック（時系列）</h2>
<p>移行当日は、作業手順よりも「順番管理」が事故防止に効きます。以下は実務で使える時系列テンプレートです。</p>
<ul>
<li><strong>T-30分</strong>: 変更凍結を全チームへ告知、未マージPRを棚卸し</li>
<li><strong>T-20分</strong>: <code>tofu version</code> と lockfile整合性確認、CI変数最終確認</li>
<li><strong>T-10分</strong>: backend到達確認、stateバックアップ取得</li>
<li><strong>T+0分</strong>: dev→stg→prodの順で <code>tofu plan</code> 実行（差分をレビュー）</li>
<li><strong>T+15分</strong>: 許容差分のみ <code>tofu apply</code> 実行、結果を監査ログへ転記</li>
<li><strong>T+30分</strong>: 主要監視（APIエラー率、レイテンシ、コスト）を15分監視</li>
</ul>
<p>さらに、切替時に最も有効なのは「中止判断ライン」を先に決めることです。例えば、<code>plan差分件数が想定の2倍以上</code> や <code>apply失敗が2連続</code> の場合は即時中断し、旧フローへ戻す、といった基準を明文化しておくと現場判断がぶれません。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>OpenTofu</category>
      <category>Terraform</category>
      <category>IaC</category>
      <category>DevOps</category>
      <category>Platform Engineering</category>
    </item>
    <item>
      <title>Kyvernoで始めるKubernetes Admission Policy実践: 事故を減らすポリシー設計プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-06-kubernetes-admission-policy-kyverno-playbook/</link>
      <pubDate>Fri, 06 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-06-kubernetes-admission-policy-kyverno-playbook/</guid>
      <description>KubernetesでKyvernoを使い、現場で運用可能なAdmission Policyを段階導入するための実装手順とトラブル対応を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="kyvernoで始めるkubernetes-admission-policy実践-事故を減らすポリシー設計プレイブック">Kyvernoで始めるKubernetes Admission Policy実践: 事故を減らすポリシー設計プレイブック</h1>
<p>Kubernetes運用で一番つらい事故は、クラスタが壊れるよりも「本来防げたはずのミスがそのまま本番へ入る」ことです。たとえば、<code>latest</code> タグのイメージが本番に入り再現不能になる、<code>resources</code> 未設定でノードが詰まる、<code>privileged</code> コンテナが混入する。これらは人の注意力だけに依存すると必ず再発します。</p>
<p>そこで有効なのが Admission Policy（入場制御）です。本記事では <strong>Kyverno</strong> を使って、現場で本当に運用できるポリシー群を段階導入する手順をまとめます。単なる「denyの例」ではなく、監査→警告→強制の移行、例外管理、CI連携まで含めて解説します。</p>
<h2 id="1-なぜkyvernoなのか">1. なぜKyvernoなのか</h2>
<p>OPA Gatekeeper も強力ですが、Kyvernoは以下の特徴があり、初期導入が比較的スムーズです。</p>
<ul>
<li>YAML中心で書ける（Rego学習コストを後回しにしやすい）</li>
<li>validate / mutate / generate / verifyImages を一貫して扱える</li>
<li>PolicyReportにより違反可視化がしやすい</li>
<li>Pod SecurityやSupply Chain対策との相性が良い</li>
</ul>
<p>「まずルールを回し始める」目的なら、Kyvernoは現実的な選択肢です。</p>
<h2 id="2-先に決めるべき設計原則">2. 先に決めるべき設計原則</h2>
<p>導入前に、以下だけは先に決めておきます。</p>
<ol>
<li><strong>導入フェーズ</strong>: <code>Audit</code> → <code>Enforce</code> を基本にする</li>
<li><strong>責任分界</strong>: プラットフォームチームが共通ポリシー、各チームがアプリ固有例外</li>
<li><strong>例外の期限</strong>: 永久例外は禁止。期限付きで必ず棚卸し</li>
<li><strong>観測性</strong>: 違反数・対象Namespace・上位違反ルールをダッシュボード化</li>
</ol>
<p>この原則なしにルールだけ増やすと、運用が破綻します。</p>
<h2 id="3-最小導入手順3060分">3. 最小導入手順（30〜60分）</h2>
<h3 id="31-kyvernoのインストール">3.1 Kyvernoのインストール</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></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>helm repo add kyverno https://kyverno.github.io/kyverno/
</span></span><span style="display:flex;"><span>helm repo update
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>helm upgrade --install kyverno kyverno/kyverno <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -n kyverno --create-namespace <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set admissionController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set backgroundController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set cleanupController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set reportsController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>本番では可用性のため、admission/backgroundは最低2レプリカ推奨です。</p>
<h3 id="32-まずはauditモードで3ルール">3.2 まずはAuditモードで3ルール</h3>
<p>最初に効くルールは、次の3つです。</p>
<ul>
<li>イメージタグに <code>latest</code> を禁止</li>
<li>CPU/Memory requests/limits必須</li>
<li><code>privileged: true</code> を禁止</li>
</ul>
<p>例: <code>latest</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></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">kyverno.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterPolicy</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">disallow-latest-tag</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">validationFailureAction</span>: <span style="color:#ae81ff">Audit</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">background</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">validate-image-tag</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">kinds</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#ae81ff">Pod</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">validate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;latestタグは禁止です。固定タグまたはdigestを使用してください。&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">foreach</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">list</span>: <span style="color:#e6db74">&#34;request.object.spec.containers&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">deny</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">conditions</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>                  - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;{{ element.image }}&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">Matches</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;.*:latest$&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="33-レポートで現状把握">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 get policyreport -A
</span></span><span style="display:flex;"><span>kubectl get clusterpolicy
</span></span><span style="display:flex;"><span>kubectl describe clusterpolicy disallow-latest-tag
</span></span></code></pre></td></tr></table>
</div>
</div><p>導入直後は違反が大量に出るのが普通です。ここで「Kyvernoが厳しすぎる」と判断しないでください。違反は“見えていなかった負債”です。</p>
<h2 id="4-実務で効くルールセット具体例">4. 実務で効くルールセット（具体例）</h2>
<h3 id="41-リソース未設定防止">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><span 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></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">kyverno.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterPolicy</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">require-resources</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">validationFailureAction</span>: <span style="color:#ae81ff">Audit</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">check-resources</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">kinds</span>: [<span style="color:#e6db74">&#34;Pod&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">validate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;全コンテナにrequests/limitsを設定してください。&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pattern</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">containers</span>:
</span></span><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;?*&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;?*&#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;?*&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;?*&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="42-特権設定防止">4.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><span 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></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">kyverno.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterPolicy</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">disallow-privileged</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">validationFailureAction</span>: <span style="color:#ae81ff">Enforce</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#66d9ef">no</span>-<span style="color:#ae81ff">privileged</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">kinds</span>: [<span style="color:#e6db74">&#34;Pod&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">validate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;privilegedコンテナは禁止です。&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pattern</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">containers</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">=(privileged)</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="43-digest固定の推奨supply-chain">4.3 Digest固定の推奨（Supply Chain）</h3>
<p>本来は digest 固定が理想です。移行期は <code>Audit</code> で始め、違反率が下がってから <code>Enforce</code> に切り替えます。</p>
<h2 id="5-auditからenforceへ移行する基準">5. AuditからEnforceへ移行する基準</h2>
<p>「何となく」で切り替えると炎上します。次の客観指標を使うと安全です。</p>
<ul>
<li>直近14日で対象ポリシー違反率が5%未満</li>
<li>主要Namespace（prod/stg/shared）で違反ゼロ</li>
<li>例外申請フロー（Issue/PRテンプレート）が整備済み</li>
<li>当番がトラブル時に切り戻し手順を理解している</li>
</ul>
<p><code>validationFailureAction</code> を一括で上げるのではなく、ルール単位・Namespace単位で段階化するのがポイントです。</p>
<h2 id="6-例外運用のテンプレート">6. 例外運用のテンプレート</h2>
<p>ポリシー導入で最も壊れるのは「例外管理」です。おすすめは以下。</p>
<ul>
<li>例外は <code>PolicyException</code> で明示</li>
<li>期限（例: 14日）を必須にする</li>
<li>チケット番号をannotationで必須化</li>
<li>期限切れを毎日バッチで通知</li>
</ul>
<p>例外YAMLに <code>expires</code> と <code>owner</code> を必須化すると、放置率が一気に下がります。</p>
<h2 id="7-ciに組み込んで本番前に落とす">7. CIに組み込んで“本番前に落とす”</h2>
<p>クラスタ投入時に拒否されるより、PR段階で検知される方が開発体験は良いです。<code>kyverno-cli</code> をCIで実行します。</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>kyverno apply policies/ -r manifests/ --audit-warn
</span></span></code></pre></td></tr></table>
</div>
</div><p>GitHub Actions例:</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-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Validate manifests with Kyverno</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    kyverno version
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    kyverno apply ./policies -r ./k8s --audit-warn</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これで「マージ後に初めて失敗する」パターンを減らせます。</p>
<h2 id="8-よくある障害と対処">8. よくある障害と対処</h2>
<h3 id="症状1-正常なpodまで拒否される">症状1: 正常なPodまで拒否される</h3>
<ul>
<li>match条件が広すぎる可能性</li>
<li><code>exclude</code> で <code>kube-system</code> や監視系を一時除外</li>
<li>まずAuditで影響範囲を確認してからEnforceへ</li>
</ul>
<h3 id="症状2-admission-timeoutでデプロイ遅延">症状2: Admission timeoutでデプロイ遅延</h3>
<ul>
<li>Kyvernoコンポーネントのリソース不足を確認</li>
<li>policy数が多い場合、ルール統合とmatch最適化</li>
<li>API Serverとのネットワーク遅延確認</li>
</ul>
<h3 id="症状3-レポートは出るが改善が進まない">症状3: レポートは出るが改善が進まない</h3>
<ul>
<li>違反上位3ルールに絞ってKPI化</li>
<li>チーム別に違反件数を見える化</li>
<li>例外を期限付きに強制</li>
</ul>
<h2 id="9-運用を続けるためのダッシュボード指標">9. 運用を続けるためのダッシュボード指標</h2>
<p>最低限、以下を可視化してください。</p>
<ul>
<li>ルール別違反件数（7日移動平均）</li>
<li>Namespace別違反件数</li>
<li><code>Audit</code> と <code>Enforce</code> の比率</li>
<li>期限切れ例外の件数</li>
<li>deploy失敗要因のうちpolicy起因の割合</li>
</ul>
<p>これを見ないと、ポリシーは「導入しただけ」で止まります。</p>
<h2 id="10-実践ロードマップ最初の4週間">10. 実践ロードマップ（最初の4週間）</h2>
<ul>
<li><strong>Week 1</strong>: Kyverno導入、3ルールをAuditで開始</li>
<li><strong>Week 2</strong>: 違反上位を改善、例外テンプレート導入</li>
<li><strong>Week 3</strong>: 一部NamespaceでEnforce化</li>
<li><strong>Week 4</strong>: CI連携完了、SLOに違反率を組み込み</li>
</ul>
<p>4週間で「人頼みのレビュー文化」から「仕組みで防ぐ文化」へ移行できます。</p>
<h2 id="まとめ">まとめ</h2>
<p>Kyverno導入の本質は、Kubernetesを縛ることではなく、<strong>再発するミスを設計で減らすこと</strong>です。</p>
<ul>
<li>最初はAuditで可視化</li>
<li>指標を持って段階的にEnforce</li>
<li>例外は期限付きで管理</li>
<li>CIに前倒し検知を組み込む</li>
</ul>
<p>この4点を守れば、ポリシーは“開発を止める壁”ではなく“事故を減らすガードレール”になります。まずは <code>latest</code> 禁止とリソース必須の2ルールから始めて、違反データを見ながら育てていきましょう。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>Kyverno</category>
      <category>Security</category>
      <category>Platform Engineering</category>
      <category>Policy as Code</category>
    </item>
    <item>
      <title>FastAPI &#43; Celery信頼性設計: 非同期ジョブを本番で壊さないための実装パターン</title>
      <link>https://www.ai2core.com/posts/2026-03-06-fastapi-celery-reliability-patterns/</link>
      <pubDate>Fri, 06 Mar 2026 09:03:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-06-fastapi-celery-reliability-patterns/</guid>
      <description>FastAPIとCeleryを使った非同期処理を本番運用するために、再実行安全性、監視、失敗復旧、デプロイ戦略を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi--celery信頼性設計-非同期ジョブを本番で壊さないための実装パターン">FastAPI + Celery信頼性設計: 非同期ジョブを本番で壊さないための実装パターン</h1>
<p>FastAPIでAPIを作ると、重い処理はすぐに非同期ジョブへ逃がしたくなります。画像変換、レポート生成、外部API連携、メール配信など、Celeryは非常に便利です。ですが、本番で問題になるのは「動くかどうか」ではなく、<strong>失敗したときに壊れないか</strong> です。</p>
<ul>
<li>同じジョブが二重実行される</li>
<li>一時障害で永遠にリトライしてキューが詰まる</li>
<li>ワーカー再起動で中途半端な状態が残る</li>
<li>完了通知が先に飛んで実データがない</li>
</ul>
<p>本記事では FastAPI + Celery + Redis 構成を前提に、再実行安全性（idempotency）と運用信頼性を上げる実装手順をまとめます。</p>
<h2 id="1-まず守るべき設計原則">1. まず守るべき設計原則</h2>
<p>非同期基盤の事故は、ほぼ次の4原則で防げます。</p>
<ol>
<li><strong>At-least-once前提</strong>（同一タスク再実行は必ず起こる）</li>
<li><strong>副作用は冪等化</strong>（何回実行されても結果が壊れない）</li>
<li><strong>状態遷移を明示</strong>（PENDING/RUNNING/SUCCEEDED/FAILED）</li>
<li><strong>失敗を可観測化</strong>（リトライ回数・死活・滞留時間を計測）</li>
</ol>
<p>この原則を外すと、障害時に「何が完了して何が未完了か」が追えなくなります。</p>
<h2 id="2-参照アーキテクチャ">2. 参照アーキテクチャ</h2>
<ul>
<li>API: FastAPI</li>
<li>Queue Broker: Redis</li>
<li>Worker: Celery</li>
<li>Result Store: PostgreSQL（業務状態）</li>
<li>Monitoring: Flower + Prometheus + Sentry</li>
</ul>
<p>ポイントは、<strong>業務上重要な状態はRedis結果バックエンドに依存しない</strong> ことです。Redisは一時的に使い、真実の状態はRDBに持たせます。</p>
<h2 id="3-実装の土台-タスク受付api">3. 実装の土台: タスク受付API</h2>
<h3 id="31-受け付け時に-idempotency_key-を必須化">3.1 受け付け時に idempotency_key を必須化</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><span 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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> FastAPI, HTTPException
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> BaseModel
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> select
</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>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">JobRequest</span>(BaseModel):
</span></span><span style="display:flex;"><span>    idempotency_key: str
</span></span><span style="display:flex;"><span>    report_type: str
</span></span><span style="display:flex;"><span>    user_id: str
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.post</span>(<span style="color:#e6db74">&#34;/reports&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">create_report</span>(req: JobRequest):
</span></span><span style="display:flex;"><span>    existing <span style="color:#f92672">=</span> find_job_by_key(req<span style="color:#f92672">.</span>idempotency_key)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> existing:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;job_id&#34;</span>: existing<span style="color:#f92672">.</span>id, <span style="color:#e6db74">&#34;status&#34;</span>: existing<span style="color:#f92672">.</span>status}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    job <span style="color:#f92672">=</span> create_job_record(
</span></span><span style="display:flex;"><span>        idempotency_key<span style="color:#f92672">=</span>req<span style="color:#f92672">.</span>idempotency_key,
</span></span><span style="display:flex;"><span>        status<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;PENDING&#34;</span>,
</span></span><span style="display:flex;"><span>        report_type<span style="color:#f92672">=</span>req<span style="color:#f92672">.</span>report_type,
</span></span><span style="display:flex;"><span>        user_id<span style="color:#f92672">=</span>req<span style="color:#f92672">.</span>user_id,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    generate_report<span style="color:#f92672">.</span>delay(job<span style="color:#f92672">.</span>id)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;job_id&#34;</span>: job<span style="color:#f92672">.</span>id, <span style="color:#e6db74">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;PENDING&#34;</span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>これでクライアント再送が来てもジョブ多重作成を防げます。</p>
<h3 id="32-db制約で最終防衛線を張る">3.2 DB制約で最終防衛線を張る</h3>
<p><code>idempotency_key</code> に UNIQUE 制約を入れ、アプリバグ時も二重作成を防ぎます。</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> async_jobs <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">CONSTRAINT</span> uq_async_jobs_idempotency <span style="color:#66d9ef">UNIQUE</span> (idempotency_key);
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="4-celeryタスクの実践設定">4. Celeryタスクの実践設定</h2>
<h3 id="41-推奨設定">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><span 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></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> celery <span style="color:#f92672">import</span> Celery
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>celery_app <span style="color:#f92672">=</span> Celery(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;worker&#34;</span>,
</span></span><span style="display:flex;"><span>    broker<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;redis://redis:6379/0&#34;</span>,
</span></span><span style="display:flex;"><span>    backend<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;redis://redis:6379/1&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>celery_app<span style="color:#f92672">.</span>conf<span style="color:#f92672">.</span>update(
</span></span><span style="display:flex;"><span>    task_acks_late<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    task_reject_on_worker_lost<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    worker_prefetch_multiplier<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>    task_time_limit<span style="color:#f92672">=</span><span style="color:#ae81ff">900</span>,
</span></span><span style="display:flex;"><span>    task_soft_time_limit<span style="color:#f92672">=</span><span style="color:#ae81ff">840</span>,
</span></span><span style="display:flex;"><span>    task_default_retry_delay<span style="color:#f92672">=</span><span style="color:#ae81ff">30</span>,
</span></span><span style="display:flex;"><span>    task_routes<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;tasks.generate_report&#34;</span>: {<span style="color:#e6db74">&#34;queue&#34;</span>: <span style="color:#e6db74">&#34;reports&#34;</span>}},
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li><code>acks_late=True</code>: 実行完了後にACK。途中クラッシュ時は再配信</li>
<li><code>prefetch_multiplier=1</code>: 取り込み過多を防ぎ、偏りを減らす</li>
<li>time limit: ハング抑止</li>
</ul>
<h3 id="42-リトライは指数バックオフ--上限">4.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></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:#a6e22e">@celery_app.task</span>(
</span></span><span style="display:flex;"><span>    bind<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    autoretry_for<span style="color:#f92672">=</span>(TemporaryExternalError,),
</span></span><span style="display:flex;"><span>    retry_backoff<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    retry_backoff_max<span style="color:#f92672">=</span><span style="color:#ae81ff">300</span>,
</span></span><span style="display:flex;"><span>    retry_jitter<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    max_retries<span style="color:#f92672">=</span><span style="color:#ae81ff">7</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">generate_report</span>(self, job_id: str):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>無制限リトライは障害増幅装置です。必ず上限を設定します。</p>
<h2 id="5-冪等タスクの実装パターン">5. 冪等タスクの実装パターン</h2>
<h3 id="51-状態遷移をトランザクションで管理">5.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><span 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></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">start_job</span>(job_id: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> session<span style="color:#f92672">.</span>begin():
</span></span><span style="display:flex;"><span>        job <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>get(Job, job_id, with_for_update<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> job<span style="color:#f92672">.</span>status <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;RUNNING&#34;</span>, <span style="color:#e6db74">&#34;SUCCEEDED&#34;</span>):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>        job<span style="color:#f92672">.</span>status <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;RUNNING&#34;</span>
</span></span><span style="display:flex;"><span>        job<span style="color:#f92672">.</span>started_at <span style="color:#f92672">=</span> utcnow()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">True</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>FOR UPDATE</code> を使い、同時実行で状態が競合しないようにします。</p>
<h3 id="52-副作用前に実行済みチェック">5.2 副作用前に“実行済みチェック”</h3>
<p>外部API呼び出しやファイル生成前に、既に成果物が存在するか確認します。</p>
<ul>
<li>既に同名レポートが生成済みならスキップ</li>
<li>外部通知は送信履歴テーブルで重複防止</li>
<li>決済や課金は必ず業務ID単位で一意化</li>
</ul>
<h3 id="53-完了処理はcompare-and-setで確定">5.3 完了処理はCompare-and-Setで確定</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></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">complete_job</span>(job_id: str, result_url: str):
</span></span><span style="display:flex;"><span>    updated <span style="color:#f92672">=</span> session<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 async_jobs
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        SET status=&#39;SUCCEEDED&#39;, result_url=:url, finished_at=now()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        WHERE id=:id AND status=&#39;RUNNING&#39;
</span></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">&#34;id&#34;</span>: job_id, <span style="color:#e6db74">&#34;url&#34;</span>: result_url},
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> updated<span style="color:#f92672">.</span>rowcount <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これで二重完了更新を防げます。</p>
<h2 id="6-失敗時の設計dlq相当の運用">6. 失敗時の設計（DLQ相当の運用）</h2>
<p>Celeryに“標準DLQ”はありませんが、実運用では次の形で代替できます。</p>
<ul>
<li>リトライ上限超過時に <code>FAILED_PERMANENT</code> へ遷移</li>
<li>失敗理由とスタックトレースをDB保存</li>
<li>再実行API（手動リカバリ）を提供</li>
<li>重大失敗はSentry + Pagerで通知</li>
</ul>
<p>この構成で「黙って死ぬジョブ」をなくせます。</p>
<h2 id="7-監視設計最低限">7. 監視設計（最低限）</h2>
<h3 id="メトリクス">メトリクス</h3>
<ul>
<li>キュー滞留数（queue length）</li>
<li>oldest message age</li>
<li>タスク成功率 / 失敗率</li>
<li>p95 実行時間</li>
<li>リトライ回数分布</li>
</ul>
<h3 id="アラート例">アラート例</h3>
<ul>
<li>滞留数が通常の3倍を10分継続</li>
<li>失敗率 &gt; 5% が15分継続</li>
<li>oldest message age &gt; 20分</li>
<li>worker heartbeat消失</li>
</ul>
<p>「CPU高い」より「キューが古い」がユーザー影響に直結します。</p>
<h2 id="8-デプロイ時の落とし穴">8. デプロイ時の落とし穴</h2>
<h3 id="81-ローリング更新での重複実行">8.1 ローリング更新での重複実行</h3>
<ul>
<li><code>acks_late</code> + graceful shutdown を設定</li>
<li><code>TERM</code> 後にタスク完了待ち時間を確保</li>
<li>長時間ジョブは分割し、中断耐性を持たせる</li>
</ul>
<h3 id="82-スキーマ変更の順序">8.2 スキーマ変更の順序</h3>
<p>非同期基盤では、ワーカーとAPIが異なるバージョンで同居します。</p>
<p>安全な順序:</p>
<ol>
<li>先に後方互換なDB変更を適用</li>
<li>ワーカーを先に更新</li>
<li>APIを更新</li>
<li>非互換削除は次リリースで</li>
</ol>
<p>これを守らないと、古いタスクが新スキーマで失敗します。</p>
<h2 id="9-ローカルステージングでの検証手順">9. ローカル・ステージングでの検証手順</h2>
<ol>
<li>正常系: ジョブ作成→完了→結果取得</li>
<li>再送系: 同一 <code>idempotency_key</code> で2回POST</li>
<li>障害系: 外部APIタイムアウトを強制しリトライ確認</li>
<li>クラッシュ系: 実行中にworker再起動し再配信確認</li>
<li>負荷系: 1000ジョブ投入で滞留時間と失敗率確認</li>
</ol>
<p>この5ケースを自動テストに入れるだけで、運用品質は大幅に上がります。</p>
<h2 id="10-本番チェックリスト">10. 本番チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> idempotency_key のUNIQUE制約あり</li>
<li><input disabled="" type="checkbox"> 冪等な状態遷移実装（RUNNING/SUCCEEDED）</li>
<li><input disabled="" type="checkbox"> リトライ上限 + バックオフ設定済み</li>
<li><input disabled="" type="checkbox"> 手動再実行導線あり</li>
<li><input disabled="" type="checkbox"> 失敗通知（Sentry/Pager）有効</li>
<li><input disabled="" type="checkbox"> 滞留監視とアラート運用あり</li>
<li><input disabled="" type="checkbox"> デプロイ手順に互換性ルール明記</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>FastAPI + Celeryの本質は、非同期化そのものではなく <strong>失敗しても壊れない設計</strong> にあります。</p>
<ul>
<li>At-least-once を前提に設計する</li>
<li>冪等性をDB制約と状態遷移で担保する</li>
<li>リトライと監視を“運用可能”な形で実装する</li>
<li>デプロイ時のバージョン混在を想定する</li>
</ul>
<p>ここまで作り込むと、ジョブ基盤は「たまに落ちるブラックボックス」から「予測可能に運用できるインフラ」へ変わります。まずは <code>idempotency_key</code> と状態遷移の明確化から始めるのがおすすめです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>Celery</category>
      <category>Redis</category>
      <category>Python</category>
      <category>Reliability</category>
    </item>
    <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>GitHub Actions OIDCで実現する鍵レス本番デプロイ：漏えい事故を減らす実装プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-05-github-actions-oidc-secure-deploy-playbook/</link>
      <pubDate>Thu, 05 Mar 2026 09:08:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-05-github-actions-oidc-secure-deploy-playbook/</guid>
      <description>長期シークレットを廃止し、GitHub Actions OIDCでAWSへ安全にデプロイするための設計・実装・監査手順を具体的に解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions-oidcで実現する鍵レス本番デプロイ漏えい事故を減らす実装プレイブック">GitHub Actions OIDCで実現する鍵レス本番デプロイ：漏えい事故を減らす実装プレイブック</h1>
<p>CI/CD の事故は、ビルドが失敗することより「漏えいしても気づけない鍵」が残り続けることのほうが深刻です。特に <code>AWS_ACCESS_KEY_ID</code> のような長期シークレットを GitHub Secrets に保存し続ける運用は、便利ですがリスクが高いです。</p>
<p>本記事では、GitHub Actions の <strong>OIDC（OpenID Connect）連携</strong>を使って、長期鍵を使わずに AWS へデプロイする実践手順をまとめます。単なる設定紹介ではなく、<strong>最小権限・ブランチ制限・監査ログ設計</strong>まで含めて、明日から本番投入できる形で説明します。</p>
<h2 id="1-まず何が危険なのか長期シークレット運用の限界">1. まず何が危険なのか：長期シークレット運用の限界</h2>
<p>従来構成では、次のような問題が起きます。</p>
<ul>
<li>Secret が漏れても検知が遅い（CIログ、誤コミット、権限の広いメンバー）</li>
<li>ローテーションが後回しになる</li>
<li>1つの鍵で複数環境へアクセスできてしまう</li>
<li>「誰のどの workflow 実行が何をしたか」が追いにくい</li>
</ul>
<p>OIDC 連携では、GitHub が発行する短命トークンを信頼し、AWS 側で一時認証情報を払い出します。つまり、<strong>保管する鍵そのものを減らす</strong>のが最大の価値です。</p>
<h2 id="2-全体アーキテクチャ">2. 全体アーキテクチャ</h2>
<p>基本フローは以下です。</p>
<ol>
<li>GitHub Actions ジョブが OIDC トークンを取得</li>
<li>AWS IAM の OIDC プロバイダとロール信頼ポリシーで検証</li>
<li>条件に一致したジョブだけ <code>AssumeRoleWithWebIdentity</code></li>
<li>一時クレデンシャルで S3/CloudFront/ECR/ECS へデプロイ</li>
</ol>
<p>ポイントは「GitHub 側の workflow 制御」だけでなく、<strong>AWS 側で repo・branch・workflow を強制する</strong>ことです。</p>
<h2 id="3-aws-側の初期設定oidc-provider--iam-role">3. AWS 側の初期設定（OIDC Provider + IAM Role）</h2>
<h3 id="31-oidc-provider-を作成">3.1 OIDC Provider を作成</h3>
<p>CLI 例（すでに存在する場合はスキップ）:</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>aws iam create-open-id-connect-provider <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --url https://token.actions.githubusercontent.com <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --client-id-list sts.amazonaws.com <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="32-信頼ポリシーを厳密化する">3.2 信頼ポリシーを厳密化する</h3>
<p>以下のように <code>sub</code> と <code>aud</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></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;Version&#34;</span>: <span style="color:#e6db74">&#34;2012-10-17&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;Statement&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Effect&#34;</span>: <span style="color:#e6db74">&#34;Allow&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Principal&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;Federated&#34;</span>: <span style="color:#e6db74">&#34;arn:aws:iam::&lt;ACCOUNT_ID&gt;:oidc-provider/token.actions.githubusercontent.com&#34;</span>
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Action&#34;</span>: <span style="color:#e6db74">&#34;sts:AssumeRoleWithWebIdentity&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Condition&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;StringEquals&#34;</span>: {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;token.actions.githubusercontent.com:aud&#34;</span>: <span style="color:#e6db74">&#34;sts.amazonaws.com&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;StringLike&#34;</span>: {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;token.actions.githubusercontent.com:sub&#34;</span>: <span style="color:#e6db74">&#34;repo:your-org/your-repo:ref:refs/heads/main&#34;</span>
</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></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>sub</code> を <code>repo:org/repo:*</code> のように広く取りすぎると、意図しない workflow からも引き受ける可能性があり危険です。</p>
<h3 id="33-デプロイ権限ポリシーを分離する">3.3 デプロイ権限ポリシーを分離する</h3>
<p>「ロール1個に全部盛り」は避けます。</p>
<ul>
<li><code>deploy-web-prod-role</code>: S3同期 + CloudFront invalidation</li>
<li><code>deploy-api-prod-role</code>: ECR push + ECS update</li>
<li><code>read-only-audit-role</code>: CloudWatch Logs / Describe 系のみ</li>
</ul>
<p>環境別（dev/stg/prod）にロールを分離すると、誤デプロイ時の被害半径が大きく減ります。</p>
<h2 id="4-github-actions-workflow-実装">4. GitHub Actions workflow 実装</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></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">deploy-web</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">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#e6db74">&#34;main&#34;</span>]
</span></span><span style="display:flex;"><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">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">deploy</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">environment</span>: <span style="color:#ae81ff">production</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">name</span>: <span style="color:#ae81ff">Configure AWS credentials via 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::&lt;ACCOUNT_ID&gt;:role/deploy-web-prod-role</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">Build</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          npm ci
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          npm run build</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">Upload to S3</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">aws s3 sync ./dist s3://example-prod-web --delete</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">Invalidate CloudFront</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">aws cloudfront create-invalidation --distribution-id E123456 --paths &#34;/*&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>重要なのは <code>permissions.id-token: write</code> を明示する点です。これがないと OIDC トークンを取得できません。</p>
<h2 id="5-事故を防ぐための実務ルール">5. 事故を防ぐための実務ルール</h2>
<h3 id="51-branch-protection-と-environment-protection-を組み合わせる">5.1 branch protection と environment protection を組み合わせる</h3>
<ul>
<li><code>main</code> 直push禁止</li>
<li>必ず PR + 1 approval</li>
<li>production environment には Required reviewers を設定</li>
<li>夜間デプロイを禁止したい場合は手動承認ステップを入れる</li>
</ul>
<h3 id="52-workflow-ファイル改変の監査">5.2 workflow ファイル改変の監査</h3>
<p><code>.github/workflows/*.yml</code> の変更は CODEOWNERS で必ずレビュアー固定にします。</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-txt" data-lang="txt"><span style="display:flex;"><span>.github/workflows/*  @platform-team
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="53-self-hosted-runner-の扱い">5.3 self-hosted runner の扱い</h3>
<p>OIDC を導入しても、runner 自体が侵害されると意味が薄れます。</p>
<ul>
<li>runner をプロジェクト共有にしない（専用化）</li>
<li>ジョブ後にワークディレクトリをクリーン</li>
<li>egress 制限をかける</li>
<li>runner グループを環境ごとに分離</li>
</ul>
<h2 id="6-トラブルシューティング">6. トラブルシューティング</h2>
<h3 id="症状1-not-authorized-to-perform-stsassumerolewithwebidentity">症状1: <code>Not authorized to perform sts:AssumeRoleWithWebIdentity</code></h3>
<p>確認ポイント:</p>
<ol>
<li><code>aud</code> が <code>sts.amazonaws.com</code> になっているか</li>
<li><code>sub</code> が実際の実行 ref と一致しているか（タグ実行でハマりやすい）</li>
<li><code>permissions.id-token: write</code> が設定されているか</li>
<li>Role ARN の typo がないか</li>
</ol>
<h3 id="症状2-main-以外で偶発的にデプロイされた">症状2: main 以外で偶発的にデプロイされた</h3>
<ul>
<li>workflow の <code>on.push.branches</code> 見直し</li>
<li>IAM 信頼ポリシー <code>sub</code> を main 固定へ</li>
<li>環境保護ルール（Required reviewers）追加</li>
</ul>
<h3 id="症状3-デプロイは通るが操作が一部失敗">症状3: デプロイは通るが操作が一部失敗</h3>
<p>これは IAM 権限不足の可能性が高いです。CloudTrail の <code>eventName</code> と <code>errorCode</code> を見て、必要最小限のアクションだけ追加します。闇雲に <code>*</code> を付けないこと。</p>
<h2 id="7-段階的な移行計画既存運用からの切替">7. 段階的な移行計画（既存運用からの切替）</h2>
<p>実務では一気に切り替えず、以下の順が安全です。</p>
<ol>
<li>OIDC ロールを作成（既存シークレットは残す）</li>
<li>staging workflow だけ OIDC に切替</li>
<li>1週間監視（失敗率・デプロイ時間・CloudTrail）</li>
<li>production を OIDC 化</li>
<li>最後に長期シークレットを削除</li>
</ol>
<p>削除前に「どの workflow がどの secret を参照しているか」を grep で確認しておくと事故が減ります。</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>git grep -n <span style="color:#e6db74">&#34;AWS_ACCESS_KEY_ID\|AWS_SECRET_ACCESS_KEY&#34;</span> .github/workflows
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="8-監査ログ設計何を見れば安全性が上がるか">8. 監査ログ設計：何を見れば安全性が上がるか</h2>
<p>最低限、次をダッシュボード化すると運用しやすいです。</p>
<ul>
<li><code>AssumeRoleWithWebIdentity</code> 実行回数（日次）</li>
<li>失敗イベント数（権限エラー/条件不一致）</li>
<li>production deploy 実行者（workflow + sha + actor）</li>
<li>1回のデプロイで変更された主要リソース数</li>
</ul>
<p>CloudTrail + CloudWatch Logs Insights での簡易クエリ例:</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>fields <span style="color:#f92672">@</span><span style="color:#66d9ef">timestamp</span>, userIdentity.sessionContext.sessionIssuer.userName, eventName, errorCode
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> filter eventSource <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;sts.amazonaws.com&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> filter eventName <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AssumeRoleWithWebIdentity&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> sort <span style="color:#f92672">@</span><span style="color:#66d9ef">timestamp</span> <span style="color:#66d9ef">desc</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> <span style="color:#66d9ef">limit</span> <span style="color:#ae81ff">50</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="まとめ">まとめ</h2>
<p>OIDC は「設定が新しいから導入する」ものではなく、<strong>長期鍵を削減して事故確率を下げるための運用設計</strong>です。導入時にやるべきことはシンプルで、次の3つに集約できます。</p>
<ol>
<li>IAM 信頼ポリシーを repo/branch 単位で厳密化する</li>
<li>workflow 側で id-token 権限と環境保護を設定する</li>
<li>CloudTrail で AssumeRole の監査を継続する</li>
</ol>
<p>ここまで実施すれば、CI/CD のセキュリティは「頑張って守る」状態から、「漏えいしにくい仕組みで守る」状態へ進化します。まずは staging 1本から置き換えるのがおすすめです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>OIDC</category>
      <category>Security</category>
      <category>AWS</category>
      <category>CI/CD</category>
    </item>
    <item>
      <title>FastAPI認証・認可の本番設計：JWT運用、権限制御、監査ログまで含めた実装パターン</title>
      <link>https://www.ai2core.com/posts/2026-03-04-fastapi-authn-authz-production-patterns/</link>
      <pubDate>Wed, 04 Mar 2026 09:35:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-04-fastapi-authn-authz-production-patterns/</guid>
      <description>FastAPIで安全に認証・認可を実装するために、トークン設計、ローテーション、RBAC、監査、障害時運用まで具体手順で解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi認証認可の本番設計jwt運用権限制御監査ログまで含めた実装パターン">FastAPI認証・認可の本番設計：JWT運用、権限制御、監査ログまで含めた実装パターン</h1>
<p>FastAPI は実装が速い反面、認証・認可を最小構成のまま本番に出してしまい、後からセキュリティ事故に発展するケースが少なくありません。特に「JWT を入れたから安全」という誤解は危険です。</p>
<p>本記事では、<strong>開発速度を落とさずに本番で耐える認証基盤</strong>を作るための設計を、コード例と運用手順込みで解説します。</p>
<h2 id="1-認証と認可を分離して設計する">1. 認証と認可を分離して設計する</h2>
<p>最初に押さえるべきは責務分離です。</p>
<ul>
<li>認証（Authentication）: 誰かを確認する</li>
<li>認可（Authorization）: 何をしてよいか判定する</li>
</ul>
<p>この2つを混ぜると、実装も監査も破綻します。FastAPI では dependency を分け、<code>get_current_user</code> と <code>require_permission</code> を独立させるのが基本です。</p>
<h2 id="2-jwt-は短命--リフレッシュ--失効管理で使う">2. JWT は「短命 + リフレッシュ + 失効管理」で使う</h2>
<p>アクセストークンを長寿命にすると、漏えい時の被害が大きくなります。実運用では以下が標準です。</p>
<ul>
<li>Access Token: 5〜15分</li>
<li>Refresh Token: 7〜30日</li>
<li>Refresh Token は DB 保存し、ローテーション時に旧トークンを失効</li>
</ul>
<p><code>sub</code> だけでなく、<code>jti</code>（トークンID）や <code>scope</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></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> datetime <span style="color:#f92672">import</span> datetime, timedelta, timezone
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> jwt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ALGORITHM <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HS256&#34;</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">create_access_token</span>(user_id: str, scopes: list[str], secret: str) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>    now <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now(timezone<span style="color:#f92672">.</span>utc)
</span></span><span style="display:flex;"><span>    payload <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;sub&#34;</span>: user_id,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;scope&#34;</span>: <span style="color:#e6db74">&#34; &#34;</span><span style="color:#f92672">.</span>join(scopes),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;iat&#34;</span>: int(now<span style="color:#f92672">.</span>timestamp()),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;exp&#34;</span>: int((now <span style="color:#f92672">+</span> timedelta(minutes<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>))<span style="color:#f92672">.</span>timestamp()),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;jti&#34;</span>: <span style="color:#e6db74">&#34;generated-uuid&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> jwt<span style="color:#f92672">.</span>encode(payload, secret, algorithm<span style="color:#f92672">=</span>ALGORITHM)
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="3-鍵管理とローテーション">3. 鍵管理とローテーション</h2>
<p>秘密鍵を <code>.env</code> に固定して数年運用するのは典型的な事故パターンです。最低限、次を実施します。</p>
<ul>
<li>KMS/Vault など外部シークレット管理を利用</li>
<li><code>kid</code> をヘッダに持たせ、複数鍵を並行運用</li>
<li>鍵ローテーション手順を runbook 化</li>
</ul>
<p>ローテーションの要点:</p>
<ol>
<li>新鍵を追加（検証側は新旧どちらも受理）</li>
<li>発行側を新鍵へ切替</li>
<li>旧鍵の有効期限を過ぎたら削除</li>
</ol>
<p>この手順にすると、無停止で切替できます。</p>
<h2 id="4-fastapi-dependencyで認可を明示化">4. FastAPI Dependencyで認可を明示化</h2>
<p>ロジック中で <code>if user.role == &quot;admin&quot;</code> を乱立させると、抜け漏れが起こります。権限チェックは 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><span 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></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> Depends, HTTPException, status
</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">require_permission</span>(required: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">checker</span>(user<span style="color:#f92672">=</span>Depends(get_current_user)):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> required <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> user<span style="color:#f92672">.</span>permissions:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> HTTPException(
</span></span><span style="display:flex;"><span>                status_code<span style="color:#f92672">=</span>status<span style="color:#f92672">.</span>HTTP_403_FORBIDDEN,
</span></span><span style="display:flex;"><span>                detail<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;insufficient permissions&#34;</span>
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> user
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> checker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@router.delete</span>(<span style="color:#e6db74">&#34;/projects/</span><span style="color:#e6db74">{project_id}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">delete_project</span>(
</span></span><span style="display:flex;"><span>    project_id: str,
</span></span><span style="display:flex;"><span>    user<span style="color:#f92672">=</span>Depends(require_permission(<span style="color:#e6db74">&#34;project:delete&#34;</span>))
</span></span><span style="display:flex;"><span>):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ルート単位で要件が見えるため、レビュー効率と監査性が上がります。</p>
<h2 id="5-rbacabac-の使い分け">5. RBAC/ABAC の使い分け</h2>
<p>小規模なら RBAC（role-based）で十分ですが、顧客単位データや組織階層があると ABAC（属性ベース）を併用した方が安全です。</p>
<ul>
<li>RBAC: <code>admin</code>, <code>editor</code>, <code>viewer</code></li>
<li>ABAC: <code>tenant_id</code>, <code>resource_owner_id</code>, <code>department</code></li>
</ul>
<p>実務では「ロールで粗く許可し、属性で絞る」が扱いやすいです。</p>
<h2 id="6-マルチテナントで必須の防御">6. マルチテナントで必須の防御</h2>
<p>マルチテナント API では、ID 推測よりも<strong>テナント境界漏れ</strong>が主要リスクです。対策は次の通りです。</p>
<ul>
<li>すべての DB クエリに <code>tenant_id</code> 条件を必須化</li>
<li>管理者 API でも境界を明示的に超える操作だけ許可</li>
<li>監査ログに <code>tenant_id</code>, <code>actor_id</code>, <code>resource_id</code> を残す</li>
</ul>
<p>SQLAlchemy でも repository 層で共通フィルタを強制すると漏れを減らせます。</p>
<h2 id="7-監査ログを設計段階で入れる">7. 監査ログを設計段階で入れる</h2>
<p>認証系は障害後に「誰が何をしたか」が必要になります。後付けだと間に合いません。最低限、次を記録します。</p>
<ul>
<li>ログイン成功/失敗（IP, user-agent, reason）</li>
<li>権限エラー（403）</li>
<li>重要操作（削除、権限変更、請求情報更新）</li>
<li>トークン失効・再発行</li>
</ul>
<p>フォーマットは JSON 構造化に統一し、SIEM や OpenSearch に流せる形にしておくと分析が速いです。</p>
<h2 id="8-レート制限とブルートフォース対策">8. レート制限とブルートフォース対策</h2>
<p>パスワード認証がある場合、レート制限なしは危険です。<code>slowapi</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></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> slowapi <span style="color:#f92672">import</span> Limiter
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> slowapi.util <span style="color:#f92672">import</span> get_remote_address
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>limiter <span style="color:#f92672">=</span> Limiter(key_func<span style="color:#f92672">=</span>get_remote_address)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@router.post</span>(<span style="color:#e6db74">&#34;/auth/login&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@limiter.limit</span>(<span style="color:#e6db74">&#34;5/minute&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">login</span>(<span style="color:#f92672">...</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>さらに次を組み合わせると強化できます。</p>
<ul>
<li>失敗回数に応じた遅延（progressive delay）</li>
<li>CAPTCHA（必要時のみ）</li>
<li>異常IP/ASN の遮断</li>
</ul>
<h2 id="9-よくある実装ミス">9. よくある実装ミス</h2>
<h3 id="ミスa-署名検証はしているが-audiss-未検証">ミスA: 署名検証はしているが <code>aud/iss</code> 未検証</h3>
<p>結果:</p>
<ul>
<li>他システム向けトークンを誤受理</li>
</ul>
<p>対処:</p>
<ul>
<li>issuer/audience を厳格検証</li>
<li>想定外クレームは拒否</li>
</ul>
<h3 id="ミスb-refresh-token-の使い回しを検知しない">ミスB: refresh token の使い回しを検知しない</h3>
<p>結果:</p>
<ul>
<li>漏えい時に長期間乗っ取られる</li>
</ul>
<p>対処:</p>
<ul>
<li>ローテーション時に旧トークン失効</li>
<li>再利用検知時はセッション全失効</li>
</ul>
<h3 id="ミスc-認可チェックが一部エンドポイントで抜ける">ミスC: 認可チェックが一部エンドポイントで抜ける</h3>
<p>結果:</p>
<ul>
<li>水平権限昇格</li>
</ul>
<p>対処:</p>
<ul>
<li>dependency ベースで強制</li>
<li>重要ルートにセキュリティテスト追加</li>
</ul>
<h2 id="10-テスト戦略必須">10. テスト戦略（必須）</h2>
<p>認証はユニットテストだけでなく、統合テストで権限境界を確認します。</p>
<ul>
<li>有効トークン/期限切れ/改ざんトークン</li>
<li>role ごとのアクセス可否</li>
<li>tenant 越境アクセス拒否</li>
<li>refresh token 再利用検知</li>
</ul>
<p>pytest では fixture で role 別トークンを用意し、回帰を防ぎます。</p>
<h2 id="11-障害時-runbook最低限">11. 障害時 runbook（最低限）</h2>
<p>インシデント時に迷わないよう、次を文書化しておきます。</p>
<ol>
<li>鍵漏えい疑い時の全トークン失効手順</li>
<li>認証基盤障害時のフェイル動作（許可しすぎを防ぐ）</li>
<li>監査ログの検索手順</li>
<li>関係者通知テンプレート</li>
</ol>
<p>特に「認証サーバーが落ちたとき、API をどうするか」は事前に決めておく必要があります。</p>
<h2 id="12-導入チェックリスト">12. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> access token は短寿命（&lt;=15分）</li>
<li><input disabled="" type="checkbox"> refresh token は DB 管理 + ローテーション</li>
<li><input disabled="" type="checkbox"> 鍵ローテーション手順がある</li>
<li><input disabled="" type="checkbox"> ルート単位で認可 dependency が明示されている</li>
<li><input disabled="" type="checkbox"> tenant 境界を DB レイヤーで強制している</li>
<li><input disabled="" type="checkbox"> 監査ログ（認証/認可/重要操作）を構造化保存</li>
<li><input disabled="" type="checkbox"> レート制限と異常検知がある</li>
<li><input disabled="" type="checkbox"> 権限境界の統合テストがある</li>
</ul>
<p>FastAPI の認証・認可は、フレームワーク機能だけでは守り切れません。<strong>トークン寿命設計、鍵運用、境界強制、監査、テスト、runbook</strong>まで含めて初めて、本番で信頼できるセキュリティ基盤になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>Security</category>
      <category>JWT</category>
      <category>OAuth2</category>
      <category>Python</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>GitHub Actions高速化実践：Matrix戦略・依存キャッシュ・失敗切り分けの設計ガイド</title>
      <link>https://www.ai2core.com/posts/2026-03-04-github-actions-matrix-cache-strategy/</link>
      <pubDate>Wed, 04 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-04-github-actions-matrix-cache-strategy/</guid>
      <description>GitHub Actionsの実行時間と失敗率を同時に改善するためのmatrix設計、キャッシュ戦略、並列最適化、トラブルシューティング手順を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions高速化実践matrix戦略依存キャッシュ失敗切り分けの設計ガイド">GitHub Actions高速化実践：Matrix戦略・依存キャッシュ・失敗切り分けの設計ガイド</h1>
<p>GitHub Actions は便利ですが、プロジェクトが成長すると「遅い」「不安定」「原因が分かりにくい」という三重苦になりがちです。特に monorepo や複数ランタイム対応（Node/Python/Go など）では、ワークフローの設計次第で CI 時間が 2〜3 倍変わります。</p>
<p>本記事では、<strong>実行時間を短くしながら失敗時の調査コストも下げる</strong>ために、matrix 設計・キャッシュ設計・障害時の確認順序を具体的に整理します。</p>
<h2 id="1-まず何を並列化するかを決める">1. まず「何を並列化するか」を決める</h2>
<p>Actions の高速化は、いきなりキャッシュ最適化から入るより、先にジョブ分解を決める方が効きます。原則は次の通りです。</p>
<ul>
<li>並列化すべき: 独立テスト（OS/バージョン別、サービス別）</li>
<li>直列にすべき: デプロイ、DB マイグレーション、本番反映</li>
<li>依存を分ける: lint/typecheck/test/build を一つに詰め込まない</li>
</ul>
<p>悪い例は、1ジョブに全部詰め込み、失敗時に最初から再実行するパターンです。良い設計では「lint は通るが test だけ失敗」のように切り分けできます。</p>
<h2 id="2-matrix-を作るときの実践ルール">2. matrix を作るときの実践ルール</h2>
<p>matrix は便利ですが、組み合わせ爆発で逆に遅くなることがあります。例えば <code>os x runtime x db</code> をすべて直積にすると、不要なジョブが大量発生します。そこで <code>include/exclude</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></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">strategy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">fail-fast</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matrix</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">os</span>: [<span style="color:#ae81ff">ubuntu-latest, macos-latest]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">node</span>: [<span style="color:#ae81ff">20</span>, <span style="color:#ae81ff">22</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">include</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">os</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">node</span>: <span style="color:#ae81ff">22</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">coverage</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">exclude</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">os</span>: <span style="color:#ae81ff">macos-latest</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">node</span>: <span style="color:#ae81ff">20</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは次です。</p>
<ol>
<li><strong>基準環境を1つ決める</strong>（例: ubuntu + latest）</li>
<li>カバレッジ計測や重い E2E は基準環境だけで実施</li>
<li>互換性確認は軽量テスト中心にする</li>
</ol>
<p>この設計にすると、品質を落とさずに全体時間を短縮できます。</p>
<h2 id="3-キャッシュは鍵設計が9割">3. キャッシュは「鍵設計」が9割</h2>
<p><code>actions/cache</code> や <code>setup-node</code> / <code>setup-python</code> のキャッシュを入れても、キー設計が甘いとヒット率が低く、逆に復元時間だけ増えます。</p>
<p>Node.js の例:</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-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-node@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">node-version</span>: <span style="color:#ae81ff">${{ matrix.node }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#e6db74">&#39;npm&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache-dependency-path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      package-lock.json
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      packages/*/package-lock.json</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Python (pip) の例:</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-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-python@v5</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">python-version</span>: <span style="color:#e6db74">&#39;3.12&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#e6db74">&#39;pip&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache-dependency-path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      requirements.txt
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      requirements-dev.txt</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>実務で効くコツ:</p>
<ul>
<li>lockfile をキーに含める（依存変化に追従）</li>
<li>OS・ランタイムバージョンをキーに含める</li>
<li>monorepo は対象サブディレクトリ単位でキー分割</li>
<li>restore-keys を入れすぎない（古いキャッシュ復元で不整合）</li>
</ul>
<h2 id="4-concurrency-で古い実行を止める">4. concurrency で「古い実行を止める」</h2>
<p>PR に連続 push されると、古い CI が残り続けてランナー枯渇を起こします。<code>concurrency</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></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">concurrency</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">group</span>: <span style="color:#ae81ff">ci-${{ github.workflow }}-${{ github.ref }}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cancel-in-progress</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これだけで無駄実行を大きく減らせます。特にレビュー中に細かい修正を重ねるチームほど効果が高いです。</p>
<h2 id="5-paths-filter-で不要ジョブを起動しない">5. paths-filter で不要ジョブを起動しない</h2>
<p>ドキュメント更新だけなのに全テストが走る、という状態はよくあります。<code>dorny/paths-filter</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></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">uses</span>: <span style="color:#ae81ff">dorny/paths-filter@v3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id</span>: <span style="color:#ae81ff">changes</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">filters</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      backend:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        - &#39;backend/**&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      frontend:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        - &#39;frontend/**&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 例: backend が変わった時だけ実行</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">if</span>: <span style="color:#ae81ff">steps.changes.outputs.backend == &#39;true&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これにより、実行時間だけでなくランナーコストも下げられます。</p>
<h2 id="6-失敗時の調査を速くするログ設計">6. 失敗時の調査を速くするログ設計</h2>
<p>CI が遅い組織は、だいたい「失敗調査も遅い」です。改善するには、次の3点を標準化します。</p>
<ul>
<li>失敗したジョブで artifact（ログ、スクリーンショット、coverage）を必ず保存</li>
<li>重要ステップに <code>::group::</code> を付けてログを畳む</li>
<li>flaky テスト検出用に rerun 情報を残す</li>
</ul>
<p>artifact 例:</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></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">Upload test reports</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">if</span>: <span style="color:#ae81ff">always()</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-artifact@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">name</span>: <span style="color:#ae81ff">test-report-${{ matrix.os }}-${{ matrix.node }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      reports/
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      coverage/</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>if: always()</code> を忘れると、失敗時ほど証跡が残らないので注意です。</p>
<h2 id="7-self-hosted-runner-を使う場合の注意点">7. self-hosted runner を使う場合の注意点</h2>
<p>高速化目的で self-hosted runner を導入する場合、運用事故が増えやすい領域です。</p>
<ul>
<li>毎回クリーンワークスペース化（残骸で再現不能バグ）</li>
<li>シークレットを runner に永続化しない</li>
<li>パッチ適用・再起動の定期メンテをスケジュール化</li>
<li>runner ラベルを用途別に分離（deploy と test を混在させない）</li>
</ul>
<p>また、デプロイ権限を持つ runner と、PR 由来コードを実行する runner は分離するのが基本です。</p>
<h2 id="8-実際の改善ステップ2週間">8. 実際の改善ステップ（2週間）</h2>
<h3 id="day-1-2-現状計測">Day 1-2: 現状計測</h3>
<ul>
<li>平均実行時間、p95 実行時間、失敗率を取得</li>
<li>一番遅いジョブ上位3つを特定</li>
</ul>
<h3 id="day-3-5-ジョブ分割と-matrix-整理">Day 3-5: ジョブ分割と matrix 整理</h3>
<ul>
<li>lint/typecheck/test/build を分離</li>
<li>matrix の組み合わせを include/exclude で整理</li>
</ul>
<h3 id="day-6-8-キャッシュ最適化">Day 6-8: キャッシュ最適化</h3>
<ul>
<li>lockfile ベースキーへ統一</li>
<li>キャッシュヒット率を可視化</li>
</ul>
<h3 id="day-9-10-無駄実行削減">Day 9-10: 無駄実行削減</h3>
<ul>
<li>concurrency + cancel-in-progress</li>
<li>paths-filter で対象限定</li>
</ul>
<h3 id="day-11-14-運用ルール化">Day 11-14: 運用ルール化</h3>
<ul>
<li>失敗時 artifact を全ジョブ標準化</li>
<li>flaky テスト記録のテンプレート化</li>
</ul>
<p>この手順で進めると、速度改善だけでなく再発防止まで一気に整います。</p>
<h2 id="9-よくある失敗パターン">9. よくある失敗パターン</h2>
<h3 id="パターンa-キャッシュを入れたのに遅い">パターンA: キャッシュを入れたのに遅い</h3>
<p>原因:</p>
<ul>
<li>キーが細かすぎて毎回ミスヒット</li>
<li>圧縮/復元コストが大きいディレクトリを丸ごとキャッシュ</li>
</ul>
<p>対処:</p>
<ul>
<li>依存に限定してキャッシュ</li>
<li>キーに lockfile ハッシュを利用</li>
</ul>
<h3 id="パターンb-matrix-失敗がノイズ化">パターンB: matrix 失敗がノイズ化</h3>
<p>原因:</p>
<ul>
<li><code>fail-fast: true</code> で他環境の情報が取れない</li>
<li>ログ命名が統一されず比較困難</li>
</ul>
<p>対処:</p>
<ul>
<li><code>fail-fast: false</code></li>
<li>artifact 命名規則を統一</li>
</ul>
<h3 id="パターンc-pr-の待ち時間が長い">パターンC: PR の待ち時間が長い</h3>
<p>原因:</p>
<ul>
<li>古いコミットの CI が走り続ける</li>
<li>変更範囲に関係ないジョブが常時起動</li>
</ul>
<p>対処:</p>
<ul>
<li>concurrency で古い実行を停止</li>
<li>paths-filter 導入</li>
</ul>
<h2 id="10-運用チェックリスト">10. 運用チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> ジョブは責務別に分離されている</li>
<li><input disabled="" type="checkbox"> matrix は必要最小限に絞られている</li>
<li><input disabled="" type="checkbox"> 依存キャッシュのキーに lockfile が含まれる</li>
<li><input disabled="" type="checkbox"> concurrency で古い実行をキャンセルしている</li>
<li><input disabled="" type="checkbox"> 変更範囲に応じたジョブ起動制御がある</li>
<li><input disabled="" type="checkbox"> 失敗時 artifact が必ず残る</li>
</ul>
<p>GitHub Actions は「機能を使う」だけでは速くなりません。<strong>実行単位の設計、キャッシュ鍵設計、無駄実行抑制、証跡設計</strong>をセットで行うと、初めて安定した CI 基盤になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>CI</category>
      <category>DevOps</category>
      <category>Node.js</category>
      <category>Python</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>Redisキャッシュスタンピード対策ガイド：高負荷時にDBを守る設計と実装</title>
      <link>https://www.ai2core.com/posts/2026-03-02-redis-cache-stampede-mitigation-guide/</link>
      <pubDate>Mon, 02 Mar 2026 09:16:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-02-redis-cache-stampede-mitigation-guide/</guid>
      <description>Redisで発生するキャッシュスタンピードを防ぐための実践ガイド。singleflight、TTLジッター、非同期再生成を具体コード付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="redisキャッシュスタンピード対策ガイド高負荷時にdbを守る設計と実装">Redisキャッシュスタンピード対策ガイド：高負荷時にDBを守る設計と実装</h1>
<p>Redis を使っていても、ピークトラフィック時に DB が突然落ちることがあります。原因の多くはキャッシュスタンピードです。人気キーの TTL が同時に切れると、大量リクエストが一斉に DB へ流れ、接続プールが飽和します。</p>
<p>「Redis を入れたのに遅い」「ピーク時だけ 500 が増える」という現象は、このパターンで説明できることが非常に多いです。</p>
<p>本記事では、キャッシュスタンピードを実運用で防ぐために、<strong>設計原則・実装パターン・監視方法</strong>を順に解説します。</p>
<h2 id="1-キャッシュスタンピードとは何か">1. キャッシュスタンピードとは何か</h2>
<p>典型シナリオ:</p>
<ol>
<li>商品ランキング API が <code>ranking:daily</code> を Redis に 300 秒で保存</li>
<li>300 秒後、人気時間帯にキー期限切れ</li>
<li>同時に 1000 リクエストが miss</li>
<li>1000 回 DB 集計が走ってレイテンシ急増</li>
</ol>
<p>このとき Redis 自体は正常でも、背後の DB が壊れます。つまり、問題はキャッシュ障害ではなく「再生成の同時実行制御」です。</p>
<h2 id="2-防御の基本は三層構え">2. 防御の基本は三層構え</h2>
<p>スタンピード対策は単一施策では不十分です。次の三層を組み合わせると安定します。</p>
<ol>
<li><strong>同時再生成の抑制</strong>（singleflight / 分散ロック）</li>
<li><strong>期限切れの分散</strong>（TTL ジッター）</li>
<li><strong>期限切れ後の挙動制御</strong>（stale-while-revalidate）</li>
</ol>
<h2 id="3-パターン1-singleflight-で同時再生成を止める">3. パターン1: singleflight で同時再生成を止める</h2>
<p>同一キーの miss が同時発生しても、1 リクエストだけ再生成し、他は待つ設計です。</p>
<p>TypeScript 例:</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></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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">inflight</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Map</span>&lt;<span style="color:#f92672">string</span>, <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">string</span>&gt;&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getOrCompute</span>(<span style="color:#a6e22e">key</span>: <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">ttlSec</span>: <span style="color:#66d9ef">number</span>, <span style="color:#a6e22e">compute</span><span style="color:#f92672">:</span> () <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">string</span>&gt;) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">cached</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">key</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">cached</span>) <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cached</span>;
</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:#a6e22e">inflight</span>.<span style="color:#a6e22e">has</span>(<span style="color:#a6e22e">key</span>)) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">p</span> <span style="color:#f92672">=</span> (<span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">compute</span>();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">value</span>, { <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">ttlSec</span> });
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">value</span>;
</span></span><span style="display:flex;"><span>      } <span style="color:#66d9ef">finally</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">inflight</span>.<span style="color:#66d9ef">delete</span>(<span style="color:#a6e22e">key</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:#a6e22e">inflight</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">p</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">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">inflight</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">key</span>)<span style="color:#f92672">!</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>単一プロセスではこれで十分ですが、複数インスタンス構成では分散ロックも必要です。</p>
<h2 id="4-パターン2-分散ロックset-nx-ex">4. パターン2: 分散ロック（SET NX EX）</h2>
<p>複数 Pod で同時 miss が起きる場合、Redis ロックで再生成担当を 1 つに制限します。</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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lockKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`lock:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">key</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lock</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">lockKey</span>, <span style="color:#a6e22e">instanceId</span>, { <span style="color:#a6e22e">NX</span>: <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">10</span> });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">lock</span>) {
</span></span><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">const</span> <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">compute</span>();
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">value</span>, { <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">ttlSec</span> });
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#a6e22e">del</span>(<span style="color:#a6e22e">lockKey</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">value</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:#75715e">// ロック未獲得: 少し待って再取得
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">await</span> <span style="color:#a6e22e">sleep</span>(<span style="color:#ae81ff">40</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">key</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>注意点は、ロック TTL が短すぎると再生成中に失効し、二重計算になることです。処理時間の p99 を見て余裕を持って設定してください。</p>
<h2 id="5-パターン3-ttl-ジッターで期限切れを分散">5. パターン3: TTL ジッターで期限切れを分散</h2>
<p>同系統キーが同時に切れると負荷波形が尖ります。TTL にランダム幅を持たせるだけでかなり改善します。</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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ttlWithJitter</span>(<span style="color:#a6e22e">baseSec</span>: <span style="color:#66d9ef">number</span>, <span style="color:#a6e22e">jitterSec</span>: <span style="color:#66d9ef">number</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">baseSec</span> <span style="color:#f92672">+</span> Math.<span style="color:#a6e22e">floor</span>(Math.<span style="color:#a6e22e">random</span>() <span style="color:#f92672">*</span> <span style="color:#a6e22e">jitterSec</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">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">value</span>, { <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">ttlWithJitter</span>(<span style="color:#ae81ff">300</span>, <span style="color:#ae81ff">90</span>) });
</span></span></code></pre></td></tr></table>
</div>
</div><p>300 秒固定より、300〜390 秒の分布にすると、同時失効が目に見えて減ります。</p>
<h2 id="6-パターン4-stale-while-revalidate">6. パターン4: stale-while-revalidate</h2>
<p>高可用性が重要な API では、期限切れ直後でも古い値を短時間返しつつ、裏で更新する手法が有効です。</p>
<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-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;value&#34;</span>: {<span style="color:#f92672">&#34;items&#34;</span>: [<span style="color:#960050;background-color:#1e0010">...</span>]},
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;hardExpireAt&#34;</span>: <span style="color:#ae81ff">1700000000</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;softExpireAt&#34;</span>: <span style="color:#ae81ff">1699999700</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li><code>now &lt; softExpireAt</code>: 新鮮データ</li>
<li><code>softExpireAt &lt;= now &lt; hardExpireAt</code>: 古い値を返しつつ非同期更新</li>
<li><code>hardExpireAt &lt;= now</code>: 同期再生成</li>
</ul>
<p>この方式はピーク帯の体感性能を維持しやすく、DB 保護にも強いです。</p>
<h2 id="7-失敗時フォールバックを設計する">7. 失敗時フォールバックを設計する</h2>
<p>キャッシュ再生成が失敗したときの動作を明確にしておくと、障害時の揺れが減ります。</p>
<p>推奨順序:</p>
<ol>
<li>stale データがあれば返す</li>
<li>stale もなければ軽量な代替レスポンス</li>
<li>最後に明示エラー（再試行可能）</li>
</ol>
<p>「毎回 DB にフォールバック」は危険です。障害時に DB をさらに追い込むため、回路遮断（circuit breaker）とセットで設計すべきです。</p>
<h2 id="8-監視で見るべき指標">8. 監視で見るべき指標</h2>
<p>対策の効果はメトリクスで評価します。</p>
<ul>
<li>cache hit ratio</li>
<li>key 単位の miss burst（短時間 miss 件数）</li>
<li>再生成処理の同時実行数</li>
<li>DB クエリ QPS と接続待ち時間</li>
</ul>
<p>Prometheus を使う場合、以下のようなカウンタを実装すると分析しやすいです。</p>
<ul>
<li><code>cache_requests_total{key_group, result=&quot;hit|miss|stale&quot;}</code></li>
<li><code>cache_rebuild_total{key_group, result=&quot;ok|error&quot;}</code></li>
<li><code>cache_lock_contention_total{key_group}</code></li>
</ul>
<h2 id="9-導入手順既存システム向け">9. 導入手順（既存システム向け）</h2>
<h3 id="step-1-熱いキーを特定">Step 1: 熱いキーを特定</h3>
<p>アクセス上位 20 キーを抽出し、まずそこだけ対策します。</p>
<h3 id="step-2-singleflight--ジッター導入">Step 2: singleflight + ジッター導入</h3>
<p>実装コストが低く、効果が高い組み合わせです。</p>
<h3 id="step-3-分散ロック導入">Step 3: 分散ロック導入</h3>
<p>複数インスタンス環境で必須。ロック競合率も計測する。</p>
<h3 id="step-4-stale-while-revalidate-追加">Step 4: stale-while-revalidate 追加</h3>
<p>高トラフィック API へ段階適用。UX と DB 安定性が両立しやすい。</p>
<h2 id="10-よくある失敗例">10. よくある失敗例</h2>
<h3 id="失敗1-ttl-を長くしてごまかす">失敗1: TTL を長くしてごまかす</h3>
<p>データ鮮度要件を満たせず、別の問題が出ます。期限延長は応急処置に留める。</p>
<h3 id="失敗2-分散ロックだけで安心する">失敗2: 分散ロックだけで安心する</h3>
<p>ロックが取れない側の待機戦略がないと、スパイクは残ります。待機・再取得・stale 応答まで設計が必要です。</p>
<h3 id="失敗3-miss-率しか見ない">失敗3: miss 率しか見ない</h3>
<p>miss が少なくても、特定キーへの burst が強ければ障害は起きます。キーグループ単位で観測してください。</p>
<h2 id="11-実運用チェックリスト">11. 実運用チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 熱いキーを定義し、キーグループで計測している</li>
<li><input disabled="" type="checkbox"> singleflight もしくは同等の同時実行制御がある</li>
<li><input disabled="" type="checkbox"> TTL にジッターを導入している</li>
<li><input disabled="" type="checkbox"> 分散ロックの TTL が処理時間 p99 を上回る</li>
<li><input disabled="" type="checkbox"> stale-while-revalidate の返却条件が明確</li>
<li><input disabled="" type="checkbox"> 障害時フォールバック（回路遮断含む）がある</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>Redis キャッシュスタンピード対策は、キャッシュを置くことではなく「miss 後の世界を設計する」ことです。</p>
<ul>
<li>同時再生成を止める</li>
<li>失効タイミングを分散する</li>
<li>期限切れ直後の応答を制御する</li>
</ul>
<p>この3点を実装すれば、ピーク時の DB 崩壊リスクは大幅に下げられます。まずはアクセス上位キーから段階導入し、メトリクスで効果を確認してください。仕組みとして回り始めると、トラフィック増加に対する耐性が目に見えて改善します。</p>
<h2 id="12-キャッシュキー設計の実務ポイント">12. キャッシュキー設計の実務ポイント</h2>
<p>スタンピード対策では、アルゴリズム以前にキー設計が重要です。<code>user:123:timeline</code> のような粒度が粗すぎるキーは、人気ユーザーにアクセスが集中したとき一気にホットスポットになります。可能なら <code>page</code> や <code>segment</code> を分割し、巨大レスポンスを小さく分けると miss 時の再生成コストを抑えられます。</p>
<p>また、キーの命名規約をチームで統一しておくと観測しやすくなります。<code>service:domain:resource:variant</code> 形式で揃えれば、メトリクス集計時に <code>key_group</code> を自動分類しやすく、どの領域で burst が起きているかを短時間で判断できます。運用性を上げるキー設計は、性能改善と同じくらい価値があります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Redis</category>
      <category>Caching</category>
      <category>Backend</category>
      <category>Performance</category>
    </item>
    <item>
      <title>PostgreSQLデッドロック調査プレイブック：再現・可視化・恒久対策までの実践手順</title>
      <link>https://www.ai2core.com/posts/2026-03-02-postgresql-deadlock-troubleshooting-playbook/</link>
      <pubDate>Mon, 02 Mar 2026 09:12:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-02-postgresql-deadlock-troubleshooting-playbook/</guid>
      <description>本番で発生するPostgreSQLデッドロックの調査と対処を、再現SQL・ログ設定・アプリ修正パターンまで具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="postgresqlデッドロック調査プレイブック再現可視化恒久対策までの実践手順">PostgreSQLデッドロック調査プレイブック：再現・可視化・恒久対策までの実践手順</h1>
<p>本番運用で厄介なのは、エラーが「たまに」しか出ない障害です。PostgreSQL のデッドロックはその代表で、発生頻度は低くてもビジネス影響が大きいことが多いです。決済や在庫更新で発生すると、リトライが雪だるま式に増え、アプリ全体の遅延を引き起こします。</p>
<p>本記事では、デッドロック発生時に現場でそのまま使える手順を、<strong>初動対応・再現・恒久対策</strong>の順で整理します。</p>
<h2 id="1-まず理解すべき前提">1. まず理解すべき前提</h2>
<p>デッドロックは「どちらかが悪い」ではなく、<strong>ロック順序が循環したときに必ず起きる現象</strong>です。PostgreSQL は循環を検出すると、どちらか一方のトランザクションを強制中断します。</p>
<p>典型的な症状:</p>
<ul>
<li><code>ERROR: deadlock detected</code></li>
<li>API の一部がランダムに 500 を返す</li>
<li>リトライ実装により DB 負荷が上振れ</li>
</ul>
<p>ここで重要なのは、単純なタイムアウトと混同しないことです。タイムアウトは待ち時間超過、デッドロックは循環待ちです。対策が違います。</p>
<h2 id="2-初動でやること515分">2. 初動でやること（5〜15分）</h2>
<h3 id="2-1-エラーログの採取">2-1. エラーログの採取</h3>
<p>まず、DB 側ログに詳細を出す設定があるか確認します。</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">SHOW</span> log_lock_waits;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SHOW</span> deadlock_timeout;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SHOW</span> log_min_error_statement;
</span></span></code></pre></td></tr></table>
</div>
</div><p>推奨設定（本番）:</p>
<pre tabindex="0"><code class="language-conf" data-lang="conf">log_lock_waits = on
deadlock_timeout = &#39;1s&#39;
log_min_error_statement = error
</code></pre><p><code>deadlock_timeout</code> を短めにすることで、待ちが長引いたケースの追跡がしやすくなります。</p>
<h3 id="2-2-現在のロック状況を確認">2-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><span 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>  a.pid,
</span></span><span style="display:flex;"><span>  a.usename,
</span></span><span style="display:flex;"><span>  a.application_name,
</span></span><span style="display:flex;"><span>  a.<span style="color:#66d9ef">state</span>,
</span></span><span style="display:flex;"><span>  a.query,
</span></span><span style="display:flex;"><span>  l.locktype,
</span></span><span style="display:flex;"><span>  l.<span style="color:#66d9ef">mode</span>,
</span></span><span style="display:flex;"><span>  l.<span style="color:#66d9ef">granted</span>,
</span></span><span style="display:flex;"><span>  a.query_start
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_activity a
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> pg_locks l <span style="color:#66d9ef">ON</span> a.pid <span style="color:#f92672">=</span> l.pid
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> a.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> a.query_start;
</span></span></code></pre></td></tr></table>
</div>
</div><p>見るべき点は「長く生きているトランザクション」と「<code>granted = false</code> が連鎖している箇所」です。</p>
<h2 id="3-再現手順を作る原因特定の最短ルート">3. 再現手順を作る（原因特定の最短ルート）</h2>
<p>デッドロックは再現しないと直せません。以下のような単純ケースをまず作ります。</p>
<h3 id="セッション-a">セッション A</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></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">BEGIN</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">UPDATE</span> accounts <span style="color:#66d9ef">SET</span> balance <span style="color:#f92672">=</span> balance <span style="color:#f92672">-</span> <span style="color:#ae81ff">100</span> <span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
</span></span><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> accounts <span style="color:#66d9ef">SET</span> balance <span style="color:#f92672">=</span> balance <span style="color:#f92672">+</span> <span style="color:#ae81ff">100</span> <span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">COMMIT</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="セッション-b">セッション B</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></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">BEGIN</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">UPDATE</span> accounts <span style="color:#66d9ef">SET</span> balance <span style="color:#f92672">=</span> balance <span style="color:#f92672">-</span> <span style="color:#ae81ff">50</span> <span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
</span></span><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> accounts <span style="color:#66d9ef">SET</span> balance <span style="color:#f92672">=</span> balance <span style="color:#f92672">+</span> <span style="color:#ae81ff">50</span> <span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">COMMIT</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>更新順序が逆なので、容易に循環が起きます。</p>
<p>この再現が取れたら、アプリコード上でも「同じテーブルを複数行更新する順序」が一定かどうかを調べます。</p>
<h2 id="4-原因の8割は更新順序の不一致">4. 原因の8割は「更新順序の不一致」</h2>
<p>多くのプロダクトで見つかるのは次のパターンです。</p>
<ol>
<li>バッチ処理は <code>id ASC</code> で更新</li>
<li>API リクエストは受信順で更新</li>
<li>並行処理時に順序が逆転</li>
</ol>
<p>この場合、解決策は明確で、<strong>全経路でロック取得順序を統一</strong>します。</p>
<p>例（Node.js / TypeScript）:</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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ids</span> <span style="color:#f92672">=</span> [<span style="color:#a6e22e">fromAccountId</span>, <span style="color:#a6e22e">toAccountId</span>].<span style="color:#a6e22e">sort</span>((<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span>) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">a</span> <span style="color:#f92672">-</span> <span style="color:#a6e22e">b</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> <span style="color:#a6e22e">tx</span>.<span style="color:#a6e22e">query</span>(<span style="color:#e6db74">&#39;SELECT id FROM accounts WHERE id = ANY($1) ORDER BY id FOR UPDATE&#39;</span>, [<span style="color:#a6e22e">ids</span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// その後に更新
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>FOR UPDATE</code> を先に順序付きで取得することで、アプリ層の揺らぎを DB で吸収できます。</p>
<h2 id="5-実装レベルの対策パターン">5. 実装レベルの対策パターン</h2>
<h3 id="5-1-トランザクションを短くする">5-1. トランザクションを短くする</h3>
<p>デッドロックは「ロック保持時間」が長いほど発生しやすくなります。トランザクション内で外部 API 呼び出しをしていないか確認してください。これは最優先で排除します。</p>
<h3 id="5-2-失敗時のリトライを制御する">5-2. 失敗時のリトライを制御する</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><span 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></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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">3</span>; <span style="color:#a6e22e">attempt</span><span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">runTx</span>();
</span></span><span style="display:flex;"><span>  } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">e</span>: <span style="color:#66d9ef">any</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>String(<span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">message</span>).<span style="color:#a6e22e">includes</span>(<span style="color:#e6db74">&#39;deadlock detected&#39;</span>) <span style="color:#f92672">||</span> <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">3</span>) <span style="color:#66d9ef">throw</span> <span style="color:#a6e22e">e</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">sleep</span>(<span style="color:#ae81ff">50</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">**</span> <span style="color:#a6e22e">attempt</span>);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="5-3-楽観ロックの導入">5-3. 楽観ロックの導入</h3>
<p>更新競合が多い領域では <code>version</code> カラムを使った楽観ロックが有効です。衝突時にアプリで再計算できます。</p>
<h2 id="6-監視運用面の改善">6. 監視・運用面の改善</h2>
<p>恒久対策を完了させるには、再発を検知できる状態を作る必要があります。</p>
<p>推奨メトリクス:</p>
<ul>
<li>deadlock 発生回数 / 分</li>
<li>lock wait 時間 p95</li>
<li>失敗リトライ回数</li>
<li>長時間トランザクション件数</li>
</ul>
<p>SQL 例（長時間 tx 検知）:</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, 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">AND</span> now() <span style="color:#f92672">-</span> xact_start <span style="color:#f92672">&gt;</span> interval <span style="color:#e6db74">&#39;30 seconds&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> tx_age <span style="color:#66d9ef">DESC</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>この結果を可視化し、閾値超過で通知するだけでも再発時の初動が速くなります。</p>
<h2 id="7-障害対応時の意思決定フレーム">7. 障害対応時の意思決定フレーム</h2>
<p>デッドロックが継続しているとき、現場は次の順序で判断すると迷いません。</p>
<ol>
<li>影響範囲（どの API / どの機能か）を確定</li>
<li>失敗処理を一時停止できるか判断（バッチ停止、機能フラグ）</li>
<li>ロック順序統一のホットフィックス可否</li>
<li>デプロイまでの間、リトライ制御で被害抑制</li>
</ol>
<p>「根本修正が間に合わない」ケースでも、リトライと負荷制御でユーザー影響を減らせます。</p>
<h2 id="8-よくあるアンチパターン">8. よくあるアンチパターン</h2>
<h3 id="アンチパターン1-serializable-に上げて解決した気になる">アンチパターン1: <code>SERIALIZABLE</code> に上げて解決した気になる</h3>
<p>隔離レベルを上げるだけでは、設計上の競合は消えません。むしろリトライ増加で負荷が悪化することがあります。</p>
<h3 id="アンチパターン2-select--for-update-を乱用する">アンチパターン2: <code>SELECT ... FOR UPDATE</code> を乱用する</h3>
<p>広範囲ロックは別の待ちを生みます。最小対象だけをロックし、順序を統一することが本質です。</p>
<h3 id="アンチパターン3-アプリログだけ見て-db-ログを見ない">アンチパターン3: アプリログだけ見て DB ログを見ない</h3>
<p>デッドロックの循環情報は DB 側ログにしか出ないことが多いです。必ず DB ログを一次情報として扱ってください。</p>
<h2 id="9-すぐ使えるチェックリスト">9. すぐ使えるチェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> <code>deadlock detected</code> の実例ログを保存した</li>
<li><input disabled="" type="checkbox"> ロック状況を <code>pg_stat_activity</code> と <code>pg_locks</code> で取得した</li>
<li><input disabled="" type="checkbox"> 再現 SQL を作成し、原因パターンを確認した</li>
<li><input disabled="" type="checkbox"> 更新順序を全経路で統一した</li>
<li><input disabled="" type="checkbox"> リトライ回数とバックオフを制限した</li>
<li><input disabled="" type="checkbox"> 監視メトリクスを追加した</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>PostgreSQL デッドロック対策は、魔法の設定値を探す作業ではありません。ポイントは一貫しており、</p>
<ul>
<li>ログで事実を取る</li>
<li>再現を作る</li>
<li>ロック順序を統一する</li>
</ul>
<p>この 3 ステップで大半の問題は改善します。障害対応時は焦って設定を増やすより、まず「どの順序で誰がロックを取ったか」を可視化することが最短ルートです。</p>
<h2 id="10-チーム運用に落とし込むためのルール化">10. チーム運用に落とし込むためのルール化</h2>
<p>技術対策ができても、運用ルールがないと再発します。特に効果が高いのは「PR テンプレートにロック観点を入れる」ことです。たとえば <code>複数行更新の順序は統一されているか</code>、<code>トランザクション内で外部I/Oをしていないか</code> をチェック項目にするだけで、設計時点で多くの問題を防げます。</p>
<p>さらに、負荷試験シナリオに競合ケース（同時更新）を追加してください。通常の性能試験は平均応答時間を見るだけで終わりがちですが、デッドロックは並行競合を作らないと検出できません。QA と開発が協調して「再現しにくい障害を再現するテスト」を用意できると、運用品質が一段上がります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Troubleshooting</category>
      <category>Backend</category>
    </item>
    <item>
      <title>GitHub Actions再利用ワークフロー運用設計：属人化を防ぎつつ開発速度を上げる実践ガイド</title>
      <link>https://www.ai2core.com/posts/2026-03-02-github-actions-reusable-workflows-governance/</link>
      <pubDate>Mon, 02 Mar 2026 09:08:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-02-github-actions-reusable-workflows-governance/</guid>
      <description>GitHub Actionsの再利用ワークフローを本番運用するための設計・権限管理・移行手順を、具体的なYAML例とチェックリスト付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions再利用ワークフロー運用設計属人化を防ぎつつ開発速度を上げる実践ガイド">GitHub Actions再利用ワークフロー運用設計：属人化を防ぎつつ開発速度を上げる実践ガイド</h1>
<p>複数リポジトリを運用していると、ほぼ同じ CI 設定を各リポジトリにコピーし続ける状態になりがちです。最初は早く見えますが、半年後には「どこに正解があるのかわからない」状態になります。セキュリティパッチを当てたいだけなのに 20 リポジトリを横断修正し、1つだけ取りこぼして監査で指摘される、というのは珍しくありません。</p>
<p>この問題に効くのが GitHub Actions の <code>workflow_call</code> を使った再利用ワークフローです。ただし、単に共通化するだけでは逆に運用事故が増えることがあります。重要なのは、<strong>共通化の粒度、権限境界、変更リリース方法</strong>を最初に設計することです。</p>
<p>本記事では、実運用で詰まりやすいポイントを中心に、導入から定着までを具体的に解説します。</p>
<h2 id="1-再利用ワークフローの基本方針">1. 再利用ワークフローの基本方針</h2>
<p>まず押さえるべき方針は次の 3 つです。</p>
<ol>
<li><strong>再利用ワークフローは「プラットフォーム製品」として扱う</strong></li>
<li><strong>呼び出し側（各アプリ repo）は薄く保つ</strong></li>
<li><strong>破壊的変更はバージョンを切って段階移行する</strong></li>
</ol>
<p>「共通化＝1ファイルに全部詰め込む」ではありません。lint, test, build, deploy を1個にまとめると、対象外プロジェクトまで影響します。まずは小さく分割し、必要なものだけ組み合わせられる構造にします。</p>
<h2 id="2-推奨ディレクトリ構成">2. 推奨ディレクトリ構成</h2>
<p>共通ワークフローを専用 repo に分離しておくと、監査や変更履歴の管理が容易になります。</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></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>org-ci-workflows/
</span></span><span style="display:flex;"><span>  .github/workflows/
</span></span><span style="display:flex;"><span>    ci-node.yml
</span></span><span style="display:flex;"><span>    ci-python.yml
</span></span><span style="display:flex;"><span>    security-scan.yml
</span></span><span style="display:flex;"><span>    release-tag.yml
</span></span><span style="display:flex;"><span>  docs/
</span></span><span style="display:flex;"><span>    onboarding.md
</span></span><span style="display:flex;"><span>    migration-checklist.md
</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">ci</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">pull_request</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#ae81ff">main]</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">node-ci</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">your-org/org-ci-workflows/.github/workflows/ci-node.yml@v1</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">node-version</span>: <span style="color:#e6db74">&#34;22&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">run-e2e</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">secrets</span>: <span style="color:#ae81ff">inherit</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この形の利点は、アプリチームが理解すべき YAML が減ることです。CI の標準知識が全員に伝播し、レビューも速くなります。</p>
<h2 id="3-実務で効く入力設計inputs">3. 実務で効く入力設計（inputs）</h2>
<p><code>inputs</code> は API 設計だと考えるべきです。曖昧な真偽値が増えると、挙動が読めなくなります。</p>
<h3 id="悪い例">悪い例</h3>
<ul>
<li><code>enable_special_mode: true</code></li>
<li><code>quick: true</code></li>
</ul>
<h3 id="良い例">良い例</h3>
<ul>
<li><code>test-scope: unit|integration|full</code></li>
<li><code>build-target: web|api|worker</code></li>
<li><code>upload-artifact: true|false</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><span 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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_call</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">test-scope</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">string</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>: <span style="color:#e6db74">&#34;unit&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">upload-artifact</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">boolean</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>入力名を自然言語に寄せすぎない</strong>のがコツです。将来の運用者が見て意味がぶれない命名に寄せます。</p>
<h2 id="4-セキュリティ設計permissions-を最小化する">4. セキュリティ設計：permissions を最小化する</h2>
<p>再利用ワークフローの導入後に増える事故が「権限過剰」です。<code>permissions: write-all</code> を残したまま運用すると、意図しないトークン利用で供給網リスクが高まります。</p>
<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></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">permissions</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 style="color:#f92672">pull-requests</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">actions</span>: <span style="color:#ae81ff">read</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>デプロイやタグ作成など書き込みが必要な処理だけ、専用ジョブに分離して <code>contents: write</code> を付与します。CI と CD の境界を分けるだけで、攻撃面積をかなり削減できます。</p>
<h2 id="5-キャッシュ設計でハマらないための実践">5. キャッシュ設計でハマらないための実践</h2>
<p>Actions の高速化でよく使う <code>actions/cache</code> は、キー設計を誤ると逆効果になります。</p>
<ul>
<li>キーが粗い: 依存不整合で不安定化</li>
<li>キーが細かすぎる: キャッシュヒット率低下</li>
</ul>
<p>Node の例:</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-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-node@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">node-version</span>: <span style="color:#ae81ff">${{ inputs.node-version }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#ae81ff">npm</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm ci</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm test</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>まずは <code>setup-*</code> の組み込みキャッシュを優先し、複雑な手動キャッシュは必要になってから追加する方が安全です。</p>
<h2 id="6-導入手順3週間での現実的移行">6. 導入手順（3週間での現実的移行）</h2>
<h3 id="week-1-現状分析">Week 1: 現状分析</h3>
<ol>
<li>全 repo の workflow を収集</li>
<li>共通ジョブを抽出（lint/test/build/security）</li>
<li>失敗率の高い処理を特定</li>
</ol>
<h3 id="week-2-共通-repo-構築">Week 2: 共通 repo 構築</h3>
<ol>
<li><code>org-ci-workflows</code> を作成</li>
<li>2〜3 種類の再利用ワークフローを作る</li>
<li>テンプレート repo で先行検証</li>
</ol>
<h3 id="week-3-段階移行">Week 3: 段階移行</h3>
<ol>
<li>低リスク repo から順次移行</li>
<li>失敗ログを収集し API（inputs）を微調整</li>
<li><code>v1</code> タグを固定し、全体展開</li>
</ol>
<p>重要なのは、一気に全 repo を置き換えないことです。先行導入で「どの入力が不足しているか」を把握し、後続 repo の移行コストを下げます。</p>
<h2 id="7-変更管理main-直参照を禁止する">7. 変更管理：main 直参照を禁止する</h2>
<p>呼び出し側で <code>@main</code> を使うと、共通 repo の更新が即時反映されます。これは便利ですが、本番では危険です。事故時に「いつ壊れたか」が追跡しづらくなります。</p>
<p>実運用では次を推奨します。</p>
<ul>
<li>呼び出しは <code>@v1</code> などのタグ固定</li>
<li>破壊的変更は <code>v2</code> を新規発行</li>
<li>重大修正のみ <code>v1.x</code> に backport</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-text" data-lang="text"><span style="display:flex;"><span>[CI Workflow Update]
</span></span><span style="display:flex;"><span>- Target: ci-node.yml
</span></span><span style="display:flex;"><span>- Change: npm audit step added
</span></span><span style="display:flex;"><span>- Impact: build time +20-40 sec
</span></span><span style="display:flex;"><span>- Action Required: none (v1.3.0 auto pickup)
</span></span><span style="display:flex;"><span>- Rollback: use v1.2.4
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="8-監視指標導入効果を数字で見る">8. 監視指標（導入効果を数字で見る）</h2>
<p>再利用ワークフローを入れたら、以下を最低でも計測します。</p>
<ul>
<li>CI 平均実行時間</li>
<li>PR あたり失敗回数</li>
<li>ワークフロー設定変更に伴う障害件数</li>
<li>セキュリティ警告検出率</li>
</ul>
<p>導入後 1 か月で、<code>設定差分の削減</code> と <code>障害切り分け時間の短縮</code> が見えてくるはずです。数字が出ない場合は、共通化の粒度が粗すぎる可能性があります。</p>
<h2 id="9-よくある失敗と対策">9. よくある失敗と対策</h2>
<h3 id="失敗1-共通化しすぎて柔軟性が消える">失敗1: 共通化しすぎて柔軟性が消える</h3>
<p>対策: 80%共通 + 20%ローカル上書きの構造にする。例外を許容する。</p>
<h3 id="失敗2-例外入力が増えて-api-が壊れる">失敗2: 例外入力が増えて API が壊れる</h3>
<p>対策: 四半期ごとに <code>inputs</code> を棚卸し。未使用入力を削除し、破壊的変更はメジャーバージョンへ。</p>
<h3 id="失敗3-platform-team-しか触れない">失敗3: Platform Team しか触れない</h3>
<p>対策: docs/onboarding.md を整備し、アプリチームが PR で改善できる運用にする。</p>
<h2 id="10-そのまま使える導入チェックリスト">10. そのまま使える導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 共通 repo を作成し、責任者を明確化した</li>
<li><input disabled="" type="checkbox"> <code>workflow_call</code> の入力仕様を文書化した</li>
<li><input disabled="" type="checkbox"> <code>permissions</code> を最小権限に設定した</li>
<li><input disabled="" type="checkbox"> 呼び出し側でタグ固定（@v1）を徹底した</li>
<li><input disabled="" type="checkbox"> 先行 repo で 1 週間運用検証した</li>
<li><input disabled="" type="checkbox"> ロールバック手順を README に記載した</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>GitHub Actions の再利用ワークフローは、単なる YAML 共通化ではなく、組織の開発基盤を製品として運用する取り組みです。導入の成否は、技術よりも運用設計に左右されます。</p>
<ul>
<li>入力仕様を API として設計する</li>
<li>権限を最小化し事故半径を縮小する</li>
<li>バージョン固定で変更を可観測にする</li>
</ul>
<p>この3点を守るだけで、CI/CD は「壊れやすい魔法」から「改善可能な仕組み」に変わります。まずは 1 つの再利用ワークフローから始め、3 週間で小さく成功を作るのが最短ルートです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>CI/CD</category>
      <category>DevOps</category>
      <category>Platform Engineering</category>
    </item>
    <item>
      <title>MCPサーバー本番設計ガイド：AIエージェント連携を安全・安定に運用するアーキテクチャ</title>
      <link>https://www.ai2core.com/posts/2026-03-01-mcp-server-production-architecture/</link>
      <pubDate>Sun, 01 Mar 2026 09:35:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-01-mcp-server-production-architecture/</guid>
      <description>Model Context Protocol（MCP）サーバーを本番運用するための設計指針を、権限分離、監査、失敗制御、運用チェックリストまで実装レベルで解説。</description>
      <content:encoded><![CDATA[<h1 id="mcpサーバー本番設計ガイドaiエージェント連携を安全安定に運用するアーキテクチャ">MCPサーバー本番設計ガイド：AIエージェント連携を安全・安定に運用するアーキテクチャ</h1>
<p>MCP（Model Context Protocol）は、LLM と外部ツールを接続する強力な仕組みです。便利な一方で、本番運用では「権限の過剰付与」「監査不能」「障害時の暴走」が起きやすく、設計を誤ると一気にリスクが跳ね上がります。</p>
<p>本記事では、MCP サーバーを業務利用する前提で、<strong>安全性・可観測性・運用性</strong>を満たす設計パターンをまとめます。PoC から本番へ上げる際のチェックリストとして使える構成にしています。</p>
<h2 id="1-mcp本番運用で先に決めるべきこと">1. MCP本番運用で先に決めるべきこと</h2>
<p>最初に決めるべきは、技術スタックではなく「権限境界」です。</p>
<ul>
<li>どのエージェントが、どのツールを使えるか</li>
<li>書き込み系操作（作成・更新・削除）の承認方式</li>
<li>外部送信（メール、投稿、通知）の監査ルール</li>
<li>失敗時の停止条件（fail-open か fail-closed か）</li>
</ul>
<p>ここを決めずに実装を始めると、あとから制約を入れられず、結果として運用停止になります。</p>
<h2 id="2-推奨アーキテクチャcontrol-plane-と-tool-plane-の分離">2. 推奨アーキテクチャ：Control Plane と Tool Plane の分離</h2>
<p>MCP 構成は最低でも2層に分けると安全です。</p>
<ol>
<li><strong>Control Plane</strong>: 認証、認可、監査、レート制御</li>
<li><strong>Tool Plane</strong>: 実際のツール実行（DB、GitHub、Browser、Messaging）</li>
</ol>
<h3 id="2-1-なぜ分離するのか">2-1. なぜ分離するのか</h3>
<p>Tool 実装に認可ロジックを埋め込むと、ツール追加のたびにセキュリティ品質がブレます。Control Plane で一元化すれば、ポリシー変更時も1箇所で反映できます。</p>
<h3 id="2-2-リクエストフロー例">2-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></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>Agent -&gt; MCP Gateway(Control Plane)
</span></span><span style="display:flex;"><span>      -&gt; Policy Engine (allow/deny, scope check)
</span></span><span style="display:flex;"><span>      -&gt; Tool Adapter (Tool Plane)
</span></span><span style="display:flex;"><span>      -&gt; Audit Logger
</span></span></code></pre></td></tr></table>
</div>
</div><p>deny の場合も必ず監査ログに記録し、試行の痕跡を残します。</p>
<h2 id="3-認可設計rbacだけでは足りない">3. 認可設計：RBACだけでは足りない</h2>
<p>本番では RBAC（役割）に加えて ABAC（属性）を使うと事故が減ります。</p>
<ul>
<li>RBAC: <code>writer</code>, <code>reviewer</code>, <code>admin</code></li>
<li>ABAC: 時間帯、環境（prod/staging）、対象リポジトリ、操作種別</li>
</ul>
<p>例:</p>
<ul>
<li>staging では <code>write</code> 可、prod は <code>read</code> のみ</li>
<li>23:00〜08:00 の外部送信を自動 deny</li>
<li><code>delete</code> は人間承認トークン必須</li>
</ul>
<p>このルールを YAML 等で宣言的に持つと、監査とレビューが容易になります。</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></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">policies</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">deny-night-send</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">action</span>: <span style="color:#ae81ff">message.send</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">time_between</span>: [<span style="color:#e6db74">&#34;23:00&#34;</span>, <span style="color:#e6db74">&#34;08:00&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">effect</span>: <span style="color:#ae81ff">deny</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">allow-read-docs</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">action</span>: <span style="color:#ae81ff">docs.read</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">env</span>: [<span style="color:#e6db74">&#34;prod&#34;</span>, <span style="color:#e6db74">&#34;staging&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">effect</span>: <span style="color:#ae81ff">allow</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="4-書き込み操作のガードレール">4. 書き込み操作のガードレール</h2>
<p>削除や公開投稿など不可逆操作は、二段階に分けます。</p>
<ol>
<li><strong>Planフェーズ</strong>: 何を変更するか差分提示</li>
<li><strong>Applyフェーズ</strong>: 承認トークン付きで実行</li>
</ol>
<h3 id="4-1-git操作の安全化">4-1. Git操作の安全化</h3>
<ul>
<li><code>main</code> 直 push 禁止（PR 経由のみ）</li>
<li>commit message に実行主体IDを埋め込む</li>
<li>重要ディレクトリは CODEOWNERS レビュー必須</li>
</ul>
<p>これにより「誰が」「なぜ」「どの差分を」適用したか追跡できます。</p>
<h2 id="5-可観測性最低限必要な監査ログ項目">5. 可観測性：最低限必要な監査ログ項目</h2>
<p>MCP の障害は、アプリログだけでは追えません。次を構造化ログで保存します。</p>
<ul>
<li>request_id</li>
<li>agent_id / session_id</li>
<li>tool_name</li>
<li>action（read/write/delete/send）</li>
<li>policy_decision（allow/deny）</li>
<li>latency_ms</li>
<li>result（success/failure）</li>
<li>redacted_input_hash</li>
</ul>
<p>入力本文を丸ごと保存すると個人情報漏えいリスクが高いので、ハッシュ化・マスキングが原則です。</p>
<h2 id="6-障害設計mcpが壊れても全体を止めない">6. 障害設計：MCPが壊れても全体を止めない</h2>
<h3 id="6-1-circuit-breaker-を必ず入れる">6-1. Circuit Breaker を必ず入れる</h3>
<p>下流ツール（例: GitHub API）が遅延した際、MCP 全体が巻き込まれないようにします。</p>
<ul>
<li>連続失敗 N 回で open</li>
<li>cool-down 後に half-open</li>
<li>成功確認後に close</li>
</ul>
<h3 id="6-2-タイムアウト予算">6-2. タイムアウト予算</h3>
<p>MCP リクエストは複数ツールを跨ぐため、全体予算を先に決めます。</p>
<ul>
<li>全体: 10 秒</li>
<li>認可: 500ms</li>
<li>ツール実行: 7 秒</li>
<li>ログ書き込み: 1 秒</li>
</ul>
<p>予算超過時は中断し、部分成功を明示するレスポンスを返すほうが、黙って待つより運用しやすいです。</p>
<h2 id="7-実運用チェックリスト導入前に必須">7. 実運用チェックリスト（導入前に必須）</h2>
<h3 id="セキュリティ">セキュリティ</h3>
<ul>
<li><input disabled="" type="checkbox"> デフォルト deny（明示 allow のみ）</li>
<li><input disabled="" type="checkbox"> 外部送信系に承認フローあり</li>
<li><input disabled="" type="checkbox"> APIキー/トークンのローテーション手順あり</li>
<li><input disabled="" type="checkbox"> 監査ログの改ざん防止（WORM/署名）</li>
</ul>
<h3 id="信頼性">信頼性</h3>
<ul>
<li><input disabled="" type="checkbox"> タイムアウト・再試行・サーキットブレーカ設定済み</li>
<li><input disabled="" type="checkbox"> ツールごとのレート制限あり</li>
<li><input disabled="" type="checkbox"> 障害時の degraded mode 定義済み</li>
<li><input disabled="" type="checkbox"> 週次で復旧訓練（ゲームデイ）実施</li>
</ul>
<h3 id="運用">運用</h3>
<ul>
<li><input disabled="" type="checkbox"> Runbook とオンコール体制がある</li>
<li><input disabled="" type="checkbox"> 重大操作の通知先が明確</li>
<li><input disabled="" type="checkbox"> 監査レポートを定期出力できる</li>
<li><input disabled="" type="checkbox"> ポリシー変更はPRレビュー必須</li>
</ul>
<h2 id="8-pocから本番へ上げるときの移行手順">8. PoCから本番へ上げるときの移行手順</h2>
<p>おすすめは次の4段階です。</p>
<ol>
<li><strong>Read-only段階</strong>: 読み取り専用ツールのみ許可</li>
<li><strong>限定Write段階</strong>: staging への書き込みのみ解放</li>
<li><strong>承認付きProd段階</strong>: prod 書き込みは人間承認必須</li>
<li><strong>自動化段階</strong>: 低リスク操作だけ自動承認</li>
</ol>
<p>この順序なら、事故を起こさず運用知見を溜められます。</p>
<h2 id="まとめ">まとめ</h2>
<p>MCP は「つなげる技術」ですが、本番では「制御する技術」に重心があります。成功する導入の共通点は次の3つです。</p>
<ul>
<li>権限境界を最初に設計する</li>
<li>監査可能な実行経路を持つ</li>
<li>障害時に安全側へ倒れる設計にする</li>
</ul>
<p>AI エージェント活用は今後さらに広がります。だからこそ、機能追加より先に運用設計を固めることが、長期的な速度と安全性を両立する最短ルートです。今日から始めるなら、まずは「default deny + 監査ログ項目定義」この2つを先に確定してください。</p>
<h2 id="9-監査レビューを回すための実務フロー">9. 監査レビューを回すための実務フロー</h2>
<p>ログを取って終わりでは意味がありません。週次または隔週で、次の観点で監査レビューを実施します。</p>
<ul>
<li>deny された操作のうち、正当要求だったものはないか</li>
<li>承認付き操作で承認理由が空欄になっていないか</li>
<li>失敗率の高いツールに設計欠陥がないか</li>
<li>夜間・休日の危険操作が発生していないか</li>
</ul>
<p>監査レビューはセキュリティ部門だけでなく、実際に運用する開発チームが同席することで改善速度が上がります。ポリシーは「守らせるもの」ではなく「運用と一緒に育てるもの」と捉えるのが重要です。</p>
<h2 id="10-導入初期に決めておくべきslo">10. 導入初期に決めておくべきSLO</h2>
<p>MCP 基盤にも SLO を置くと、感覚論で運用しなくて済みます。例として以下が扱いやすいです。</p>
<ul>
<li>正常リクエスト成功率: 99.5%以上</li>
<li>ポリシー判定レイテンシ p95: 300ms 以下</li>
<li>重大操作の監査ログ欠損率: 0%</li>
<li>障害発生時の検知時間: 5分以内</li>
</ul>
<p>このSLOをダッシュボード化し、週次で逸脱をレビューするだけで、MCP運用の成熟度は大きく上がります。PoC段階から数値を持っておくと、本番移行時の説得材料としても有効です。</p>
<p>補足として、導入時は「全ツール同時公開」を避け、1ツールずつトラフィックを段階開放するのが安全です。障害発生時の切り戻し対象が明確になり、原因分析時間を大幅に短縮できます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>MCP</category>
      <category>AI Agent</category>
      <category>Architecture</category>
      <category>Security</category>
    </item>
    <item>
      <title>Python asyncioバックプレッシャー設計：落ちない非同期バッチを作る実装パターン</title>
      <link>https://www.ai2core.com/posts/2026-03-01-python-asyncio-backpressure-design/</link>
      <pubDate>Sun, 01 Mar 2026 09:20:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-01-python-asyncio-backpressure-design/</guid>
      <description>asyncioシステムが高負荷時に破綻しないためのバックプレッシャー設計を、キュー制御、同時実行数、タイムアウト、再試行戦略まで具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="python-asyncioバックプレッシャー設計落ちない非同期バッチを作る実装パターン">Python asyncioバックプレッシャー設計：落ちない非同期バッチを作る実装パターン</h1>
<p><code>asyncio</code> は速く作れる一方で、負荷が上がった瞬間に崩壊する設計を作りやすいという側面があります。特に「処理待ちが無限に積み上がる」「外部API遅延で全体が詰まる」「リトライ嵐でさらに遅くなる」は典型的です。</p>
<p>本記事では、非同期ワーカーを本番運用する前提で、<strong>バックプレッシャーを実装に落とす方法</strong>を解説します。単なる概念ではなく、すぐ使えるコード断片を中心に進めます。</p>
<h2 id="1-なぜバックプレッシャーが必要か">1. なぜバックプレッシャーが必要か</h2>
<p>バックプレッシャーは「これ以上は受けない」仕組みです。これがない設計は、ピーク時に次の順で壊れます。</p>
<ol>
<li>入力が処理速度を超える</li>
<li>キューが無限増加してメモリ圧迫</li>
<li>GC増加でスループット低下</li>
<li>タイムアウト増加→リトライ増加</li>
<li>システム全体が雪崩れる</li>
</ol>
<p>つまり、受けすぎないことは性能ではなく可用性の話です。</p>
<h2 id="2-基本設計3つの制限を必ず入れる">2. 基本設計：3つの制限を必ず入れる</h2>
<h3 id="2-1-キュー上限bounded-queue">2-1. キュー上限（bounded queue）</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></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">import</span> asyncio
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>QUEUE_MAX <span style="color:#f92672">=</span> <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>queue: asyncio<span style="color:#f92672">.</span>Queue[dict] <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Queue(maxsize<span style="color:#f92672">=</span>QUEUE_MAX)
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>maxsize</code> なしは原則禁止です。業務要件で「捨てられない」場合でも、無限キューより「受け付け停止 + 明示エラー」のほうが復旧可能です。</p>
<h3 id="2-2-同時実行数上限semaphore">2-2. 同時実行数上限（semaphore）</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></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>CONCURRENCY <span style="color:#f92672">=</span> <span style="color:#ae81ff">20</span>
</span></span><span style="display:flex;"><span>semaphore <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Semaphore(CONCURRENCY)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">guarded_call</span>(fn, <span style="color:#f92672">*</span>args, <span style="color:#f92672">**</span>kwargs):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> semaphore:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> fn(<span style="color:#f92672">*</span>args, <span style="color:#f92672">**</span>kwargs)
</span></span></code></pre></td></tr></table>
</div>
</div><p>CPU でも I/O でも、同時実行数に上限を持たせると遅延の尾が短くなります。</p>
<h3 id="2-3-タイムアウトtimeout-budget">2-3. タイムアウト（timeout budget）</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></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">import</span> asyncio
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">with_timeout</span>(coro, timeout_sec: float):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>wait_for(coro, timeout<span style="color:#f92672">=</span>timeout_sec)
</span></span></code></pre></td></tr></table>
</div>
</div><p>タイムアウトは「短すぎるか長すぎるか」ではなく、<strong>上流SLOから逆算</strong>します。例えば API 全体予算が 1.5 秒なら、外部API呼び出しを 600ms に固定し、残りをローカル処理に残す、という考え方です。</p>
<h2 id="3-実践ワーカーパターンそのまま使える">3. 実践ワーカーパターン（そのまま使える）</h2>
<p>以下は、キュー + 複数ワーカー + 再試行 + DLQ（Dead Letter Queue）を備えた最小構成です。</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><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</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">import</span> asyncio
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> dataclasses <span style="color:#f92672">import</span> dataclass
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Any
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@dataclass</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Task</span>:
</span></span><span style="display:flex;"><span>    payload: dict[str, Any]
</span></span><span style="display:flex;"><span>    retry: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>MAX_RETRY <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>QUEUE_MAX <span style="color:#f92672">=</span> <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>WORKERS <span style="color:#f92672">=</span> <span style="color:#ae81ff">16</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>main_queue: asyncio<span style="color:#f92672">.</span>Queue[Task] <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Queue(maxsize<span style="color:#f92672">=</span>QUEUE_MAX)
</span></span><span style="display:flex;"><span>dlq: asyncio<span style="color:#f92672">.</span>Queue[Task] <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Queue()
</span></span><span style="display:flex;"><span>sem <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Semaphore(<span style="color:#ae81ff">32</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">process_payload</span>(payload: dict[str, Any]) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 外部API呼び出しやDB処理を想定</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">0.05</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">worker</span>(worker_id: int):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>        task <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> main_queue<span style="color:#f92672">.</span>get()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> sem:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>wait_for(process_payload(task<span style="color:#f92672">.</span>payload), timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">1.0</span>)
</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>            <span style="color:#66d9ef">if</span> task<span style="color:#f92672">.</span>retry <span style="color:#f92672">&lt;</span> MAX_RETRY:
</span></span><span style="display:flex;"><span>                task<span style="color:#f92672">.</span>retry <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">0.1</span> <span style="color:#f92672">*</span> (<span style="color:#ae81ff">2</span> <span style="color:#f92672">**</span> task<span style="color:#f92672">.</span>retry))  <span style="color:#75715e"># 指数バックオフ</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> main_queue<span style="color:#f92672">.</span>put(task)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> dlq<span style="color:#f92672">.</span>put(task)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">finally</span>:
</span></span><span style="display:flex;"><span>            main_queue<span style="color:#f92672">.</span>task_done()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run</span>():
</span></span><span style="display:flex;"><span>    workers <span style="color:#f92672">=</span> [asyncio<span style="color:#f92672">.</span>create_task(worker(i)) <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(WORKERS)]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> main_queue<span style="color:#f92672">.</span>join()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">finally</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> w <span style="color:#f92672">in</span> workers:
</span></span><span style="display:flex;"><span>            w<span style="color:#f92672">.</span>cancel()
</span></span></code></pre></td></tr></table>
</div>
</div><p>重要なのは、再試行回数を有限にし、失敗タスクを DLQ に逃がす点です。無限リトライは障害時に自爆装置になります。</p>
<h2 id="4-入力側での受けすぎ防止">4. 入力側での「受けすぎ防止」</h2>
<p>ワーカーが健全でも、入口が無制限なら負けます。API であれば次の制御を入れます。</p>
<ul>
<li>受け付けキュー残量が閾値超えなら 429 を返す</li>
<li>tenant 単位でレート制限を分離</li>
<li>優先度キュー（重要ジョブを優先）</li>
</ul>
<h3 id="4-1-fastapiでの簡易例">4-1. FastAPIでの簡易例</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></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, HTTPException
</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>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.post</span>(<span style="color:#e6db74">&#34;/enqueue&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">enqueue</span>(item: dict):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> main_queue<span style="color:#f92672">.</span>qsize() <span style="color:#f92672">&gt;</span> int(QUEUE_MAX <span style="color:#f92672">*</span> <span style="color:#ae81ff">0.9</span>):
</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">429</span>, detail<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;queue saturated&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> main_queue<span style="color:#f92672">.</span>put(Task(payload<span style="color:#f92672">=</span>item))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;accepted&#34;</span>: <span style="color:#66d9ef">True</span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>「受け付けない」ことはユーザー体験を悪化させるように見えますが、全体停止よりはるかに安全です。429 を返す場合は <code>Retry-After</code> を併せて返し、クライアント再送間隔を制御します。</p>
<h2 id="5-観測性最低限見るべき4指標">5. 観測性：最低限見るべき4指標</h2>
<p>バックプレッシャーは入れただけでは不十分で、監視しないと調整できません。</p>
<ol>
<li><code>queue_depth</code>（キュー長）</li>
<li><code>processing_latency_p95</code></li>
<li><code>timeout_rate</code></li>
<li><code>dlq_rate</code></li>
</ol>
<h3 id="5-1-アラート基準の実例">5-1. アラート基準の実例</h3>
<ul>
<li>queue_depth &gt; 80% が 5分継続</li>
<li>timeout_rate &gt; 2% が 10分継続</li>
<li>dlq_rate が平常時の 3倍超</li>
</ul>
<p>アラートは「発火しやすい」より「行動が決まる」閾値を優先します。鳴るたびに対応が変わる設定は、運用疲労を生みます。</p>
<h2 id="6-負荷試験で必ず確認する項目">6. 負荷試験で必ず確認する項目</h2>
<p>本番前に k6 や Locust で負荷試験を行い、次を確認します。</p>
<ul>
<li>1.5x 負荷で queue_depth が収束するか</li>
<li>2.0x 負荷で 429 が適切に返るか</li>
<li>外部API遅延を注入しても DLQ へ逃がせるか</li>
<li>復帰後に backlog を解消できるか</li>
</ul>
<h3 id="6-1-テスト時の失敗パターン">6-1. テスト時の失敗パターン</h3>
<ul>
<li>セマフォ上限を増やしすぎ、下流DBが先に死ぬ</li>
<li>タイムアウト短縮だけで成功率が急落</li>
<li>リトライ間隔が短すぎて輻輳を悪化</li>
</ul>
<p>負荷試験は「最大スループット競争」ではなく「壊れ方の確認」です。</p>
<h2 id="7-運用で効く改善の順番">7. 運用で効く改善の順番</h2>
<p>改善は次の順番でやると効果が出やすいです。</p>
<ol>
<li>キュー上限と429制御を導入</li>
<li>同時実行数とタイムアウトを固定</li>
<li>DLQと再処理ジョブを作る</li>
<li>指標とアラートを整備</li>
<li>tenant別の公平制御（重い顧客の分離）</li>
</ol>
<p>最初から完璧なスケジューラは不要です。まず「壊れない最小構成」を作り、その上で最適化します。</p>
<h2 id="まとめ">まとめ</h2>
<p><code>asyncio</code> の本質的な課題は、速さではなく「過負荷時の振る舞い」です。バックプレッシャーは、ピーク時に品質を守るための安全装置であり、設計段階で必ず入れるべきです。</p>
<ul>
<li>無限キューを禁止する</li>
<li>同時実行数を固定する</li>
<li>タイムアウトと再試行を予算化する</li>
<li>失敗はDLQに逃がす</li>
</ul>
<p>この4点を実装すれば、負荷が来ても「遅くなるだけ」で済み、止まらないシステムに近づきます。安定運用を目指すなら、まずは今日中に <code>maxsize</code> と <code>Semaphore</code> をコードに入れるところから始めてください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Python</category>
      <category>asyncio</category>
      <category>Backend</category>
      <category>Performance</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>RAG評価基盤の作り方：精度・再現率・運用コストを同時に最適化する実践手順</title>
      <link>https://www.ai2core.com/posts/2026-02-28-rag-evaluation-pipeline-practical/</link>
      <pubDate>Sat, 28 Feb 2026 17:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-28-rag-evaluation-pipeline-practical/</guid>
      <description>RAGシステムの品質評価を自動化し、検索・生成・運用コストをバランスさせる評価パイプラインの実装方法を解説。</description>
      <content:encoded><![CDATA[<h1 id="rag評価基盤の作り方精度再現率運用コストを同時に最適化する実践手順">RAG評価基盤の作り方：精度・再現率・運用コストを同時に最適化する実践手順</h1>
<p>RAG（Retrieval Augmented Generation）は導入が進んでいますが、運用で最も難しいのは「改善したつもり」が頻発する点です。embedding モデルを変えた、chunk サイズを変えた、reranker を追加した。どれも良さそうに見えるのに、ユーザー満足は上がらない。このギャップを埋めるのが評価基盤です。</p>
<p>本記事では、RAG を継続改善するための評価パイプラインを、データセット設計から CI 統合まで具体的に解説します。</p>
<h2 id="rag評価で見るべき3層">RAG評価で見るべき3層</h2>
<p>RAG の品質は 1 指標では測れません。最低でも次の3層を分けて評価します。</p>
<ol>
<li><strong>Retrieval層</strong>: 正しい文書を取れているか</li>
<li><strong>Generation層</strong>: 回答が正確で有用か</li>
<li><strong>System層</strong>: レイテンシ・コスト・安定性</li>
</ol>
<p>この分離がないと、生成品質低下の原因が retrieval なのか prompt なのか判別できません。</p>
<h2 id="ステップ1評価データセットを設計する">ステップ1：評価データセットを設計する</h2>
<h3 id="1-1-問い合わせカテゴリを分割">1-1. 問い合わせカテゴリを分割</h3>
<p>例として次の5カテゴリに分けます。</p>
<ul>
<li>定義確認（用語説明）</li>
<li>手順質問（How-to）</li>
<li>例外対応（エラー解決）</li>
<li>比較検討（A vs B）</li>
<li>根拠提示（出典必須）</li>
</ul>
<p>カテゴリごとに難易度と重要度を持たせ、偏りを防ぎます。</p>
<h3 id="1-2-正解の持ち方">1-2. 正解の持ち方</h3>
<p>正解は「理想回答1つ」では不十分です。RAGでは表現揺れが自然なので、次を保存します。</p>
<ul>
<li>期待要素（必須ポイント）</li>
<li>禁止要素（誤情報、過剰断定）</li>
<li>参照すべき文書ID</li>
</ul>
<p>この形式にすると、自動評価と人手レビューを両立できます。</p>
<h2 id="ステップ2retrieval評価を自動化">ステップ2：Retrieval評価を自動化</h2>
<p>代表指標:</p>
<ul>
<li>Recall@k</li>
<li>MRR</li>
<li>nDCG</li>
</ul>
<p>例えば、正解文書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></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">recall_at_k</span>(retrieved_ids, gold_ids, k<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span>):
</span></span><span style="display:flex;"><span>    topk <span style="color:#f92672">=</span> set(retrieved_ids[:k])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1.0</span> <span style="color:#66d9ef">if</span> len(topk<span style="color:#f92672">.</span>intersection(gold_ids)) <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span> <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">0.0</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>運用では平均値だけでなく、カテゴリ別分布を見ることが重要です。手順質問だけ recall が低い場合、chunk 戦略や見出し抽出に問題がある可能性が高いです。</p>
<h2 id="ステップ3generation評価の設計">ステップ3：Generation評価の設計</h2>
<p>自動評価では次を推奨します。</p>
<ul>
<li>Faithfulness（出典との整合）</li>
<li>Answer Relevance（質問への適合）</li>
<li>Completeness（必要要素網羅）</li>
<li>Safety（禁止事項違反）</li>
</ul>
<p>LLM-as-a-judge を使う場合、判定プロンプトを固定し、temperature=0 で再現性を確保します。さらに、週次で人手サンプル監査を入れて判定ドリフトを検出します。</p>
<h2 id="ステップ4system評価遅延コスト">ステップ4：System評価（遅延・コスト）</h2>
<p>品質改善がコスト爆増を招くと継続できません。次を同時に計測します。</p>
<ul>
<li>P50/P95 latency</li>
<li>平均 input/output token</li>
<li>1回答あたり推定コスト</li>
<li>timeout率、fallback率</li>
</ul>
<p>この4指標を CI レポートに含めると、精度改善の副作用を早期に発見できます。</p>
<h2 id="ステップ5ciへの組み込み">ステップ5：CIへの組み込み</h2>
<p>PR ごとに評価ジョブを実行し、閾値を満たさない変更をブロックします。</p>
<p>判定例:</p>
<ul>
<li>Recall@5: 0.82 以上</li>
<li>Faithfulness: 0.90 以上</li>
<li>P95 latency: 2500ms 以下</li>
<li>Cost/answer: $0.005 以下</li>
</ul>
<p>疑似フロー:</p>
<ol>
<li>変更ブランチでインデックス再構築</li>
<li>評価データセット100件で推論</li>
<li>指標を計算して前回基準と比較</li>
<li>差分レポートをPRコメントに投稿</li>
</ol>
<p>これで「なんとなく改善」を排除できます。</p>
<h2 id="ステップ6オンライン評価との接続">ステップ6：オンライン評価との接続</h2>
<p>オフライン評価だけでは実利用の多様性を拾えません。オンライン指標を接続します。</p>
<ul>
<li>ユーザー評価（👍/👎）</li>
<li>再質問率（同一セッションで再問い合わせ）</li>
<li>人間オペレータ転送率</li>
</ul>
<p>重要なのは trace_id でオフライン指標と紐づけることです。これにより「オフラインは良いのに本番満足が低い」差分を原因追跡できます。</p>
<h2 id="改善ループの実例">改善ループの実例</h2>
<p>ある社内ヘルプデスクRAGでの改善例:</p>
<ul>
<li>問題: 手順質問で誤回答が多い</li>
<li>原因: chunk が短すぎ、手順文脈が分断</li>
<li>対策: section-aware chunking + reranker導入</li>
</ul>
<p>結果:</p>
<ul>
<li>Recall@5: 0.74 → 0.88</li>
<li>Faithfulness: 0.81 → 0.93</li>
<li>P95 latency: +180ms（許容内）</li>
</ul>
<p>このように、どの変更がどの指標に効いたかを記録すると、次回改善の再現性が高まります。</p>
<h2 id="よくある失敗">よくある失敗</h2>
<ol>
<li><strong>評価データが少なすぎる</strong>
<ul>
<li>20件程度では統計的に不安定。最低100件、理想300件。</li>
</ul>
</li>
<li><strong>単一スコアで判定する</strong>
<ul>
<li>精度だけでコストを見ないと運用破綻。</li>
</ul>
</li>
<li><strong>判定プロンプトを頻繁に変える</strong>
<ul>
<li>指標比較の連続性が失われる。</li>
</ul>
</li>
<li><strong>失敗事例をデータセットへ反映しない</strong>
<ul>
<li>同じ不具合を繰り返す。</li>
</ul>
</li>
</ol>
<h2 id="90日ロードマップ">90日ロードマップ</h2>
<ul>
<li><strong>0-30日</strong>: 評価データセット整備、retrieval指標導入</li>
<li><strong>31-60日</strong>: generation指標 + CIゲート導入</li>
<li><strong>61-90日</strong>: オンライン評価統合、週次改善会の定着</li>
</ul>
<p>この順序なら、運用負荷を抑えつつ確実に品質を上げられます。</p>
<h2 id="まとめ">まとめ</h2>
<p>RAG の実力は、モデル選定より評価基盤で決まります。retrieval、generation、system の3層を分離し、CI に組み込むことで、改善の再現性が生まれます。</p>
<p>まずは小さく始めて、失敗ケースを評価データセットに反映し続けてください。評価が回り始めると、RAG は「当たるかどうかの賭け」から「制御可能なプロダクト」へ変わります。</p>
<h2 id="実装例評価結果をprコメントに自動投稿する">実装例：評価結果をPRコメントに自動投稿する</h2>
<p>運用で効くのは、評価結果を開発者が日常的に見る導線を作ることです。GitHub Actions で評価スクリプトを実行し、結果を PR コメントへ投稿します。</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">rag-eval</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>: [<span style="color:#ae81ff">pull_request]</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">evaluate</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">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 style="color:#f92672">run</span>: <span style="color:#ae81ff">uv sync</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">uv run python scripts/run_rag_eval.py --dataset evalset_v3.json --out report.json</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">uv run python scripts/post_pr_comment.py report.json</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この仕組みがあると、レビュー段階で「この変更は Faithfulness を 0.04 落とすが latency は改善」という会話ができ、意思決定が定量化されます。</p>
<h2 id="評価データセットの更新運用">評価データセットの更新運用</h2>
<p>評価セットを固定しすぎると、現実の問い合わせ変化に追従できません。次のルールを推奨します。</p>
<ul>
<li>月1回、実ユーザー失敗ケースを20件追加</li>
<li>四半期ごとに古いケースを棚卸し</li>
<li>重要カテゴリ比率を維持（例: 手順質問30%以上）</li>
</ul>
<p>この更新を怠ると、指標が良くても体感品質が落ちる「評価腐敗」が起きます。</p>
<h2 id="abテストとの接続">A/Bテストとの接続</h2>
<p>大きな変更（embedding刷新、reranker導入）は、オフライン評価だけでなくオンライン A/B を併用します。</p>
<ul>
<li>A群: 現行パイプライン</li>
<li>B群: 新パイプライン</li>
<li>比較指標: 👍率、再質問率、回答時間、コスト</li>
</ul>
<p>2週間程度の観測で統計差が出るケースが多く、主観ベースの議論を減らせます。</p>
<h2 id="まとめ定着のポイント">まとめ（定着のポイント）</h2>
<p>RAG 改善を継続する鍵は、評価を「一回の検証」ではなく「開発フローの標準」にすることです。CI コメント、データセット更新、A/B テストを回すことで、品質向上が偶然ではなく再現可能な活動になります。</p>
<h3 id="補足">補足</h3>
<p>評価結果は経営指標とも接続できます。問い合わせ解決率やサポート工数削減と紐づけることで、RAG 改善が事業価値にどう効いたかまで説明可能になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>RAG</category>
      <category>LLM</category>
      <category>Evaluation</category>
      <category>MLOps</category>
    </item>
    <item>
      <title>GitHub Actionsセルフホストランナー防衛術：CI/CDの供給網リスクを減らす実装ガイド</title>
      <link>https://www.ai2core.com/posts/2026-02-28-github-actions-selfhosted-runner-security/</link>
      <pubDate>Sat, 28 Feb 2026 13:30:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-28-github-actions-selfhosted-runner-security/</guid>
      <description>セルフホストランナー運用で発生する主要リスクを洗い出し、実装可能な防御策を段階的に紹介。</description>
      <content:encoded><![CDATA[<h1 id="github-actionsセルフホストランナー防衛術cicdの供給網リスクを減らす実装ガイド">GitHub Actionsセルフホストランナー防衛術：CI/CDの供給網リスクを減らす実装ガイド</h1>
<p>セルフホストランナーは高速で柔軟です。特定ツールチェーンや社内ネットワーク接続が必要な環境では、ほぼ必須といえます。一方で、設定を誤ると CI/CD が攻撃経路になります。</p>
<p>近年のインシデントでは、依存パッケージ汚染だけでなく「Actions workflow の権限過多」「fork 由来PRでの秘密情報流出」「ランナー残存データ」が問題化しています。</p>
<p>本記事では、セルフホストランナーを安全に運用するための防衛策を、設計レイヤごとに整理します。</p>
<h2 id="脅威モデルを先に定義する">脅威モデルを先に定義する</h2>
<p>まず守る対象を明確にします。</p>
<ul>
<li>リポジトリのソースコード</li>
<li>Secrets（クラウド鍵、署名鍵、トークン）</li>
<li>配布物の完全性（改ざん防止）</li>
<li>社内ネットワーク接続経路</li>
</ul>
<p>攻撃経路は主に次です。</p>
<ol>
<li>悪意ある PR が workflow を悪用</li>
<li>Marketplace Action の supply chain 汚染</li>
<li>ランナー上に残る credential / build artifact</li>
<li>過剰な <code>GITHUB_TOKEN</code> 権限</li>
</ol>
<p>この4点を潰す設計が防御の中心になります。</p>
<h2 id="1-ランナーは使い捨てを前提にする">1. ランナーは「使い捨て」を前提にする</h2>
<p>長寿命ランナーは便利ですが、攻撃後の残留リスクが高いです。可能ならジョブ単位で破棄できる ephemeral 構成を採用します。</p>
<ul>
<li>Kubernetes + Actions Runner Controller</li>
<li>VMテンプレートから都度起動</li>
<li>ジョブ終了後に完全破棄</li>
</ul>
<p>少なくとも <code>/tmp</code> と workspace を確実に消去し、Docker layer cache の共有範囲を制御してください。</p>
<h2 id="2-workflow-権限を最小化する">2. workflow 権限を最小化する</h2>
<p><code>permissions: write-all</code> は禁止レベルです。workflowごとに最小権限を明記します。</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">permissions</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 style="color:#f92672">pull-requests</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id-token</span>: <span style="color:#ae81ff">write</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>特に <code>id-token: write</code> は OIDC 連携に必要ですが、不要ジョブで許可しないこと。権限漏れがクラウド侵害に直結します。</p>
<h2 id="3-fork-pr-の実行ポリシーを分離する">3. fork PR の実行ポリシーを分離する</h2>
<p>外部 fork からの PR で secrets を使うジョブを実行しない設計が必須です。</p>
<p>推奨:</p>
<ul>
<li><code>pull_request</code> イベント: テストのみ、secrets 無し</li>
<li><code>pull_request_target</code>: 原則禁止、必要なら厳格レビュー</li>
<li>デプロイ系は <code>push</code>（保護ブランチ）だけ</li>
</ul>
<p>また、<code>workflow_run</code> を使って「検証済み成果物だけを次段へ渡す」2段構成にすると安全性が上がります。</p>
<h2 id="4-action-の固定と検証">4. Action の固定と検証</h2>
<p><code>uses: actions/checkout@v4</code> のようなタグ指定だけでは、将来更新の影響を受けます。高リスク工程では commit SHA 固定を検討します。</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-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@8ade135a...</span> <span style="color:#75715e"># SHA pin</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>加えて、許可済み Action の allowlist を組織ポリシー化します。無制限に Marketplace Action を使わせると監査不能になります。</p>
<h2 id="5-secrets-管理は-oidc-中心へ">5. Secrets 管理は OIDC 中心へ</h2>
<p>長期固定鍵を GitHub Secrets に置く運用は、漏洩時の被害が大きいです。クラウド連携は OIDC フェデレーションに移行し、短命トークンを発行します。</p>
<p>利点:</p>
<ul>
<li>鍵配布不要</li>
<li>期限短い</li>
<li>リポジトリ/ブランチ条件で絞れる</li>
</ul>
<p>AWS なら role trust policy に <code>sub</code> 条件を入れ、「main ブランチの release workflow だけ許可」といった制御が可能です。</p>
<h2 id="6-ネットワーク分離と出口制御">6. ネットワーク分離と出口制御</h2>
<p>セルフホストランナーが社内フラットネットワークに直結している構成は危険です。ランナー専用サブネットを作り、次を実施します。</p>
<ul>
<li>egress allowlist（必要ドメインのみ）</li>
<li>社内DBへの直接接続禁止</li>
<li>管理プレーンと実行プレーン分離</li>
</ul>
<p>「CIだから社内に近いほど便利」は短期的発想です。侵害前提で最小到達範囲に設計します。</p>
<h2 id="7-監査ログと改ざん検知">7. 監査ログと改ざん検知</h2>
<p>必要なログ:</p>
<ul>
<li>workflow 実行者</li>
<li>使用ランナーID</li>
<li>取得したクラウド権限</li>
<li>成果物ハッシュ</li>
</ul>
<p>さらに、リリース成果物に SBOM と署名（cosign / sigstore）を付与し、配布前検証を自動化します。これで supply chain の追跡性が大きく向上します。</p>
<h2 id="8-実装チェックリストそのまま使える">8. 実装チェックリスト（そのまま使える）</h2>
<ul>
<li><input disabled="" type="checkbox"> セルフホストランナーは ephemeral 運用</li>
<li><input disabled="" type="checkbox"> workflow permissions を最小権限化</li>
<li><input disabled="" type="checkbox"> fork PR と deploy workflow を分離</li>
<li><input disabled="" type="checkbox"> 高リスク Action は SHA pin</li>
<li><input disabled="" type="checkbox"> OIDC による短命認証へ移行</li>
<li><input disabled="" type="checkbox"> ネットワーク egress 制限</li>
<li><input disabled="" type="checkbox"> artifact 署名と SBOM 生成</li>
<li><input disabled="" type="checkbox"> 監査ログを90日以上保持</li>
</ul>
<h2 id="9-段階導入プラン4週間">9. 段階導入プラン（4週間）</h2>
<ul>
<li>Week1: 権限棚卸し、<code>write-all</code> 排除</li>
<li>Week2: fork PR ポリシー分離、allowlist導入</li>
<li>Week3: OIDC移行、固定鍵削減</li>
<li>Week4: ephemeral runner 化、署名/SBOM実装</li>
</ul>
<p>一気に変えると運用停止リスクがあるため、週単位で区切るのが現実的です。</p>
<h2 id="まとめ">まとめ</h2>
<p>セルフホストランナーは強力ですが、セキュリティ設計を誤ると CI/CD が最も危険な入口になります。ポイントは「最小権限」「使い捨て」「短命認証」「監査可能性」の4つです。</p>
<p>まずは <code>permissions</code> の最小化と fork PR 分離から始めてください。ここを押さえるだけでも、供給網リスクは大幅に下げられます。</p>
<h2 id="実運用での検知ルール例siem連携">実運用での検知ルール例（SIEM連携）</h2>
<p>防御策を実装しても、検知が弱いと侵害を見逃します。次のイベントを SIEM 側で高優先度アラート化してください。</p>
<ul>
<li>深夜帯の workflow 権限変更</li>
<li>普段使わないランナーラベルでのジョブ実行</li>
<li>release workflow で未知の Action 呼び出し</li>
<li>OIDC 経由で想定外クラウドロールを取得</li>
</ul>
<p>検知時には自動で <code>repository dispatch</code> を使い、該当リポジトリのデプロイを一時停止する仕組みを入れると被害拡大を防げます。</p>
<h2 id="インシデント対応runbookの最小構成">インシデント対応Runbookの最小構成</h2>
<p>供給網インシデントは初動が遅れると致命傷になります。Runbook には最低限次を含めます。</p>
<ol>
<li>影響範囲特定（対象リポジトリ、workflow、artifact）</li>
<li>該当 Secrets/Role の無効化</li>
<li>ランナー群の全廃棄と再構築</li>
<li>直近リリース成果物のハッシュ再検証</li>
<li>監査ログ保全と関係者通知</li>
</ol>
<p>この手順を平時に演習しておくことで、実際の障害時に迷いを減らせます。</p>
<h2 id="組織導入で効くポリシー">組織導入で効くポリシー</h2>
<ul>
<li>新規 workflow はセキュリティレビュー必須</li>
<li>Action 追加時はリスク評価テンプレート提出</li>
<li>重要リポジトリは branch protection + required review を強制</li>
<li>セルフホストランナーの管理責任者を明確化</li>
</ul>
<p>技術対策だけでは継続しません。責任分界とレビュー手順を運用ルール化することが、防御の持続性を高めます。</p>
<h2 id="まとめ運用視点">まとめ（運用視点）</h2>
<p>最終的に重要なのは「侵害されないこと」ではなく「侵害されても被害を限定し、速く復旧できること」です。セルフホストランナーを使うなら、最初からゼロトラスト前提で設計し、検知・対応まで含めた体制を整えてください。</p>
<h3 id="最後に">最後に</h3>
<p>現場では「便利だから後で固める」が最も危険です。セルフホストランナーは導入初日から最小権限と隔離を前提に設計し、定期監査で逸脱を戻す運用を続けてください。</p>
<h3 id="追加の運用tip">追加の運用Tip</h3>
<p>新しいリポジトリを作るたびに同じ議論をしないため、テンプレートリポジトリへ安全な workflow 雛形を同梱しておくと効果的です。初期状態を安全側に固定するだけで、運用負荷と事故率の両方を下げられます。</p>
<p>継続的な棚卸しを習慣化してください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>Security</category>
      <category>CI/CD</category>
      <category>DevSecOps</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>
    <item>
      <title>PostgreSQLインデックス最適化の現場手順：遅いクエリを再現・診断・改善する実践プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-02-27-postgresql-indexing-production-playbook/</link>
      <pubDate>Fri, 27 Feb 2026 13:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-27-postgresql-indexing-production-playbook/</guid>
      <description>EXPLAIN ANALYZEの読み方から複合/部分/式インデックスの使い分け、リリース手順までを実例ベースで解説。</description>
      <content:encoded><![CDATA[<h1 id="postgresqlインデックス最適化の現場手順遅いクエリを再現診断改善する実践プレイブック">PostgreSQLインデックス最適化の現場手順：遅いクエリを再現・診断・改善する実践プレイブック</h1>
<p>「CPUは余っているのに画面が遅い」「特定時間帯だけ API が詰まる」。この手の問題の多くは、アプリではなく SQL の実行計画に原因があります。特に PostgreSQL では、インデックス設計と統計情報の状態が性能をほぼ決めます。</p>
<p>本記事では、実務で使う手順に沿って、遅延クエリの改善を再現可能な形で解説します。単なる理論紹介ではなく、<strong>調査順序、判断基準、リリース時の注意点</strong>まで含めてまとめます。</p>
<h2 id="まず守るべき3原則">まず守るべき3原則</h2>
<ol>
<li><strong>推測でインデックスを作らない</strong>
体感で追加すると write 性能とストレージが悪化します。必ず実行計画を見てから判断します。</li>
<li><strong>改善前後を数値で比較する</strong>
P95、rows、shared read blocks を記録し、効果を証明します。</li>
<li><strong>本番反映は CONCURRENTLY を基本にする</strong>
テーブルロックで事故らないため、<code>CREATE INDEX CONCURRENTLY</code> を優先します。</li>
</ol>
<h2 id="ケース設定注文一覧apiが遅い">ケース設定：注文一覧APIが遅い</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></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> id, user_id, status, total_amount, created_at
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> tenant_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> status <span style="color:#66d9ef">IN</span> (<span style="color:#e6db74">&#39;paid&#39;</span>, <span style="color:#e6db74">&#39;shipped&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> created_at <span style="color:#f92672">&gt;=</span> NOW() <span style="color:#f92672">-</span> INTERVAL <span style="color:#e6db74">&#39;30 days&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> created_at <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">50</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>データ量は <code>orders</code> 1.2億件、1テナントあたり数百万件。現象は「特定テナントだけ 3〜6 秒」です。</p>
<h2 id="手順1pg_stat_statementsで優先度をつける">手順1：pg_stat_statementsで優先度をつける</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></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> queryid, calls, total_exec_time, mean_exec_time, <span style="color:#66d9ef">rows</span>, query
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_statements
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> total_exec_time <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>ここで対象クエリの <code>calls</code> が多く、<code>mean_exec_time</code> が高いことを確認。改善効果が大きいと判断できます。</p>
<h2 id="手順2explain-analyze-buffersでボトルネックを特定">手順2：EXPLAIN ANALYZE BUFFERSでボトルネックを特定</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></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">EXPLAIN</span> (<span style="color:#66d9ef">ANALYZE</span>, BUFFERS, <span style="color:#66d9ef">VERBOSE</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> ...;
</span></span></code></pre></td></tr></table>
</div>
</div><p>典型的な悪い例は次の通りです。</p>
<ul>
<li><code>Seq Scan on orders</code></li>
<li><code>Rows Removed by Filter</code> が極端に多い</li>
<li><code>Sort Method: external merge Disk</code>（メモリ不足でディスクソート）</li>
</ul>
<p>この状態では、絞り込み条件に合うインデックスが不足しています。</p>
<h2 id="手順3最小コストで効くインデックス設計">手順3：最小コストで効くインデックス設計</h2>
<p>今回の条件は <code>tenant_id</code>, <code>status</code>, <code>created_at</code> です。ORDER BY も <code>created_at DESC</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">CREATE</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_orders_tenant_status_created_at_desc
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> orders (tenant_id, status, created_at <span style="color:#66d9ef">DESC</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここで順序が重要です。先頭列は等価条件（tenant_id）、次に低カーディナリティ条件（status）、最後に範囲・並び替え列（created_at）を置きます。</p>
<h3 id="部分インデックスの検討">部分インデックスの検討</h3>
<p><code>status</code> が多数あるが実際に使うのが paid/shipped だけなら、部分インデックスでさらに削減できます。</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">CREATE</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_orders_recent_paid_shipped
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> orders (tenant_id, created_at <span style="color:#66d9ef">DESC</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> status <span style="color:#66d9ef">IN</span> (<span style="color:#e6db74">&#39;paid&#39;</span>, <span style="color:#e6db74">&#39;shipped&#39;</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>この方式はサイズが小さく、キャッシュ効率が高いのが利点です。</p>
<h2 id="手順4改善効果を検証">手順4：改善効果を検証</h2>
<p>同一条件で再度 <code>EXPLAIN ANALYZE</code> を実施します。</p>
<p>確認ポイント:</p>
<ul>
<li><code>Index Scan</code> か <code>Bitmap Heap Scan</code> に変わっているか</li>
<li>実行時間が目標値（例: 200ms 未満）に入ったか</li>
<li>shared read blocks が大幅に減ったか</li>
<li><code>rows=50</code> を早期に取り出せているか</li>
</ul>
<p>改善後に 4.2 秒 → 120ms 程度まで落ちるケースは珍しくありません。</p>
<h2 id="それでも遅い場合の追加施策">それでも遅い場合の追加施策</h2>
<h3 id="1-カバリングインデックスinclude">1) カバリングインデックス（INCLUDE）</h3>
<p>取得列が多いとテーブルアクセスが残ります。PostgreSQL では <code>INCLUDE</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></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">CREATE</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_orders_covering
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> orders (tenant_id, status, created_at <span style="color:#66d9ef">DESC</span>)
</span></span><span style="display:flex;"><span>INCLUDE (total_amount, user_id);
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="2-統計情報の更新">2) 統計情報の更新</h3>
<p>データ偏りが強いと planner が誤判定します。</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">ANALYZE</span> orders;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> orders <span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">COLUMN</span> status <span style="color:#66d9ef">SET</span> <span style="color:#66d9ef">STATISTICS</span> <span style="color:#ae81ff">1000</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ANALYZE</span> orders;
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="3-パーティショニング">3) パーティショニング</h3>
<p>30日検索が多いなら、月次パーティションで読み取り範囲を削るのも有効です。既存移行はコストが高いので、まずは新規データから段階導入します。</p>
<h2 id="リリース時の安全手順">リリース時の安全手順</h2>
<p>本番では速度改善より安全性が優先です。次の順番を守ると事故が減ります。</p>
<ol>
<li>負荷が低い時間帯を選ぶ</li>
<li><code>CREATE INDEX CONCURRENTLY</code> を実行</li>
<li>進捗確認: <code>pg_stat_progress_create_index</code></li>
<li>完了後に代表クエリで実行計画を確認</li>
<li>監視（CPU、I/O、lock wait、replication lag）を 30 分観察</li>
<li>不要化した旧インデックスは別日に削除</li>
</ol>
<p>いきなり削除しない理由は、想定外クエリで回帰する可能性があるためです。1〜2日観測してから <code>DROP INDEX CONCURRENTLY</code> するのが安定運用です。</p>
<h2 id="アンチパターン集">アンチパターン集</h2>
<ul>
<li><code>LIKE '%keyword%'</code> に B-tree インデックスを貼る
<ul>
<li>→ pg_trgm + GIN を使う</li>
</ul>
</li>
<li>すべての列に単体インデックスを作る
<ul>
<li>→ planner が迷う、write コスト増</li>
</ul>
</li>
<li>UUID主キーだけ見て満足する
<ul>
<li>→ 実際の検索条件列を優先</li>
</ul>
</li>
<li>autovacuum 設定を放置
<ul>
<li>→ bloat 増で index scan が遅くなる</li>
</ul>
</li>
</ul>
<h2 id="計測テンプレート運用向け">計測テンプレート（運用向け）</h2>
<p>改善作業を属人化しないために、次のテンプレートで記録すると再利用できます。</p>
<ul>
<li>対象クエリID（pg_stat_statements）</li>
<li>改善前 mean/P95</li>
<li>改善前実行計画（テキスト保存）</li>
<li>追加したインデックスDDL</li>
<li>改善後 mean/P95</li>
<li>副作用（write増、ストレージ増、vacuum時間）</li>
<li>ロールバック手順</li>
</ul>
<p>このフォーマットをWiki化しておくと、次回の性能障害対応が非常に速くなります。</p>
<h2 id="まとめ">まとめ</h2>
<p>PostgreSQL の性能改善は、魔法のパラメータよりも「再現・診断・検証」の手順で決まります。特にインデックスは効果が大きい反面、副作用もあるため、実行計画と計測値で判断することが重要です。</p>
<p>遅延問題に直面したら、まず <code>pg_stat_statements</code> で対象を絞り、<code>EXPLAIN ANALYZE BUFFERS</code> で事実を取り、<code>CONCURRENTLY</code> で安全に改善する。この流れをチーム標準にすれば、DB運用の安定性は確実に上がります。</p>
<h2 id="現場で使うトラブルシュート手順夜間障害対応向け">現場で使うトラブルシュート手順（夜間障害対応向け）</h2>
<p>実際の障害対応では、理想的な調査順序を守れないことがあります。そこで夜間当番でも使える短縮手順を用意しておくと有効です。</p>
<ol>
<li>まず <code>pg_stat_activity</code> で待機イベントを確認（lock か I/O か）</li>
<li>次に <code>pg_locks</code> で競合トランザクションを特定</li>
<li>対象クエリの <code>EXPLAIN (ANALYZE, BUFFERS)</code> を取得</li>
<li>直近デプロイ差分（SQL/マイグレーション）を確認</li>
<li>即効性のある一時回避（statement timeout、read replica 振り分け）を実施</li>
</ol>
<p>短期回避後に恒久対策を行う、という二段運用が安定します。</p>
<h3 id="ロック競合の例">ロック競合の例</h3>
<p><code>ALTER TABLE</code> と長時間 SELECT が競合すると、アプリの体感遅延が一気に悪化します。マイグレーションは <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;2s&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SET</span> statement_timeout <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;30s&#39;</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="クエリヒントが使えない前提での工夫">クエリヒントが使えない前提での工夫</h3>
<p>PostgreSQL は MySQL のようなヒント句が限定的なため、実行計画の誘導は以下で行います。</p>
<ul>
<li>統計情報を正しく更新</li>
<li>不要な関数適用を避ける（索引利用阻害）</li>
<li>OR 条件を UNION ALL 分割で単純化</li>
</ul>
<p>例えば <code>WHERE date(created_at) = CURRENT_DATE</code> は index を使いにくいため、次のように書き換えます。</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">WHERE</span> created_at <span style="color:#f92672">&gt;=</span> <span style="color:#66d9ef">CURRENT_DATE</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> created_at <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">CURRENT_DATE</span> <span style="color:#f92672">+</span> INTERVAL <span style="color:#e6db74">&#39;1 day&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この1点だけで scan 範囲が激減することがあります。</p>
<h2 id="チーム運用に落とし込むためのルール">チーム運用に落とし込むためのルール</h2>
<p>最後に、性能改善を個人依存にしないための運用ルールを提案します。</p>
<ul>
<li>新規API追加時は必ず「想定SQL」と「必要インデックス案」を設計レビューに含める</li>
<li>週次で slow query 上位10件を確認し、改善オーナーを割り当てる</li>
<li>重要テーブルの index hit ratio と bloat 率を定期監視する</li>
</ul>
<p>この運用が回ると、障害対応だけでなく機能開発の速度も上がります。DB 性能は裏方ではなく、プロダクト体験の中心です。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>PostgreSQL</category>
      <category>Performance</category>
      <category>Index</category>
      <category>Database</category>
    </item>
    <item>
      <title>LLM運用の可観測性を実装する：OpenTelemetryでつくるPrompt/Token/Latency監視の実践</title>
      <link>https://www.ai2core.com/posts/2026-02-27-llm-observability-opentelemetry/</link>
      <pubDate>Fri, 27 Feb 2026 09:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-27-llm-observability-opentelemetry/</guid>
      <description>OpenTelemetryを使ってLLMアプリのレイテンシ、トークン、品質劣化を追跡する実装手順を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="llm運用の可観測性を実装するopentelemetryでつくるprompttokenlatency監視の実践">LLM運用の可観測性を実装する：OpenTelemetryでつくるPrompt/Token/Latency監視の実践</h1>
<p>LLMアプリは「動く」だけでは本番品質になりません。運用を始めると、次のような問題が必ず発生します。</p>
<ul>
<li>昨日まで 1.2 秒だった応答が突然 4 秒台になる</li>
<li>コストが月末に急増したが、どの機能が原因かわからない</li>
<li>回答品質が落ちたと言われるが、どのプロンプト変更が影響したか追えない</li>
<li>リトライ回数や外部API待ちの偏りが可視化されていない</li>
</ul>
<p>この課題を解く鍵が「可観測性（Observability）」です。本記事では OpenTelemetry を軸に、LLM アプリの監視をゼロから構築する実装を、実際に運用で使える粒度で説明します。</p>
<h2 id="なぜ-apm-だけでは-llm-を見切れないのか">なぜ APM だけでは LLM を見切れないのか</h2>
<p>従来の Web アプリ監視（CPU、HTTP レイテンシ、エラーレート）だけでは、LLM 特有の故障点が見えません。理由は、LLM の品質とコストが「入力テキスト」と「推論設定」に強く依存するためです。</p>
<p>少なくとも次の軸が必要です。</p>
<ol>
<li><strong>Prompt 可視化</strong>: システム/ユーザー/ツール呼び出しの構成</li>
<li><strong>Token 可視化</strong>: input/output token、モデル別単価、キャッシュヒット率</li>
<li><strong>推論経路可視化</strong>: retrieval → rerank → generation の各ステップ時間</li>
<li><strong>品質シグナル</strong>: hallucination 率、参照文書一致率、ユーザー評価</li>
</ol>
<p>つまり、HTTP 1 本のログでは不十分で、<strong>トレース単位で LLM 実行を分解</strong>する必要があります。</p>
<h2 id="アーキテクチャの全体像">アーキテクチャの全体像</h2>
<p>最初に、実装対象を次の構成とします。</p>
<ul>
<li>API: FastAPI</li>
<li>LLM: OpenAI / Azure OpenAI（抽象化）</li>
<li>RAG: pgvector + reranker</li>
<li>Observability: OpenTelemetry SDK + OTLP Exporter + Grafana Tempo/Loki/Prometheus</li>
</ul>
<p>処理フローは次の通りです。</p>
<ol>
<li>リクエスト受信時に <code>trace_id</code> を生成</li>
<li>Retrieval、Rerank、Generate をそれぞれ span 化</li>
<li>各 span に token、model、temperature、cache_hit を attribute として記録</li>
<li>失敗時は exception をイベントとして保存</li>
<li>レスポンス時にコスト推定を metrics として送信</li>
</ol>
<h2 id="ステップ1opentelemetryの初期設定">ステップ1：OpenTelemetryの初期設定</h2>
<p>まずは Python で最小セットを導入します。</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>uv add opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-fastapi
</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><span 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></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:#75715e"># observability.py</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry <span style="color:#f92672">import</span> trace, metrics
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry.sdk.trace <span style="color:#f92672">import</span> TracerProvider
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry.sdk.trace.export <span style="color:#f92672">import</span> BatchSpanProcessor
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry.exporter.otlp.proto.grpc.trace_exporter <span style="color:#f92672">import</span> OTLPSpanExporter
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry.sdk.resources <span style="color:#f92672">import</span> Resource
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>resource <span style="color:#f92672">=</span> Resource<span style="color:#f92672">.</span>create({
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;service.name&#34;</span>: <span style="color:#e6db74">&#34;tech-blog-autopilot-api&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;service.version&#34;</span>: <span style="color:#e6db74">&#34;1.3.0&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;deployment.environment&#34;</span>: <span style="color:#e6db74">&#34;production&#34;</span>,
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>provider <span style="color:#f92672">=</span> TracerProvider(resource<span style="color:#f92672">=</span>resource)
</span></span><span style="display:flex;"><span>provider<span style="color:#f92672">.</span>add_span_processor(
</span></span><span style="display:flex;"><span>    BatchSpanProcessor(OTLPSpanExporter(endpoint<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://otel-collector:4317&#34;</span>, insecure<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>))
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>trace<span style="color:#f92672">.</span>set_tracer_provider(provider)
</span></span><span style="display:flex;"><span>tracer <span style="color:#f92672">=</span> trace<span style="color:#f92672">.</span>get_tracer(<span style="color:#e6db74">&#34;llm-pipeline&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここで重要なのは、<strong>service.name を固定すること</strong>です。デプロイごとに揺れるとダッシュボードが分断され、比較分析ができません。</p>
<h2 id="ステップ2llm処理を-span-で分割する">ステップ2：LLM処理を span で分割する</h2>
<p>実運用では「遅い」の原因が retrieval なのか generation なのかで対応が変わります。そこで、処理を細かく 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><span 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></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>tracer <span style="color:#f92672">=</span> trace<span style="color:#f92672">.</span>get_tracer(<span style="color:#e6db74">&#34;llm-pipeline&#34;</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">generate_answer</span>(query: str, user_id: str):
</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;rag.pipeline&#34;</span>) <span style="color:#66d9ef">as</span> root:
</span></span><span style="display:flex;"><span>        root<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;user.id&#34;</span>, user_id)
</span></span><span style="display:flex;"><span>        root<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;feature&#34;</span>, <span style="color:#e6db74">&#34;support-chat&#34;</span>)
</span></span><span style="display:flex;"><span>
</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;rag.retrieve&#34;</span>) <span style="color:#66d9ef">as</span> span_retrieve:
</span></span><span style="display:flex;"><span>            docs <span style="color:#f92672">=</span> retrieve_docs(query)
</span></span><span style="display:flex;"><span>            span_retrieve<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;retrieved.count&#34;</span>, len(docs))
</span></span><span style="display:flex;"><span>
</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;rag.rerank&#34;</span>) <span style="color:#66d9ef">as</span> span_rerank:
</span></span><span style="display:flex;"><span>            ranked <span style="color:#f92672">=</span> rerank_docs(query, docs)
</span></span><span style="display:flex;"><span>            span_rerank<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;rerank.top_k&#34;</span>, <span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span>
</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;llm.generate&#34;</span>) <span style="color:#66d9ef">as</span> span_gen:
</span></span><span style="display:flex;"><span>            response <span style="color:#f92672">=</span> call_llm(query, ranked)
</span></span><span style="display:flex;"><span>            span_gen<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;llm.model&#34;</span>, response<span style="color:#f92672">.</span>model)
</span></span><span style="display:flex;"><span>            span_gen<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;llm.input_tokens&#34;</span>, response<span style="color:#f92672">.</span>usage<span style="color:#f92672">.</span>input_tokens)
</span></span><span style="display:flex;"><span>            span_gen<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;llm.output_tokens&#34;</span>, response<span style="color:#f92672">.</span>usage<span style="color:#f92672">.</span>output_tokens)
</span></span><span style="display:flex;"><span>            span_gen<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;llm.temperature&#34;</span>, <span style="color:#ae81ff">0.2</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> response<span style="color:#f92672">.</span>text
</span></span></code></pre></td></tr></table>
</div>
</div><p>この分割で、「retrieval が中央値 70ms → 280ms に悪化」「特定モデルだけ output token が急増」など、運用判断に直結する情報が取得できます。</p>
<h2 id="ステップ3コストをメトリクス化する">ステップ3：コストをメトリクス化する</h2>
<p>運用現場で最も効くのは、<strong>推定コストをリアルタイムに可視化</strong>することです。モデル単価表をコードに持ち、1リクエストごとに計算して metrics に送ります。</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></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>MODEL_PRICE <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;gpt-4.1-mini&#34;</span>: {<span style="color:#e6db74">&#34;in&#34;</span>: <span style="color:#ae81ff">0.0000003</span>, <span style="color:#e6db74">&#34;out&#34;</span>: <span style="color:#ae81ff">0.0000012</span>},
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;gpt-4.1&#34;</span>: {<span style="color:#e6db74">&#34;in&#34;</span>: <span style="color:#ae81ff">0.000003</span>, <span style="color:#e6db74">&#34;out&#34;</span>: <span style="color:#ae81ff">0.000012</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">estimate_cost</span>(model: str, in_tokens: int, out_tokens: int) <span style="color:#f92672">-&gt;</span> float:
</span></span><span style="display:flex;"><span>    p <span style="color:#f92672">=</span> MODEL_PRICE[model]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> in_tokens <span style="color:#f92672">*</span> p[<span style="color:#e6db74">&#34;in&#34;</span>] <span style="color:#f92672">+</span> out_tokens <span style="color:#f92672">*</span> p[<span style="color:#e6db74">&#34;out&#34;</span>]
</span></span></code></pre></td></tr></table>
</div>
</div><p>推奨は次の3指標です。</p>
<ul>
<li><code>llm_cost_usd_total</code>（counter）</li>
<li><code>llm_tokens_input_total</code> / <code>llm_tokens_output_total</code>（counter）</li>
<li><code>llm_latency_ms</code>（histogram）</li>
</ul>
<p>これを feature、tenant、model のラベルで集計すると、予算統制が一気に楽になります。</p>
<h2 id="ステップ4品質低下を検知する仕組みを入れる">ステップ4：品質低下を検知する仕組みを入れる</h2>
<p>レイテンシとコストだけでは不十分です。品質監視を最低限でも導入します。</p>
<h3 id="4-1-自動評価ジョブ">4-1. 自動評価ジョブ</h3>
<p>夜間バッチで固定データセット（100問程度）を流し、次を記録します。</p>
<ul>
<li>正答率（正解文との semantic similarity）</li>
<li>出典一致率（回答が引用した文書IDの妥当性）</li>
<li>禁止事項違反率（PII、コンプラNG）</li>
</ul>
<h3 id="4-2-本番フィードバック">4-2. 本番フィードバック</h3>
<p>UI で 👍 / 👎 を取り、trace_id と紐づけます。こうすると「悪評の大半が temperature=0.9 の実験フラグ経由」など、根因分析が可能です。</p>
<h2 id="ステップ5運用で効くダッシュボードを作る">ステップ5：運用で効くダッシュボードを作る</h2>
<p>実際に使われるダッシュボードは、項目を欲張らない方が強いです。最初は次の 6 つに絞ってください。</p>
<ol>
<li>P50/P95 レイテンシ（全体 + モデル別）</li>
<li>リクエスト数とエラー率（HTTP + LLM例外）</li>
<li>日次コスト（全体 + feature別）</li>
<li>input/output token 推移</li>
<li>retrieval 件数と空振り率</li>
<li>ユーザー評価（👍率）</li>
</ol>
<p>特に P95 とコストは同一画面に置くのがポイントです。高速化で品質が落ちた、または品質改善でコストが跳ねた、というトレードオフが即時に見えます。</p>
<h2 id="よくある失敗と回避策">よくある失敗と回避策</h2>
<h3 id="失敗1prompt全文を生で保存して個人情報を漏らす">失敗1：Prompt全文を生で保存して個人情報を漏らす</h3>
<p>対策は、PII マスキングを export 前に必ず実行することです。メール、電話番号、住所は正規表現だけでなく、NER ベースで二重防御すると安全です。</p>
<h3 id="失敗2span属性の命名がバラバラ">失敗2：span属性の命名がバラバラ</h3>
<p><code>llm.input_tokens</code> と <code>input_token_count</code> が混在すると集計不能になります。命名規約をリポジトリに固定し、CI で lint してください。</p>
<h3 id="失敗3高カーディナリティ地獄">失敗3：高カーディナリティ地獄</h3>
<p><code>user_id</code> をそのままメトリクスラベルに入れると TSDB が破綻します。ユーザー軸は trace/log に置き、metrics は tenant や plan 程度に抑えます。</p>
<h2 id="導入ロードマップ2週間">導入ロードマップ（2週間）</h2>
<ul>
<li><strong>Day 1-2</strong>: FastAPI + LLM呼び出しに trace 埋め込み</li>
<li><strong>Day 3-4</strong>: token/cost メトリクス送信</li>
<li><strong>Day 5-6</strong>: Grafana ダッシュボード構築</li>
<li><strong>Day 7-9</strong>: しきい値アラート設計（P95、error、cost）</li>
<li><strong>Day 10-12</strong>: 品質評価バッチ導入</li>
<li><strong>Day 13-14</strong>: インシデント演習（意図的劣化を検知できるか）</li>
</ul>
<p>2週間で「見える化」は十分達成できます。完璧を目指すより、まず計測可能にすることが重要です。</p>
<h2 id="まとめ">まとめ</h2>
<p>LLM運用で本当に困るのは、失敗そのものではなく「失敗の理由が見えない」状態です。OpenTelemetry を使って retrieval、generation、token、cost、品質を一貫して観測できるようにすると、改善サイクルが回り始めます。</p>
<p>可観測性は守りではなく、開発速度を上げるための攻めの基盤です。まずは span を3つに分けるところから始めてください。それだけで、LLM運用の景色が大きく変わります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>LLM</category>
      <category>OpenTelemetry</category>
      <category>Observability</category>
      <category>Prompt Engineering</category>
    </item>
    <item>
      <title>2026年のAIエージェント進化論：シングルプロンプトからマルチエージェント協調へ</title>
      <link>https://www.ai2core.com/posts/2026-02-24-ai-agents-evolution/</link>
      <pubDate>Tue, 24 Feb 2026 18:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-24-ai-agents-evolution/</guid>
      <description>LangGraphやAutoGenを活用したマルチエージェントシステムのアーキテクチャと実装のポイント。</description>
      <content:encoded><![CDATA[<h1 id="2026年のaiエージェント進化論シングルプロンプトからマルチエージェント協調へ">2026年のAIエージェント進化論：シングルプロンプトからマルチエージェント協調へ</h1>
<h2 id="はじめに">はじめに</h2>
<p>「この複雑なレポート作成、AIに丸投げできないだろうか？」
「ユーザーからの曖昧な指示を解釈して、コードを書き、テストし、デプロイまで自動化したい。」</p>
<p>AI、特に大規模言語モデル（LLM）の進化に触れたエンジニアなら、一度はこんな夢を描いたことがあるのではないでしょうか。しかし、ChatGPTのような単一のプロンプトで対話するモデルに複雑なタスクを依頼すると、途中で文脈を見失ったり、期待とは異なるアウトプットが出てきたりと、その限界に直面することも少なくありません。</p>
<p>ReAct（Reasoning and Acting）のようなフレームワークを用いてツールを使わせる「シングルエージェント」は大きな進歩でしたが、それでもなお、複雑で多段階のタスクを自律的にこなすには力不足でした。まるで、一人の優秀な新入社員に、いきなり会社の全業務を任せるようなものです。</p>
<p>もし、AIが一人ではなく、「専門家チーム」として協調して働いてくれたらどうでしょう？リサーチ担当、コーディング担当、レビュー担当、そしてプロジェクト全体を管理するマネージャー。それぞれが専門知識を持ち、互いにコミュニケーションを取りながら、一つの大きな目標に向かって自律的にタスクを遂行する。</p>
<p>本記事では、そんな未来を実現する技術として注目を集める**「マルチエージェント・システム」**について、その概念から具体的な実装方法までを深く掘り下げます。特に、この分野を牽引する2大フレームワーク、**Microsoftの「AutoGen」<strong>と</strong>LangChainの「LangGraph」**に焦点を当て、そのアーキテクチャ、実装のポイント、そして現場で活かすための実践的なTipsを、豊富なコード例とともに解説していきます。</p>
<p>この記事を読み終える頃には、あなたはシングルプロンプトの呪縛から解き放たれ、自律的なAIエージェントチームを編成するための確かな知識とインスピレーションを得ているはずです。</p>
<h2 id="なぜ今マルチエージェントシステムなのか">なぜ今、マルチエージェント・システムなのか？</h2>
<p>LLMの能力が飛躍的に向上し、GPT-4oのようなマルチモーダル対応モデルが登場する中で、なぜわざわざ複数のエージェントを協調させる必要があるのでしょうか。その理由は、**「シングルエージェントの限界」<strong>と</strong>「タスクの複雑性への対応」**にあります。</p>
<h3 id="シングルエージェントの限界">シングルエージェントの限界</h3>
<p>従来のシングルエージェントのアーキテクチャは、基本的に一つの「思考の連鎖（Chain of Thought）」に依存しています。これは、直線的な思考プロセスには強いものの、以下のような課題を抱えています。</p>
<ol>
<li><strong>思考の硬直性</strong>: 一つの計画に固執し、途中で問題が発生しても柔軟に軌道修正するのが苦手です。複数の選択肢を並行して検討したり、第三者の視点でレビューしたりといった、人間が行うような複雑な意思決定が困難です。</li>
<li><strong>コンテキストの肥大化</strong>: タスクが複雑になるほど、プロンプトに含めるべき情報（過去のやり取り、ツールの使用履歴、中間生成物）が増大します。これはAPIコストの増加、処理速度の低下、そしてLLMが重要な情報を見失う「Lost in the Middle」問題を引き起こします。</li>
<li><strong>責任範囲の曖昧さ</strong>: 一つのエージェントにあらゆる役割（計画、実行、検証、修正）を詰め込もうとすると、プロンプトが極めて複雑になり、かえって性能が低下します。各ステップで何をすべきかが曖昧になり、幻覚（ハルシネーション）のリスクも高まります。</li>
</ol>
<h3 id="人間の組織に学ぶ専門化と協調">人間の組織に学ぶ「専門化」と「協調」</h3>
<p>これらの課題を解決するヒントは、私たち自身の社会、つまり「組織」にあります。優れた企業は、一人の天才が全てをこなすのではなく、営業、開発、マーケティング、品質管理といった専門部署が互いに連携・協調することで、複雑で大きな目標を達成します。</p>
<p>マルチエージェント・システムは、この組織論をAIの世界に持ち込むアプローチです。</p>
<ul>
<li><strong>専門化 (Specialization)</strong>: 各エージェントに特定の役割と専門知識を与えます。「コードを書くのが得意なエージェント」「書かれたコードを厳しくレビューするエージェント」「ユーザーとの対話を受け持つエージェント」といったように、責任範囲を限定することで、各エージェントのプロンプトをシンプルかつ高性能に保てます。</li>
<li><strong>協調 (Collaboration)</strong>: エージェント同士がメッセージを交換し、対話することで、問題を解決します。例えば、コーディングエージェントが書いたコードをレビューエージェントがチェックし、修正点をフィードバックする。この対話のループを通じて、生成物の品質をスパイラル状に向上させることができます。</li>
<li><strong>自律性 (Autonomy)</strong>: 全体の目標が与えられると、エージェントチームは自律的にタスクを分解し、役割を分担し、協調してタスクを遂行します。これにより、人間がマイクロマネジメントする必要がなくなります。</li>
</ul>
<p>このパラダイムシフトは、単なるAIの性能向上ではなく、<strong>AIによる問題解決の「方法論」そのものの進化</strong>と言えるでしょう。</p>
<h2 id="具体的な解決策autogenとlanggraphによる実装">具体的な解決策：AutoGenとLangGraphによる実装</h2>
<p>それでは、実際にマルチエージェント・システムを構築するためのフレームワークを見ていきましょう。ここでは、特に人気の高いAutoGenとLangGraphを取り上げ、それぞれの思想と実装方法を解説します。</p>
<h3 id="1-autogen対話による自律的タスク解決">1. AutoGen：対話による自律的タスク解決</h3>
<p>AutoGenは、Microsoft Researchが開発したフレームワークで、<strong>エージェント間の対話</strong>を中心に据えた設計が特徴です。複数のエージェント（<code>ConversableAgent</code>）を定義し、それらが互いにチャットを繰り返すことで、タスクが進行していきます。</p>
<h4 id="autogenのアーキテクチャ">AutoGenのアーキテクチャ</h4>
<p>AutoGenの基本的な登場人物は以下の通りです。</p>
<ul>
<li><strong><code>AssistantAgent</code></strong>: LLMを搭載した標準的なAIエージェント。与えられた役割（例：「あなたはPythonの専門家です」）に基づいて発言やコード生成を行います。</li>
<li><strong><code>UserProxyAgent</code></strong>: 人間の代理人として振る舞う特殊なエージェント。他のエージェントからコードを受け取ると、それを<strong>実際に実行</strong>しようと試みます。実行結果（成功、失敗、エラーメッセージ）を次のメッセージとして相手に返すことで、対話のループが生まれます。また、人間の入力を促し、介入（Human-in-the-Loop）を可能にします。</li>
<li><strong><code>GroupChatManager</code></strong>: 3体以上のエージェントが参加するグループチャットを管理し、次に誰が発言するかを制御します。</li>
</ul>
<h4 id="実装例コード生成実行タスク">実装例：コード生成＆実行タスク</h4>
<p>ここでは、「あるURLから株価データを取得し、それをプロットして画像ファイルとして保存する」というタスクを、2体のエージェントで解決する例を見てみましょう。</p>
<p><strong>1. セットアップ</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></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 <span style="color:#e6db74">&#34;pyautogen[retrievechat]&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>2. 設定ファイルの準備</strong></p>
<p>プロジェクトのルートに <code>OAI_CONFIG_LIST</code> という名前でJSONファイルを作成し、APIキーを設定します。</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-json" data-lang="json"><span style="display:flex;"><span>[
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;model&#34;</span>: <span style="color:#e6db74">&#34;gpt-4o&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;api_key&#34;</span>: <span style="color:#e6db74">&#34;sk-...&#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><strong>3. Pythonコード</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><span 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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> autogen
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># LLMの設定を読み込む</span>
</span></span><span style="display:flex;"><span>config_list <span style="color:#f92672">=</span> autogen<span style="color:#f92672">.</span>config_list_from_json(<span style="color:#e6db74">&#34;OAI_CONFIG_LIST&#34;</span>)
</span></span><span style="display:flex;"><span>llm_config <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;config_list&#34;</span>: config_list}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 1. アシスタントエージェント（コーダー）の定義</span>
</span></span><span style="display:flex;"><span>coder <span style="color:#f92672">=</span> autogen<span style="color:#f92672">.</span>AssistantAgent(
</span></span><span style="display:flex;"><span>    name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Coder&#34;</span>,
</span></span><span style="display:flex;"><span>    llm_config<span style="color:#f92672">=</span>llm_config,
</span></span><span style="display:flex;"><span>    system_message<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;あなたは優秀なPythonプログラマーです。Pythonコードを生成し、問題を解決します。コードは ```python ... ``` の中に記述してください。&#34;</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:#75715e"># 2. ユーザープロキシエージェント（コード実行者・人間の代理）の定義</span>
</span></span><span style="display:flex;"><span>user_proxy <span style="color:#f92672">=</span> autogen<span style="color:#f92672">.</span>UserProxyAgent(
</span></span><span style="display:flex;"><span>    name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;UserProxy&#34;</span>,
</span></span><span style="display:flex;"><span>    human_input_mode<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;NEVER&#34;</span>,  <span style="color:#75715e"># 人間の入力を介さず自動で進行</span>
</span></span><span style="display:flex;"><span>    max_consecutive_auto_reply<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>    is_termination_msg<span style="color:#f92672">=</span><span style="color:#66d9ef">lambda</span> x: x<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;content&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>)<span style="color:#f92672">.</span>rstrip()<span style="color:#f92672">.</span>endswith(<span style="color:#e6db74">&#34;TERMINATE&#34;</span>),
</span></span><span style="display:flex;"><span>    code_execution_config<span style="color:#f92672">=</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;work_dir&#34;</span>: <span style="color:#e6db74">&#34;coding&#34;</span>,  <span style="color:#75715e"># コードを実行する作業ディレクトリ</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;use_docker&#34;</span>: <span style="color:#66d9ef">False</span>,  <span style="color:#75715e"># Dockerを使わない場合はFalse (True推奨)</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    system_message<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;あなたはコードの実行者です。Coderから提案されたコードを実行し、その結果を報告します。問題があればエラーを伝えてください。&#34;</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:#75715e"># タスクの定義と対話の開始</span>
</span></span><span style="display:flex;"><span>task <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">yfinanceとmatplotlibを使って、過去1年間のテスラ(TSLA)の株価を取得し、
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">終値を折れ線グラフでプロットしてください。
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">グラフは &#39;tsla_stock_price.png&#39; という名前でファイルに保存してください。
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>user_proxy<span style="color:#f92672">.</span>initiate_chat(
</span></span><span style="display:flex;"><span>    coder,
</span></span><span style="display:flex;"><span>    message<span style="color:#f92672">=</span>task
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="実行プロセスの解説">実行プロセスの解説</h4>
<p>このコードを実行すると、<code>user_proxy</code>が最初のタスクを<code>coder</code>に投げます。</p>
<ol>
<li><strong><code>coder</code></strong>: タスクを理解し、<code>yfinance</code>と<code>matplotlib</code>をインストールする必要があると考え、それらを使ったPythonコードを生成して返信します。</li>
<li><strong><code>user_proxy</code></strong>: <code>coder</code>から受け取ったコードブロックを検出し、<code>coding</code>ディレクトリ内でそのコードを実行します。</li>
<li><strong>（成功した場合）</strong>: コードが正常に実行され、<code>tsla_stock_price.png</code>が生成されます。<code>user_proxy</code>は実行結果（標準出力など）を<code>coder</code>に報告します。</li>
<li><strong><code>coder</code></strong>: 成功報告を受け、タスクが完了したと判断し、「TERMINATE」という終了キーワードを含むメッセージを返します。</li>
<li><strong><code>user_proxy</code></strong>: 「TERMINATE」を検知し、対話を終了します。</li>
</ol>
<p>もし途中でエラー（例：ライブラリがインストールされていない）が発生すれば、<code>user_proxy</code>はそのエラーメッセージを<code>coder</code>に伝えます。すると<code>coder</code>は「ライブラリをインストールしてください」といった修正案や、エラーを解決するための新しいコードを提案し、対話が続行されます。この<strong>試行錯誤のループ</strong>こそが、AutoGenの強みです。</p>
<h3 id="2-langgraphグラフによる状態遷移ワークフローの制御">2. LangGraph：グラフによる状態遷移ワークフローの制御</h3>
<p>LangGraphは、人気のLLMフレームワークLangChainから派生したライブラリで、<strong>状態遷移グラフ（Stateful Graphs）<strong>としてエージェントのワークフローを定義します。対話の自律性に重きを置くAutoGenとは対照的に、LangGraphは</strong>ワークフローの制御性</strong>に優れています。</p>
<h4 id="langgraphのアーキテクチャ">LangGraphのアーキテクチャ</h4>
<p>LangGraphの中心的な概念は以下の通りです。</p>
<ul>
<li><strong>State</strong>: グラフ全体で共有される状態オブジェクト。辞書やPydanticモデルで定義し、各ステップの出力がこのStateに蓄積されていきます。</li>
<li><strong>Nodes</strong>: グラフのノード（節点）。Python関数として定義され、それぞれが特定の処理（エージェントの呼び出し、ツールの実行など）を担当します。各ノードは現在の<code>State</code>を受け取り、更新した<code>State</code>の一部を返します。</li>
<li><strong>Edges</strong>: ノード間の繋がり（辺）。どのノードの次にどのノードを実行するかを定義します。</li>
<li><strong>Conditional Edges</strong>: 条件付きの辺。現在の<code>State</code>に基づいて、次に進むべきノードを動的に決定します。これにより、ループや分岐を持つ複雑なワークフローが実現できます。</li>
</ul>
<p><img alt="LangGraphの概念図" loading="lazy" src="https://blog.langchain.dev/content/images/2024/04/image-1.png">
<em>(出典: LangChain Blog)</em></p>
<h4 id="実装例リサーチタスクのワークフロー">実装例：リサーチタスクのワークフロー</h4>
<p>ここでは、「あるテーマについてWebでリサーチし、複数の視点から記事を作成し、それをレビューして最終的なレポートを生成する」というワークフローをLangGraphで構築してみましょう。</p>
<p><strong>1. セットアップ</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></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 langgraph langchain langchain_openai duckduckgo-search
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>2. Pythonコード</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><span 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><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 44
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 45
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 46
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 47
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 48
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 49
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 50
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 51
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 52
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 53
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 54
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 55
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 56
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 57
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 58
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 59
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 60
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 61
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 62
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 63
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 64
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 65
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 66
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 67
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 68
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 69
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 70
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 71
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 72
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 73
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 74
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 75
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 76
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 77
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 78
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 79
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 80
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 81
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 82
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 83
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 84
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 85
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 86
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 87
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 88
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 89
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 90
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 91
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 92
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 93
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 94
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 95
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 96
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 97
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 98
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 99
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">100
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">101
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">102
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">103
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">104
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">105
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">106
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">107
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">108
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">109
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">110
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">111
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">112
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">113
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">114
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">115
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">116
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">117
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">118
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">119
</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">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> TypedDict, Annotated, List
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain_core.messages <span style="color:#f92672">import</span> BaseMessage
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain_openai <span style="color:#f92672">import</span> ChatOpenAI
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langgraph.graph <span style="color:#f92672">import</span> StateGraph, END
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain_community.tools <span style="color:#f92672">import</span> DuckDuckGoSearchRun
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 環境変数にAPIキーを設定</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># os.environ[&#34;OPENAI_API_KEY&#34;] = &#34;YOUR_API_KEY&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ツール（Web検索）の準備</span>
</span></span><span style="display:flex;"><span>search_tool <span style="color:#f92672">=</span> DuckDuckGoSearchRun()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># LLMモデルの定義</span>
</span></span><span style="display:flex;"><span>model <span style="color:#f92672">=</span> ChatOpenAI(temperature<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gpt-4o&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- 1. グラフの状態 (State) を定義 ---</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AgentState</span>(TypedDict):
</span></span><span style="display:flex;"><span>    topic: str
</span></span><span style="display:flex;"><span>    search_results: str
</span></span><span style="display:flex;"><span>    draft: str
</span></span><span style="display:flex;"><span>    review: str
</span></span><span style="display:flex;"><span>    revision_count: int
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- 2. グラフのノード (Nodes) を定義 ---</span>
</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><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">researcher_node</span>(state: AgentState):
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;--- ノード: Researcher ---&#34;</span>)
</span></span><span style="display:flex;"><span>    topic <span style="color:#f92672">=</span> state[<span style="color:#e6db74">&#34;topic&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># LLMに検索クエリを考えさせる</span>
</span></span><span style="display:flex;"><span>    query_generation_prompt <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;「</span><span style="color:#e6db74">{</span>topic<span style="color:#e6db74">}</span><span style="color:#e6db74">」について調査するための、効果的な検索クエリを3つ考えてください。&#34;</span>
</span></span><span style="display:flex;"><span>    query_response <span style="color:#f92672">=</span> model<span style="color:#f92672">.</span>invoke(query_generation_prompt)
</span></span><span style="display:flex;"><span>    queries <span style="color:#f92672">=</span> query_response<span style="color:#f92672">.</span>content<span style="color:#f92672">.</span>strip()<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#39;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#39;</span>)
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    results <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> query <span style="color:#f92672">in</span> queries:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;検索中: </span><span style="color:#e6db74">{</span>query<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        results <span style="color:#f92672">+=</span> search_tool<span style="color:#f92672">.</span>run(query) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;search_results&#34;</span>: results}
</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><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">writer_node</span>(state: AgentState):
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;--- ノード: Writer ---&#34;</span>)
</span></span><span style="display:flex;"><span>    topic <span style="color:#f92672">=</span> state[<span style="color:#e6db74">&#34;topic&#34;</span>]
</span></span><span style="display:flex;"><span>    search_results <span style="color:#f92672">=</span> state[<span style="color:#e6db74">&#34;search_results&#34;</span>]
</span></span><span style="display:flex;"><span>    prompt <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    以下の検索結果を基に、「</span><span style="color:#e6db74">{</span>topic<span style="color:#e6db74">}</span><span style="color:#e6db74">」に関するブログ記事のドラフトを作成してください。
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    検索結果:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    </span><span style="color:#e6db74">{</span>search_results<span style="color:#e6db74">}</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>    draft <span style="color:#f92672">=</span> model<span style="color:#f92672">.</span>invoke(prompt)<span style="color:#f92672">.</span>content
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;draft&#34;</span>: draft}
</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><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">reviewer_node</span>(state: AgentState):
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;--- ノード: Reviewer ---&#34;</span>)
</span></span><span style="display:flex;"><span>    topic <span style="color:#f92672">=</span> state[<span style="color:#e6db74">&#34;topic&#34;</span>]
</span></span><span style="display:flex;"><span>    draft <span style="color:#f92672">=</span> state[<span style="color:#e6db74">&#34;draft&#34;</span>]
</span></span><span style="display:flex;"><span>    prompt <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    あなたは優秀な編集者です。以下の「</span><span style="color:#e6db74">{</span>topic<span style="color:#e6db74">}</span><span style="color:#e6db74">」に関するブログ記事のドラフトをレビューしてください。
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    改善点があれば具体的に指摘し、問題がなければ「PERFECT」とだけ回答してください。
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    ドラフト:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    </span><span style="color:#e6db74">{</span>draft<span style="color:#e6db74">}</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>    review <span style="color:#f92672">=</span> model<span style="color:#f92672">.</span>invoke(prompt)<span style="color:#f92672">.</span>content
</span></span><span style="display:flex;"><span>    revision_count <span style="color:#f92672">=</span> state<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;revision_count&#34;</span>, <span style="color:#ae81ff">0</span>) <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;review&#34;</span>: review, <span style="color:#e6db74">&#34;revision_count&#34;</span>: revision_count}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- 3. 条件付きの辺 (Conditional Edge) を定義 ---</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">should_continue</span>(state: AgentState):
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;--- 条件分岐 ---&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> state[<span style="color:#e6db74">&#34;revision_count&#34;</span>] <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">3</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;最大修正回数に達しました。&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;end&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;PERFECT&#34;</span> <span style="color:#f92672">in</span> state[<span style="color:#e6db74">&#34;review&#34;</span>]:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;レビューをパスしました。&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;end&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;修正が必要です。&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;continue&#34;</span>
</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>workflow <span style="color:#f92672">=</span> StateGraph(AgentState)
</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>workflow<span style="color:#f92672">.</span>add_node(<span style="color:#e6db74">&#34;researcher&#34;</span>, researcher_node)
</span></span><span style="display:flex;"><span>workflow<span style="color:#f92672">.</span>add_node(<span style="color:#e6db74">&#34;writer&#34;</span>, writer_node)
</span></span><span style="display:flex;"><span>workflow<span style="color:#f92672">.</span>add_node(<span style="color:#e6db74">&#34;reviewer&#34;</span>, reviewer_node)
</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>workflow<span style="color:#f92672">.</span>set_entry_point(<span style="color:#e6db74">&#34;researcher&#34;</span>)
</span></span><span style="display:flex;"><span>workflow<span style="color:#f92672">.</span>add_edge(<span style="color:#e6db74">&#34;researcher&#34;</span>, <span style="color:#e6db74">&#34;writer&#34;</span>)
</span></span><span style="display:flex;"><span>workflow<span style="color:#f92672">.</span>add_edge(<span style="color:#e6db74">&#34;writer&#34;</span>, <span style="color:#e6db74">&#34;reviewer&#34;</span>)
</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>workflow<span style="color:#f92672">.</span>add_conditional_edges(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;reviewer&#34;</span>,
</span></span><span style="display:flex;"><span>    should_continue,
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;continue&#34;</span>: <span style="color:#e6db74">&#34;writer&#34;</span>, <span style="color:#75715e"># 修正が必要ならライターに戻る</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;end&#34;</span>: END
</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:#75715e"># グラフをコンパイル</span>
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> workflow<span style="color:#f92672">.</span>compile()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- 5. グラフを実行 ---</span>
</span></span><span style="display:flex;"><span>inputs <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;topic&#34;</span>: <span style="color:#e6db74">&#34;2024年の生成AIのトレンド&#34;</span>, <span style="color:#e6db74">&#34;revision_count&#34;</span>: <span style="color:#ae81ff">0</span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> output <span style="color:#f92672">in</span> app<span style="color:#f92672">.</span>stream(inputs):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> key, value <span style="color:#f92672">in</span> output<span style="color:#f92672">.</span>items():
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;ノード &#39;</span><span style="color:#e6db74">{</span>key<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39; の出力:&#34;</span>)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;---&#34;</span>)
</span></span><span style="display:flex;"><span>        print(value)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;=&#34;</span><span style="color:#f92672">*</span><span style="color:#ae81ff">30</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="実行プロセスの解説-1">実行プロセスの解説</h4>
<p>このコードは、以下のような明確なワークフローを実行します。</p>
<ol>
<li><strong><code>researcher</code></strong>: 与えられたトピックに基づいてWeb検索を実行し、結果を<code>State</code>に保存します。</li>
<li><strong><code>writer</code></strong>: <code>researcher</code>が収集した情報をもとに、記事のドラフトを作成し、<code>State</code>に保存します。</li>
<li><strong><code>reviewer</code></strong>: <code>writer</code>が書いたドラフトをレビューします。</li>
<li><strong><code>should_continue</code> (条件分岐)</strong>:
<ul>
<li>レビュー結果が「PERFECT」なら、ワークフローは終了（<code>END</code>）します。</li>
<li>修正点があれば、<code>writer</code>ノードに処理を戻し、ドラフトの修正を促します（ループ）。</li>
<li>ループが3回を超えた場合も、無限ループを避けるために処理を終了します。</li>
</ul>
</li>
</ol>
<p>このように、LangGraphは処理の流れを明示的にグラフとして定義するため、デバッグが容易で、ビジネスロジックのような複雑なフローを堅牢に実装するのに適しています。</p>
<h2 id="メリットとデメリットそしてツールの比較">メリットとデメリット、そしてツールの比較</h2>
<p>マルチエージェント・システムは強力ですが、銀の弾丸ではありません。導入にあたっては、その利点と課題を理解することが重要です。</p>
<h3 id="マルチエージェントシステムのメリット">マルチエージェント・システムのメリット</h3>
<ul>
<li><strong>高度な問題解決能力</strong>: 複雑なタスクを専門家チームのように分業・協調して解決できる。</li>
<li><strong>堅牢性と自己修正</strong>: レビューやフィードバックのループを組み込むことで、生成物の品質を向上させ、エラーから自律的に回復できる。</li>
<li><strong>モジュール性と拡張性</strong>: 新しい役割を持つエージェントをノードや対話者として追加するのが比較的容易。</li>
<li><strong>プロセスの透明性</strong>: エージェント間の対話ログや状態遷移を追跡することで、AIが「どのように」その結論に至ったのかを理解しやすくなる。</li>
</ul>
<h3 id="マルチエージェントシステムのデメリットと課題">マルチエージェント・システムのデメリットと課題</h3>
<ul>
<li><strong>設計の複雑性</strong>: どのような役割のエージェントが必要か、どのようなワークフローや対話プロトコルを設計するかが成功の鍵となり、高度な設計能力が求められる。</li>
<li><strong>制御の難しさ</strong>: 特に自律性の高いシステムでは、エージェントが無限ループに陥ったり、意図しない方向にタスクを進めたりするリスクがある。</li>
<li><strong>コストの増加</strong>: 複数のエージェントが何度もLLM APIを呼び出すため、シングルエージェントに比べてトークン消費量とコストが大幅に増加する可能性がある。</li>
<li><strong>レイテンシーの増大</strong>: エージェント間の通信やLLMの呼び出しが重なるため、最終的な結果を得るまでの時間が長くなる傾向がある。</li>
</ul>
<h3 id="langgraph-vs-autogenどちらを選ぶべきか">LangGraph vs AutoGen：どちらを選ぶべきか？</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: left">特徴</th>
          <th style="text-align: left">LangGraph (by LangChain)</th>
          <th style="text-align: left">AutoGen (by Microsoft)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>思想</strong></td>
          <td style="text-align: left"><strong>状態遷移グラフ</strong>によるワークフロー制御</td>
          <td style="text-align: left"><strong>対話</strong>による自律的な協調</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>制御性</strong></td>
          <td style="text-align: left"><strong>高い</strong>。処理の流れを明示的にグラフで定義するため、予測可能でデバッグしやすい。</td>
          <td style="text-align: left"><strong>中程度</strong>。エージェント間の対話に依存するため、創発的な挙動を示すが、制御は難しい。</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>柔軟性</strong></td>
          <td style="text-align: left"><strong>非常に高い</strong>。ノードはただのPython関数なので、任意のロジックを自由に組み込める。</td>
          <td style="text-align: left"><strong>高い</strong>。Agentクラスを継承してカスタマイズ可能だが、対話の枠組みに従う必要がある。</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>学習コスト</strong></td>
          <td style="text-align: left">やや高い。グラフ理論や状態管理の概念を理解する必要がある。</td>
          <td style="text-align: left">比較的低い。<code>initiate_chat</code>で始められ、直感的に理解しやすい。</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>ベストな用途</strong></td>
          <td style="text-align: left">複雑なビジネスプロセス、ETLパイプライン、自己修正ループなど、<strong>手順が明確なタスク</strong>。</td>
          <td style="text-align: left">研究開発、コード生成、ブレーンストーミングなど、<strong>解決策が未知で探索的なタスク</strong>。</td>
      </tr>
  </tbody>
</table>
<p>結論として、「厳密なワークフローを構築したいならLangGraph」「エージェントの自律的な協調に任せてみたいならAutoGen」という使い分けが考えられます。</p>
<h2 id="現場で使える実践的なtips">現場で使える実践的なTips</h2>
<p>マルチエージェント・システムを本番環境で運用するには、いくつかの工夫が必要です。</p>
<ol>
<li><strong>スモールスタートを心がける</strong>: 最初から10体のエージェントチームを作るのではなく、まずは2〜3体のコアな役割のエージェントから始め、徐々に拡張していきましょう。</li>
<li><strong>役割（Role）のプロンプトを磨き込む</strong>: 各エージェントの<code>system_message</code>は、その性能を決定づける最も重要な要素です。「あなたは何者で、何が得意で、何をしてはいけないのか」を可能な限り明確に定義してください。</li>
<li><strong>強力なマネージャー/オーケストレーターを置く</strong>: LangGraphのグラフ定義そのものや、複数のエージェントを統括するマネージャーエージェントの設計は非常に重要です。タスクの分解、進行管理、最終的な成果物の統合といった役割を担わせましょう。</li>
<li><strong>コスト管理戦略を立てる</strong>:
<ul>
<li><strong>モデルの使い分け</strong>: 簡単なタスク（要約、分類など）には安価なモデル（例: GPT-3.5 Turbo, Claude 3 Sonnet）を使い、高度な推論やコーディングが必要な場面では高性能モデル（例: GPT-4o, Claude 3 Opus）を使うハイブリッド構成を検討します。</li>
<li><strong>サーキットブレーカー</strong>: APIコールの回数や対話のターン数に上限を設け、無限ループによるコスト増大を防ぎます。LangGraphの例で示した<code>revision_count</code>がこれにあたります。</li>
</ul>
</li>
<li><strong>人間参加のループ (Human-in-the-Loop) を組み込む</strong>: 全てを自動化するのではなく、重要な意思決定ポイント（例：生成したコードの実行前、顧客へのメール送信前）では、必ず人間の承認を求めるステップをワークフローに組み込みましょう。AutoGenの<code>UserProxyAgent</code>は、このための優れた仕組みを提供しています。</li>
<li><strong>ロギングとトレーサビリティ</strong>: エージェント間の全てのやり取りや状態の変化を詳細にログとして記録します。LangSmithのようなツールを使うと、複雑なエージェントの挙動を可視化し、デバッグを大幅に効率化できます。</li>
</ol>
<h2 id="まとめ">まとめ</h2>
<p>私たちは今、AI開発における大きな転換点に立っています。単一のLLMに完璧な答えを求める「シングルプロンプトの時代」は終わりを告げ、多様な能力を持つAIエージェントが協調して複雑な問題を解決する**「マルチエージェント協調の時代」**が幕を開けようとしています。</p>
<p>この記事では、その中核技術であるマルチエージェント・システムの概念と、それを実現するAutoGenとLangGraphという二つの強力なフレームワークについて解説しました。</p>
<ul>
<li><strong>AutoGen</strong>は、エージェント間の「対話」を通じて、自己修正的なループを生み出し、探索的なタスクを自律的に解決します。</li>
<li><strong>LangGraph</strong>は、「状態遷移グラフ」としてワークフローを明示的に定義することで、複雑なビジネスプロセスを堅牢かつ制御可能に実装します。</li>
</ul>
<p>これらの技術は、まだ発展途上であり、コストや制御性の面で課題も残されています。しかし、そのポテンシャルは計り知れません。もはや私たちの仕事は、単に賢いAIを一つ作ることではなく、<strong>いかにして「優秀なAIチーム」を設計し、編成し、マネジメントするか</strong>という、より高度な次元へとシフトしています。</p>
<p>2026年に向けて、この流れはさらに加速していくでしょう。ぜひ、この記事をきっかけに、まずは簡単な2エージェントシステムから、あなたの身の回りの課題解決に挑戦してみてください。そこに、次世代のAIアプリケーション開発の未来が広がっているはずです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>AI Agent</category>
      <category>LangChain</category>
      <category>LLM</category>
    </item>
    <item>
      <title>WebAssemblyとWASI：ブラウザを越えてサーバーサイドへ進出するWasmの可能性</title>
      <link>https://www.ai2core.com/posts/2026-02-24-webassembly-wasi/</link>
      <pubDate>Tue, 24 Feb 2026 12:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-24-webassembly-wasi/</guid>
      <description>WASI (WebAssembly System Interface) の基本概念と、Docker代替として期待されるサーバーサイドWasmの動向。</description>
      <content:encoded><![CDATA[<h2 id="webassemblyとwasiブラウザを越えてサーバーサイドへ進出するwasmの可能性">WebAssemblyとWASI：ブラウザを越えてサーバーサイドへ進出するWasmの可能性</h2>
<h3 id="はじめに">はじめに</h3>
<p>「コンテナの起動が遅い…」「開発環境と本番環境の差異でまたハマった…」「マイクロサービスのイメージサイズが肥大化してリソースを圧迫している…」</p>
<p>もしあなたがサーバーサイド開発に携わっているなら、このような悩みに一度は直面したことがあるのではないでしょうか。Dockerをはじめとするコンテナ技術は、現代のアプリケーション開発に革命をもたらしましたが、その一方で、起動時間のオーバーヘッド、リソース消費、セキュリティの懸念といった新たな課題も生み出しています。</p>
<p>もし、コンテナよりも高速に起動し、軽量で、かつ強力なセキュリティサンドボックスを持つ技術があるとしたらどうでしょう？しかも、特定のOSやCPUアーキテクチャに依存せず、真のポータビリティを実現できるとしたら？</p>
<p>この記事では、その答えとなりうる「WebAssembly (Wasm)」と、そのエコシステムをブラウザの外へ拡張する「WASI (WebAssembly System Interface)」について、深く掘り下げていきます。単なる技術解説に留まらず、Wasmがなぜ「Dockerの次」とまで言われるのか、その理由とサーバーサイドでの具体的な活用法、そして未来の可能性までを、コード例を交えながら徹底的に解説します。この記事を読み終える頃には、あなたはWasmがサーバーサイドコンピューティングの新たなパラダイムを切り拓く可能性を確信しているはずです。</p>
<h3 id="なぜ今サーバーサイドwasmが重要なのか">なぜ今、サーバーサイドWasmが重要なのか？</h3>
<p>WebAssemblyは、もともとWebブラウザ上でネイティブコードに近いパフォーマンスを実現するために生まれました。JavaScriptの代替または補完として、C/C++/Rustなどで書かれたコードを高速に実行できるバイナリフォーマットとして注目を集め、既に多くのWebアプリケーションで活用されています。</p>
<p>しかし、Wasmの持つ4つの主要な特性は、ブラウザの世界に留めておくにはあまりにも魅力的でした。</p>
<ol>
<li><strong>高速 (Fast):</strong> ネイティブに近い速度で実行可能な、効率的なバイナリフォーマット。</li>
<li><strong>安全 (Secure):</strong> デフォルトでメモリ安全なサンドボックス内で実行され、ホストシステムへのアクセスは明示的に許可された機能に限定される（Capability-based security）。</li>
<li><strong>ポータブル (Portable):</strong> 特定のCPUアーキテクチャやOSに依存しない。Wasmランタイムがあればどこでも同じように動作する。</li>
<li><strong>コンパクト (Compact):</strong> バイナリフォーマットは非常に小さく、ネットワーク経由での配信やストレージ効率に優れる。</li>
</ol>
<p>これらの特性は、サーバーサイドやエッジコンピューティングが抱える課題、特にコンテナ技術のペインポイントを解決する大きな可能性を秘めていました。</p>
<h4 id="コンテナ技術が抱える課題">コンテナ技術が抱える課題</h4>
<p>Dockerは素晴らしい技術ですが、いくつかのトレードオフがあります。</p>
<ul>
<li><strong>起動時間とオーバーヘッド:</strong> コンテナは軽量な仮想マシンと言われますが、それでもアプリケーションの起動にはOSのプロセスを立ち上げ、ファイルシステムをマウントするなど、数秒単位の時間がかかります。これがFaaS（Function as a Service）などのコールドスタート問題の一因となります。</li>
<li><strong>リソース消費:</strong> 各コンテナは、アプリケーションの依存ライブラリだけでなく、OSのユーザーランドの一部を含むレイヤ化されたイメージを持ちます。これにより、イメージサイズが数百MBから数GBに達することも珍しくなく、ディスク容量やメモリを消費します。</li>
<li><strong>セキュリティ:</strong> コンテナはNamespaceやCgroupsといったLinuxカーネルの機能を利用してプロセスを分離しますが、ホストOSのカーネルを共有しています。そのため、カーネルの脆弱性がコンテナの分離を破壊するリスクが常に存在します。</li>
<li><strong>ポータビリティの限界:</strong> 「Linuxコンテナ」はLinuxカーネル上で動作することを前提としています。WindowsやmacOSでDockerを使う場合、内部的にはLinuxの仮想マシンが動作しており、真のクロスプラットフォームとは言えません。</li>
</ul>
<p>これらの課題に対し、WebAssemblyは全く新しいアプローチを提示します。OS全体を仮想化するのではなく、個々のアプリケーションプロセスを、OSから完全に独立した軽量なサンドボックス内で実行するのです。</p>
<p>しかし、ここで一つの大きな壁がありました。オリジナルのWebAssemblyは、ブラウザのJavaScript APIと連携することしか想定されていなかったのです。ファイルシステムへのアクセス、ネットワーク通信、現在時刻の取得といった、サーバーサイドアプリケーションに必須の機能が標準化されていませんでした。</p>
<p>この問題を解決するために登場したのが、<strong>WASI (WebAssembly System Interface)</strong> です。</p>
<h3 id="wasiwebassemblyと世界をつなぐ架け橋">WASI：WebAssemblyと世界をつなぐ架け橋</h3>
<p>WASIは、WebAssemblyモジュールがホスト環境（ブラウザ、サーバー、エッジデバイスなど）のシステム機能へアクセスするための、標準化されたAPIです。WASIを「WebAssemblyのためのOSインターフェース」あるいは「POSIXのようなもの」と考えると分かりやすいでしょう。</p>
<p>WASIの登場により、Wasmはついにブラウザという揺りかごから飛び立ち、サーバーサイドという広大な大地でその真価を発揮する準備が整いました。</p>
<h4 id="wasiの仕組み">WASIの仕組み</h4>
<p>WASIは、Capability-based security（権限ベースのセキュリティ）モデルを基本としています。これは「プログラムは、明示的に与えられた権限（ファイルディスクリプタ、ソケットなど）しか利用できない」という原則です。</p>
<p>以下の図は、Wasm/WASIアプリケーションがOSとどのように対話するかを示しています。</p>
<pre tabindex="0"><code>+--------------------------+
|  Your Application Code   |  (e.g., Rust, Go, C++)
|  (Business Logic)        |
+--------------------------+
             | (Compile Time)
             v
+--------------------------+
|    Wasm Module (.wasm)   |
| (contains WASI imports)  |
+--------------------------+
             | (Runtime)
+--------------------------+  &lt;-- Wasm Sandbox Boundary
|      Wasm Runtime        |
| (e.g., Wasmtime, Wasmer) |
+------------+-------------+
             | (WASI Implementation)
             v
+--------------------------+
|        Host OS           |
| (Linux, macOS, Windows)  |
+--------------------------+
</code></pre><ol>
<li><strong>アプリケーションコード:</strong> 開発者は使い慣れた言語（Rust, Go, C++など）でコードを書きます。</li>
<li><strong>コンパイル:</strong> 専用のツールチェイン（例: <code>wasm32-wasi</code> ターゲット）を使い、Wasmモジュール（<code>.wasm</code> ファイル）にコンパイルします。この時、ファイルI/Oなどのシステムコールは、WASIのimport関数呼び出しに変換されます。</li>
<li><strong>実行:</strong> Wasmランタイムが <code>.wasm</code> ファイルをロードします。</li>
<li><strong>権限の付与:</strong> ランタイムを起動する際に、「このディレクトリへの読み込みを許可する」「このポートでの待ち受けを許可する」といった権限を明示的に与えます。</li>
<li><strong>システムコール:</strong> WasmモジュールがWASI関数（例: <code>fd_write</code>）を呼び出すと、ランタイムがそれを捕捉し、与えられた権限の範囲内でホストOSの対応するシステムコール（例: <code>write</code>）を実行します。</li>
</ol>
<p>この仕組みにより、Wasmモジュールは悪意のあるコードを含んでいたとしても、許可されていないファイルにアクセスしたり、意図しないネットワーク接続を確立したりすることは原理的に不可能です。これは、コンテナよりも遥かにきめ細かく、強力なセキュリティモデルと言えます。</p>
<h4 id="実践rustでwasiアプリケーションを作ってみる">実践：RustでWASIアプリケーションを作ってみる</h4>
<p>百聞は一見に如かず。実際に簡単なWASIアプリケーションを作成し、動かしてみましょう。ここでは、多くのWasm/WASIプロジェクトで採用されているRustを使用します。</p>
<p><strong>ステップ1: 環境構築</strong></p>
<p>まず、RustとWasmのコンパイルターゲットをインストールします。</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Rustをインストール (未インストールの場合)</span>
</span></span><span style="display:flex;"><span>curl --proto <span style="color:#e6db74">&#39;=https&#39;</span> --tlsv1.2 -sSf https://sh.rustup.rs | sh
</span></span><span style="display:flex;"><span>source <span style="color:#e6db74">&#34;</span>$HOME<span style="color:#e6db74">/.cargo/env&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># wasm32-wasi ターゲットを追加</span>
</span></span><span style="display:flex;"><span>rustup target add wasm32-wasi
</span></span></code></pre></td></tr></table>
</div>
</div><p>次に、Wasmランタイムをインストールします。ここでは代表的なランタイムの一つである <code>Wasmtime</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>curl https://wasmtime.dev/install.sh -sSf | bash
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>ステップ2: コードの作成</strong></p>
<p><code>hello-wasi</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-bash" data-lang="bash"><span style="display:flex;"><span>cargo new hello-wasi
</span></span><span style="display:flex;"><span>cd hello-wasi
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>src/main.rs</code> を開き、以下のコードに書き換えます。このコードは、カレントディレクトリに <code>output.txt</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></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-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::fs::File;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::io::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::env;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">main</span>() -&gt; <span style="color:#a6e22e">std</span>::io::Result<span style="color:#f92672">&lt;</span>()<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    println!(<span style="color:#e6db74">&#34;Hello from inside Wasm!&#34;</span>);
</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><span style="color:#75715e"></span>    <span style="color:#66d9ef">let</span> args: Vec<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span> <span style="color:#f92672">=</span> env::args().collect();
</span></span><span style="display:flex;"><span>    println!(<span style="color:#e6db74">&#34;I see these args: </span><span style="color:#e6db74">{:?}</span><span style="color:#e6db74">&#34;</span>, args);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// output.txt にメッセージを書き込む
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">let</span> <span style="color:#66d9ef">mut</span> file <span style="color:#f92672">=</span> File::create(<span style="color:#e6db74">&#34;output.txt&#34;</span>)<span style="color:#f92672">?</span>;
</span></span><span style="display:flex;"><span>    file.write_all(<span style="color:#e6db74">b</span><span style="color:#e6db74">&#34;Hello, WASI world!</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)<span style="color:#f92672">?</span>;
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    println!(<span style="color:#e6db74">&#34;Successfully wrote to output.txt&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Ok(())
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>このコードは、標準的なRustのファイル操作API (<code>std::fs::File</code>) を使っているだけです。WASIの素晴らしい点は、開発者がWASI固有のAPIを意識する必要がほとんどないことです。既存の標準ライブラリが、コンパイル時に自動的にWASIの呼び出しに変換されます。</p>
<p><strong>ステップ3: コンパイル</strong></p>
<p><code>wasm32-wasi</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>cargo build --target wasm32-wasi
</span></span></code></pre></td></tr></table>
</div>
</div><p>成功すると、<code>target/wasm32-wasi/debug/hello-wasi.wasm</code> というファイルが生成されます。これが私たちのWASIアプリケーション本体です。</p>
<p><strong>ステップ4: 実行</strong></p>
<p><code>Wasmtime</code> を使って実行します。ここで重要なのが、WASIの権限設定です。</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><span style="color:#75715e"># --dir=. でカレントディレクトリへのアクセスを許可する</span>
</span></span><span style="display:flex;"><span>wasmtime run --dir<span style="color:#f92672">=</span>. target/wasm32-wasi/debug/hello-wasi.wasm -- some test args
</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><span style="color:#75715e"># Hello from inside Wasm!</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I see these args: [&#34;target/wasm32-wasi/debug/hello-wasi.wasm&#34;, &#34;some&#34;, &#34;test&#34;, &#34;args&#34;]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Successfully wrote to output.txt</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>実行後、カレントディレクトリを確認すると、<code>output.txt</code> が作成され、中に &ldquo;Hello, WASI world!&rdquo; と書き込まれているはずです。</p>
<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></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"># --dir=. を付けずに実行</span>
</span></span><span style="display:flex;"><span>wasmtime run target/wasm32-wasi/debug/hello-wasi.wasm
</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><span style="color:#75715e"># Hello from inside Wasm!</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I see these args: [&#34;target/wasm32-wasi/debug/hello-wasi.wasm&#34;]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Error: failed to run main module `...`</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Caused by:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#    0: failed to create file `output.txt`</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#    1: open &#34;output.txt&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#    2: capability not allowed</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>capability not allowed</code> というエラーメッセージが表示され、プログラムが異常終了しました。これは、WASIのセキュリティモデルが正しく機能している証拠です。Wasmモジュールは、許可されていないリソース（この場合はカレントディレクトリへの書き込み）にアクセスしようとして、ランタイムによってブロックされたのです。</p>
<h3 id="メリットとデメリットサーバーサイドwasm-vs-コンテナ">メリットとデメリット：サーバーサイドWasm vs コンテナ</h3>
<p>Wasm/WASIがコンテナと比べてどのような利点と欠点を持つのか、表にまとめて比較してみましょう。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">項目</th>
          <th style="text-align: left">WebAssembly (WASI)</th>
          <th style="text-align: left">コンテナ (Docker)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>起動速度</strong></td>
          <td style="text-align: left"><strong>非常に高速 (サブミリ秒)</strong></td>
          <td style="text-align: left">高速 (数百ミリ秒〜数秒)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>リソース消費</strong></td>
          <td style="text-align: left"><strong>非常に軽量 (数MB)</strong></td>
          <td style="text-align: left">軽量 (数十MB〜数GB)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>セキュリティ</strong></td>
          <td style="text-align: left"><strong>強力なサンドボックス (Capability-based)</strong></td>
          <td style="text-align: left">プロセス分離 (Namespace, Cgroups)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>ポータビリティ</strong></td>
          <td style="text-align: left"><strong>OSとCPUアーキテクチャから独立</strong></td>
          <td style="text-align: left">OSカーネルに依存 (主にLinux)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>イメージサイズ</strong></td>
          <td style="text-align: left"><strong>非常に小さい (数KB〜数MB)</strong></td>
          <td style="text-align: left">大きい (数百MB〜数GB)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>エコシステム</strong></td>
          <td style="text-align: left">発展途上</td>
          <td style="text-align: left"><strong>成熟</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>標準化</strong></td>
          <td style="text-align: left">進行中 (ネットワーク、スレッド等)</td>
          <td style="text-align: left"><strong>事実上の標準 (OCI)</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>デバッグ/監視</strong></td>
          <td style="text-align: left">ツールが限定的</td>
          <td style="text-align: left"><strong>ツールが豊富</strong></td>
      </tr>
  </tbody>
</table>
<h4 id="wasmwasiの明確なメリット">Wasm/WASIの明確なメリット</h4>
<ul>
<li><strong>圧倒的なパフォーマンス:</strong> サブミリ秒単位の起動速度は、コールドスタートが致命的となるFaaSやサーバーレス環境で絶大な効果を発揮します。また、メモリフットプリントが小さいため、同じハードウェア上でより多くのインスタンスを高密度に実行できます。</li>
<li><strong>鉄壁のセキュリティ:</strong> デフォルトで何も許可しないサンドボックスモデルは、「最小権限の原則」を強制します。これにより、サプライチェーン攻撃などで悪意のあるコードが混入した場合でも、被害を最小限に抑えることができます。</li>
<li><strong>真のポータビリティ:</strong> 一度Wasmにコンパイルすれば、Wasmランタイムが動作する環境であれば、Linuxサーバー、Windowsデスクトップ、macOSラップトップ、Raspberry Pi、さらにはIoTデバイスまで、どこでも全く同じように動作します。<code>build once, run anywhere</code> の真の実現です。</li>
</ul>
<h4 id="wasmwasiの現在の課題デメリット">Wasm/WASIの現在の課題（デメリット）</h4>
<ul>
<li><strong>エコシステムの未成熟さ:</strong> コンテナにはDocker Hub、Kubernetes、Prometheus、Helmといった巨大で成熟したエコシステムが存在します。WasmにもWasmEdge、Spin、WasmCloudといった有望なプロジェクトはありますが、ツールの成熟度や選択肢の豊富さではまだ及びません。</li>
<li><strong>標準化の途上:</strong> WASIは現在も活発に開発が進められています。ファイルシステムや標準入出力といった基本的な部分は安定していますが、非同期I/O、高度なネットワーク機能（ソケット）、スレッディングといった部分はまだ標準化の途上にあります。これにより、現状では複雑なネットワークアプリケーションの実装が難しい場合があります。</li>
<li><strong>既存資産との連携:</strong> 多くの企業は、既にコンテナベースのCI/CDパイプラインやオーケストレーションシステムを構築しています。これらの既存資産とWasmをどう連携させていくかは、導入における大きな課題です。</li>
</ul>
<p>Wasmはコンテナを完全に置き換える「銀の弾丸」ではなく、それぞれの特性を理解し、適材適所で使い分ける、あるいは組み合わせて利用することが重要です。</p>
<h3 id="現場で使える実践的なtipswasmはどこで輝くのか">現場で使える実践的なTips：Wasmはどこで輝くのか？</h3>
<p>理論は十分です。では、具体的にどのようなユースケースでWasm/WASIは真価を発揮するのでしょうか？</p>
<h4 id="1-faas--サーバーレスコンピューティング">1. FaaS / サーバーレスコンピューティング</h4>
<p>AWS LambdaなどのFaaSプラットフォームでは、コールドスタートが常に課題となります。Wasmの超高速な起動時間は、この問題を劇的に改善します。リクエストが来てからWasmインスタンスを起動しても、ユーザーが体感できるほどの遅延は発生しません。Fermyon CloudやCloudflare Workersといったプラットフォームは、既にこの利点を活かしたサービスを提供しています。</p>
<h4 id="2-エッジコンピューティング">2. エッジコンピューティング</h4>
<p>リソースが限られ、多様なハードウェアが混在するエッジ環境は、Wasmの独壇場です。軽量でポータブルなWasmモジュールは、CPUパワーやメモリが少ないデバイスでも効率的に動作し、中央のサーバーから簡単にデプロイ・更新できます。CDNのエッジでリクエストを加工したり、IoTゲートウェイでセンサーデータを処理したりといった用途に最適です。</p>
<h4 id="3-安全なプラグインシステム">3. 安全なプラグインシステム</h4>
<p>アプリケーションにサードパーティ製のプラグインやユーザー定義関数（UDF）を組み込む際、セキュリティは最大の懸念事項です。Wasmの強力なサンドボックスを使えば、プラグインコードがホストアプリケーションやシステムに悪影響を与えることを防ぎ、安全に機能を拡張できます。Envoyプロキシのフィルター、データベースのUDF、SaaSアプリケーションのカスタマイズ機能などで採用が進んでいます。</p>
<h4 id="4-軽量なマイクロサービス">4. 軽量なマイクロサービス</h4>
<p>全てのマイクロサービスがコンテナを必要とするわけではありません。単一の責務を持つ小さなサービスであれば、Wasmモジュールとして実装・デプロイすることで、リソース消費を大幅に削減し、デプロイ時間を短縮できます。Fermyonが開発する <strong>Spin</strong> は、Wasmベースのマイクロサービス開発を簡素化するための優れたフレームワークです。</p>
<p><strong>Spinを使ってみる:</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><span 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></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"># Spinをインストール</span>
</span></span><span style="display:flex;"><span>curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># RustベースのHTTPマイクロサービスを作成</span>
</span></span><span style="display:flex;"><span>spin new -t http-rust my-first-spin-app
</span></span><span style="display:flex;"><span>cd my-first-spin-app
</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>cargo build --target wasm32-wasi
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ビルド &amp; 実行</span>
</span></span><span style="display:flex;"><span>spin build <span style="color:#f92672">&amp;&amp;</span> spin up
</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><span style="color:#75715e"># curl -i http://127.0.0.1:3000</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># HTTP/1.1 200 OK</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># content-type: text/plain; charset=utf-8</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># content-length: 12</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># date: ...</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Hello, Fermyon</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>このように、フレームワークを利用することで、驚くほど簡単にWasmベースのアプリケーションを構築し、実行できます。</p>
<h3 id="まとめコンテナの次に来る波に備える">まとめ：コンテナの次に来る波に備える</h3>
<p>WebAssemblyとWASIが切り拓くサーバーサイドの世界は、まだ始まったばかりです。しかし、そのポテンシャルは計り知れません。高速な起動、軽量なフットプリント、堅牢なセキュリティ、そして真のポータビリティは、現在のクラウドネイティブ技術が抱える多くの課題に対する、エレガントな解決策となり得ます。</p>
<p>WasmがすぐにDockerやKubernetesを完全に置き換えることはないでしょう。むしろ、最初はKubernetes上でWasmワークロードを実行する <code>Krustlet</code> のようなプロジェクトを通じて共存し、徐々にその適用範囲を広げていくと考えられます。FaaS、エッジ、プラグインといった特定の領域で強みを発揮し、やがてはマイクロサービスの主要な選択肢の一つとなる未来が待っています。</p>
<p>エンジニアとして、私たちはこの新しい波に乗り遅れるわけにはいきません。まずは手元でWasmtimeやSpinを試し、簡単なWASIアプリケーションをビルドしてみてください。RustやGo、TinyGoといった言語で、その開発体験に触れてみてください。</p>
<p>ブラウザのサンドボックスから解き放たれ、サーバーサイドという広大な舞台に立ったWebAssembly。その進化は、これからのアプリケーション開発のあり方を根底から変える可能性を秘めています。コンテナがもたらした革命の次の章は、今まさに始まろうとしているのです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>WebAssembly</category>
      <category>WASI</category>
      <category>Backend</category>
    </item>
    <item>
      <title>React Compilerがもたらすフロントエンドの革新：手動メモ化からの解放</title>
      <link>https://www.ai2core.com/posts/2026-02-24-react-compiler/</link>
      <pubDate>Tue, 24 Feb 2026 08:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-24-react-compiler/</guid>
      <description>React Compilerの仕組みと、useMemoやuseCallbackが不要になる未来のReact開発について。</description>
      <content:encoded><![CDATA[<h2 id="react-compilerがもたらすフロントエンドの革新手動メモ化からの解放">React Compilerがもたらすフロントエンドの革新：手動メモ化からの解放</h2>
<h3 id="はじめにそのusememo本当に必要ですか">はじめに：そのuseMemo、本当に必要ですか？</h3>
<p>React開発者の皆さん、日々のコーディングお疲れ様です。コンポーネントのパフォーマンスを最適化するために、<code>useMemo</code>や<code>useCallback</code>、<code>React.memo</code>といったAPIと格闘した経験は誰にでもあるでしょう。依存配列（dependency array）を睨みながら、「この値は含めるべきか？」「この関数をメモ化しないと子コンポーネントが再レンダリングされてしまう…」と頭を悩ませる時間は、決して少なくないはずです。</p>
<p>これらのAPIは、Reactアプリケーションのパフォーマンスを維持するための強力なツールである一方、私たちのコードに複雑さをもたらす諸刃の剣でもあります。</p>
<ul>
<li><strong>コードの可読性の低下:</strong> 本来のロジックとは無関係なメモ化のための記述が、コンポーネントを肥大化させます。</li>
<li><strong>依存配列の管理ミス:</strong> 依存配列に漏れがあれば、値が更新されず、気づきにくいバグの原因となります。逆に、過剰に含めればメモ化の意味がなくなります。</li>
<li><strong>早すぎる最適化:</strong> パフォーマンス上の問題が起きていないにもかかわらず、慣習的にすべての関数を<code>useCallback</code>でラップしてしまう「過剰なメモ化」は、かえってコードを複雑にするだけです。</li>
</ul>
<p>もし、このような手動でのパフォーマンスチューニングから解放され、Reactが「よしなに」最適なパフォーマンスを発揮してくれるとしたら、私たちの開発体験はどれほど向上するでしょうか？</p>
<p>この記事では、その夢のような未来を実現する可能性を秘めた<strong>React Compiler</strong>について、その仕組みから私たち開発者に与える影響まで、徹底的に深掘りしていきます。React Compilerは、単なる便利ツールではありません。それは、Reactのメンタルモデルそのものを変革し、私たちを「手動メモ化の呪縛」から解放する、フロントエンド開発の大きな一歩なのです。</p>
<h3 id="なぜreact-compilerは生まれたのか背景にある根深い課題">なぜReact Compilerは生まれたのか？背景にある根深い課題</h3>
<p>React Compilerの重要性を理解するためには、まずReactの基本的なレンダリングの仕組みと、なぜ私たちが<code>useMemo</code>や<code>useCallback</code>を使わなければならなかったのかを再確認する必要があります。</p>
<h4 id="reactの再レンダリングの仕組みと素朴さ">Reactの再レンダリングの仕組みと「素朴さ」</h4>
<p>Reactの基本的な設計思想は「シンプルさ」にあります。StateやPropsが変更されると、コンポーネントは**再レンダリング（re-render）**されます。再レンダリングとは、コンポーネント関数が再実行され、新しいReact要素（仮想DOM）のツリーを生成するプロセスです。Reactは、この新しいツリーと前回のツリーを比較（差分検出、Reconciliation）し、変更があった部分だけを実際のDOMに適用します。</p>
<p>このモデルは非常に直感的で分かりやすい反面、パフォーマンス上の課題を内包しています。例えば、親コンポーネントが再レンダリングされると、<strong>propsが変更されていなくても、デフォルトではすべての子コンポーネントも再レンダリング</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><span 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></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-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useState</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// このコンポーネントはpropsを持たない
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ChildComponent</span> <span style="color:#f92672">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;ChildComponent is rendered&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">div</span>&gt;<span style="color:#a6e22e">I</span> <span style="color:#a6e22e">am</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">child</span>.&lt;/<span style="color:#f92672">div</span>&gt;;
</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">const</span> <span style="color:#a6e22e">ParentComponent</span> <span style="color:#f92672">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">count</span>, <span style="color:#a6e22e">setCount</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>(<span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">p</span>&gt;<span style="color:#a6e22e">Count</span><span style="color:#f92672">:</span> {<span style="color:#a6e22e">count</span>}&lt;/<span style="color:#f92672">p</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">onClick</span><span style="color:#f92672">=</span>{() =&gt; <span style="color:#a6e22e">setCount</span>(<span style="color:#a6e22e">c</span> =&gt; <span style="color:#a6e22e">c</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>)}&gt;<span style="color:#a6e22e">Increment</span>&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">ChildComponent</span> /&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</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>Increment</code>ボタンをクリックすると<code>ParentComponent</code>の<code>count</code>ステートが更新され、<code>ParentComponent</code>が再レンダリングされます。このとき、<code>ChildComponent</code>には何の変化もないにもかかわらず、コンソールには<code>ChildComponent is rendered</code>と表示され、再レンダリングが走っていることがわかります。</p>
<p>アプリケーションが小規模なうちは問題になりませんが、コンポーネントツリーが深くなり、各コンポーネントの処理が複雑になるにつれて、この不要な再レンダリングがパフォーマンスのボトルネックとなるのです。</p>
<h4 id="手動メモ化という職人芸とその限界">手動メモ化という「職人芸」とその限界</h4>
<p>この問題を解決するために、Reactは私たちに最適化のためのツールを提供しました。それが<code>React.memo</code>、<code>useMemo</code>、<code>useCallback</code>です。</p>
<ul>
<li><strong><code>React.memo</code>:</strong> コンポーネントをラップすることで、propsが変更された場合のみ再レンダリングされるようにする高階コンポーネント。</li>
<li><strong><code>useMemo</code>:</strong> 計算コストの高い処理の結果をメモ化（キャッシュ）し、依存する値が変更された場合のみ再計算するフック。</li>
<li><strong><code>useCallback</code>:</strong> 関数のインスタンスをメモ化し、依存する値が変更された場合のみ新しい関数を生成するフック。これは特に、<code>React.memo</code>でラップした子コンポーネントに関数をpropsとして渡す際に効果を発揮します。</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><span 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></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-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useState</span>, <span style="color:#a6e22e">useCallback</span>, <span style="color:#a6e22e">memo</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// React.memoでラップ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ChildComponent</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">memo</span>(({ <span style="color:#a6e22e">onButtonClick</span> }) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;ChildComponent is rendered&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">onClick</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">onButtonClick</span>}&gt;<span style="color:#a6e22e">Click</span> <span style="color:#a6e22e">me</span>&lt;/<span style="color:#f92672">button</span>&gt;;
</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">const</span> <span style="color:#a6e22e">ParentComponent</span> <span style="color:#f92672">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">count</span>, <span style="color:#a6e22e">setCount</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>(<span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">otherState</span>, <span style="color:#a6e22e">setOtherState</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>(<span style="color:#66d9ef">false</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// useCallbackで関数をメモ化しないと、ParentComponentが再レンダリングされるたびに
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// 新しいonButtonClick関数が生成され、ChildComponentのpropsが変更されたと見なされ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// React.memoの効果がなくなってしまう。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">handleButtonClick</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">useCallback</span>(() =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Button clicked!&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ここでcountを使う場合は依存配列に含める必要がある
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// console.log(count);
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  }, []); <span style="color:#75715e">// 依存配列が空なので、この関数は初回レンダリング時しか生成されない
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">p</span>&gt;<span style="color:#a6e22e">Count</span><span style="color:#f92672">:</span> {<span style="color:#a6e22e">count</span>}&lt;/<span style="color:#f92672">p</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">onClick</span><span style="color:#f92672">=</span>{() =&gt; <span style="color:#a6e22e">setCount</span>(<span style="color:#a6e22e">c</span> =&gt; <span style="color:#a6e22e">c</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>)}&gt;<span style="color:#a6e22e">Increment</span> <span style="color:#a6e22e">Count</span>&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">onClick</span><span style="color:#f92672">=</span>{() =&gt; <span style="color:#a6e22e">setOtherState</span>(<span style="color:#a6e22e">s</span> =&gt; <span style="color:#f92672">!</span><span style="color:#a6e22e">s</span>)}&gt;<span style="color:#a6e22e">Toggle</span> <span style="color:#a6e22e">Other</span> <span style="color:#a6e22e">State</span>&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">ChildComponent</span> <span style="color:#a6e22e">onButtonClick</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">handleButtonClick</span>} /&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</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>handleButtonClick</code>の中で<code>count</code>ステートを使いたくなったらどうでしょう？依存配列に<code>count</code>を追加しなければならず、そうなると<code>count</code>が更新されるたびに新しい関数が生成され、結局<code>ChildComponent</code>は再レンダリングされてしまいます。</p>
<p>このように、手動メモ化は非常に繊細で、エラーを起こしやすい作業です。</p>
<ul>
<li><strong>認知負荷の増大:</strong> 開発者は常にコンポーネントの依存関係と再レンダリングの連鎖を意識しなければなりません。</li>
<li><strong>バグの温床:</strong> 依存配列の指定ミスは、アプリケーションの挙動をおかしくする深刻なバグに直結します。</li>
<li><strong>コードの陳腐化:</strong> リファクタリングによって依存関係が変わった際に、メモ化のコードを更新し忘れることがよくあります。</li>
</ul>
<p>Reactチームは、この「Reactを正しく、かつ効率的に書くことの難しさ」を根本的な課題と捉えました。開発者が本来のビジネスロジックに集中できる環境を作ることこそが、Reactの生産性をさらに高める鍵であると考えたのです。その答えが、<strong>React Compiler</strong>でした。</p>
<h3 id="react-compilerの核心コンパイラがreactのコードを書き換える">React Compilerの核心：コンパイラがReactのコードを書き換える</h3>
<p>React Compiler（開発コードネーム: &ldquo;Forget&rdquo;）は、その名の通り、Reactのコードを解析し、<strong>自動的にメモ化を適用するコンパイラ</strong>です。これはライブラリやフックではなく、Babelプラグインとして提供され、ビルドプロセスに組み込まれます。</p>
<p>開発者は、これまで通り「素朴な」Reactコードを書くだけです。<code>useMemo</code>や<code>useCallback</code>のことは忘れて（&ldquo;Forget&quot;して）構いません。すると、コンパイラがビルド時にコードをスキャンし、どこをメモ化すべきかを判断し、最適な形でメモ化のコードを自動的に挿入してくれるのです。</p>
<h4 id="仕組みの概要静的解析とコード変換">仕組みの概要：静的解析とコード変換</h4>
<p>React Compilerは、どのようにしてこの魔法のような処理を実現しているのでしょうか？その核心は、<strong>高度な静的解析</strong>にあります。</p>
<ol>
<li>
<p><strong>コードの解析（Parsing）:</strong>
コンパイラはまず、コンポーネントのコードを解析し、その構造（どの変数がどこで定義され、どこで使われているか）を理解します。State、Props、フック、通常の変数や関数などをすべて識別し、それらの依存関係をグラフとして構築します。</p>
</li>
<li>
<p><strong>リアクティビティ分析（Reactivity Analysis）:</strong>
次に、コンパイラは「何が変更されたときに、どの部分が影響を受けるか」を分析します。これは、Reactのリアクティビティモデル（StateやPropsの変更がレンダリングをトリガーする仕組み）を深く理解しているからこそ可能です。</p>
<ul>
<li>この値はリアクティブか？（例: <code>useState</code>の返り値）</li>
<li>この関数はリアクティブな値に依存しているか？</li>
<li>このJSXブロックは、どの値が変更されたときに再計算が必要か？</li>
</ul>
</li>
<li>
<p><strong>自動メモ化（Auto-memoization）:</strong>
分析結果に基づいて、コンパイラはコード内の適切な箇所にメモ化のロジックを挿入します。これは、私たちが手で<code>useMemo</code>や<code>useCallback</code>を書くのと同じような最適化ですが、はるかに正確かつ網羅的です。コンパイラは、値がプリミティブ（数値、文字列など）かオブジェクトか、あるいは関数かを見極め、それぞれに最適なメモ化戦略を適用します。</p>
</li>
<li>
<p><strong>コード生成（Code Generation）:</strong>
最後に、コンパイラは最適化された新しいコードを生成します。このコードには、<code>useMemo</code>や<code>useCallback</code>に似た、しかしより低レベルで効率的なコンパイラ専用のキャッシュ機構が埋め込まれています。</p>
</li>
</ol>
<h4 id="コード変換の具体例">コード変換の具体例</h4>
<p>言葉だけでは分かりにくいので、コンパイラがどのようなコード変換を行うのか、具体的な例を見てみましょう。</p>
<p><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><span 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></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-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useState</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">UserProfile</span> <span style="color:#f92672">=</span> ({ <span style="color:#a6e22e">user</span> }) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">filter</span>, <span style="color:#a6e22e">setFilter</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>(<span style="color:#e6db74">&#39;&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// このフィルタリング処理は、user.postsが巨大な場合にコストがかかる可能性がある
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">filteredPosts</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">posts</span>.<span style="color:#a6e22e">filter</span>(<span style="color:#a6e22e">post</span> =&gt;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">title</span>.<span style="color:#a6e22e">toLowerCase</span>().<span style="color:#a6e22e">includes</span>(<span style="color:#a6e22e">filter</span>.<span style="color:#a6e22e">toLowerCase</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:#75715e">// この関数は、UserProfileが再レンダリングされるたびに再生成される
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">handleFilterChange</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">e</span>) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">setFilter</span>(<span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">target</span>.<span style="color:#a6e22e">value</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">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">input</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text&#34;</span> <span style="color:#a6e22e">value</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">filter</span>} <span style="color:#a6e22e">onChange</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">handleFilterChange</span>} /&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">h2</span>&gt;{<span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">name</span>}<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#a6e22e">s</span> <span style="color:#a6e22e">Posts</span>&lt;/<span style="color:#f92672">h2</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">filteredPosts</span>.<span style="color:#a6e22e">map</span>(<span style="color:#a6e22e">post</span> =&gt; (
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">li</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">id</span>}&gt;{<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">title</span>}&lt;/<span style="color:#f92672">li</span>&gt;
</span></span><span style="display:flex;"><span>        ))}
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</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>filter</code>を変更するたびに<code>UserProfile</code>が再レンダリグされ、<code>filteredPosts</code>の計算が走り、<code>handleFilterChange</code>関数も再生成されます。もし親コンポーネントから渡される<code>user</code>以外のpropsが変更された場合でも、これらの処理はすべて再実行されてしまいます。</p>
<p><strong>コンパイラによる変換後のコード（概念的なイメージ）:</strong></p>
<p>コンパイラは内部的に<code>useMemoCache</code>のようなAPIを使い、以下のようなコードを生成します（※これはあくまで概念を説明するための擬似コードであり、実際の出力とは異なります）。</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></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-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useState</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useMemoCache</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react/compiler-runtime&#39;</span>; <span style="color:#75715e">// コンパイラが内部的に使用するAPI
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">UserProfile</span> <span style="color:#f92672">=</span> ({ <span style="color:#a6e22e">user</span> }) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">cache</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">useMemoCache</span>(<span style="color:#ae81ff">3</span>); <span style="color:#75715e">// コンパイラが必要なキャッシュスロット数を計算
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">filter</span>, <span style="color:#a6e22e">setFilter</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>(<span style="color:#e6db74">&#39;&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// filteredPostsの計算をメモ化
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">filteredPosts</span> <span style="color:#f92672">=</span> (() =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 依存関係である user.posts と filter が変更されたかチェック
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">cache</span>[<span style="color:#ae81ff">0</span>] <span style="color:#f92672">!==</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">posts</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">cache</span>[<span style="color:#ae81ff">1</span>] <span style="color:#f92672">!==</span> <span style="color:#a6e22e">filter</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">result</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">posts</span>.<span style="color:#a6e22e">filter</span>(<span style="color:#a6e22e">post</span> =&gt;
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">title</span>.<span style="color:#a6e22e">toLowerCase</span>().<span style="color:#a6e22e">includes</span>(<span style="color:#a6e22e">filter</span>.<span style="color:#a6e22e">toLowerCase</span>())
</span></span><span style="display:flex;"><span>      );
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">cache</span>[<span style="color:#ae81ff">0</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">posts</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">cache</span>[<span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">filter</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">cache</span>[<span style="color:#ae81ff">2</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">result</span>; <span style="color:#75715e">// 結果をキャッシュ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cache</span>[<span style="color:#ae81ff">2</span>]; <span style="color:#75715e">// キャッシュされた値を返す
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  })();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// handleFilterChange関数をメモ化
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">handleFilterChange</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">e</span>) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">setFilter</span>(<span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">target</span>.<span style="color:#a6e22e">value</span>);
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// handleFilterChangeはリアクティブな値に依存しないため、
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// コンパイラはこれが不変であると判断し、メモ化の必要すらないかもしれない。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// もし依存があれば、useCallbackのようにメモ化される。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* ...JSX... */</span>}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</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>filteredPosts</code>の計算が<code>user.posts</code>と<code>filter</code>に依存していることをコンパイラが自動で検出し、これらの値が変更されない限りは再計算が行われないように最適化されています。開発者はこの複雑なキャッシュロジックを一切書く必要がありません。</p>
<h4 id="reactのルールの重要性">「Reactのルール」の重要性</h4>
<p>React Compilerが安全にコードを変換できる大前提として、<strong>私たちが「Reactのルール（Rules of React）」を守ってコードを書いていること</strong>が挙げられます。</p>
<ul>
<li><strong>Hooksはトップレベルで呼び出すこと。</strong> ループや条件分岐、ネストした関数の中で呼び出してはいけません。</li>
<li><strong>ReactコンポーネントとHooksは純粋であること。</strong> 同じ入力（props, state）に対しては常に同じ出力（JSX）を返す必要があります。レンダリング中に副作用（Stateの直接変更など）を起こしてはいけません。</li>
</ul>
<p>これらのルールは、コンパイラがコードの依存関係を静的に、つまりコードを実行することなく正確に予測するための生命線です。ルールが守られていないコードは、コンパイラの解析を妨げ、予期せぬ動作やエラーの原因となります。</p>
<p>幸い、私たちはすでに<code>eslint-plugin-react-hooks</code>という優れたリンターツールを持っており、これらのルールを強制することができます。React Compiler時代においては、このリンターの重要性がさらに増すことになるでしょう。</p>
<h3 id="メリットとデメリットreact開発はどう変わるのか">メリットとデメリット：React開発はどう変わるのか</h3>
<p>React Compilerの導入は、私たちの開発プロセスに多岐にわたる影響を与えます。</p>
<h4 id="メリット">メリット</h4>
<ol>
<li>
<p><strong>劇的な開発者体験（DX）の向上</strong>
最大のメリットは、何と言っても手動メモ化の煩わしさからの解放です。パフォーマンスチューニングという認知負荷の高い作業をコンパイラに任せることで、開発者はプロダクトの本質的な価値であるビジネスロジックの実装に集中できます。&ldquo;Just write React&rdquo; という、Reactが本来目指していた理想の開発スタイルに近づくことができます。</p>
</li>
<li>
<p><strong>最適なパフォーマンスの実現</strong>
人間による手動の最適化は、しばしば不完全です。最適化が不足している箇所を見逃したり、逆に不必要な箇所まで過剰にメモ化してしまったりします。コンパイラは、コードを網羅的に分析し、人間では見逃してしまうような細かな部分まで、一貫したルールに基づいて最適化を行います。これにより、手動で行うよりも優れた、あるいは少なくとも同等のパフォーマンスを安定して実現できます。</p>
</li>
<li>
<p><strong>コードの可読性と保守性の向上</strong>
<code>useMemo</code>や<code>useCallback</code>のラッパーがコードから消えることで、コンポーネントはよりシンプルで宣言的になります。ロジックが追いやすくなり、コードレビューの負担も軽減されます。将来のリファクタリング時にも、メモ化の依存関係を気にする必要がなくなり、保守性が大きく向上します。</p>
</li>
<li>
<p><strong>React学習コストの低減</strong>
React初学者がつまずきやすいポイントの一つが、<code>useMemo</code>や<code>useCallback</code>の概念と、依存配列の正しい使い方です。React Compilerが標準となれば、これらの高度な最適化手法を初期段階で学ぶ必要がなくなり、Reactの基本的な考え方（State、Props、コンポーネント）の習得に集中できるようになります。</p>
</li>
</ol>
<h4 id="デメリットと懸念点">デメリットと懸念点</h4>
<p>もちろん、良いことばかりではありません。新しい技術には必ずトレードオフや考慮すべき点が存在します。</p>
<ol>
<li>
<p><strong>コンパイラの「魔法」化</strong>
内部で何が起きているのかが分かりにくくなる、いわゆる「Magic」な部分が増える可能性があります。「なぜこのコンポーネントは再レンダリングされないのか？」あるいは「なぜここは期待通りに更新されないのか？」といった問題のデバッグが、これまで以上に難しくなるかもしれません。これに対しては、React DevToolsがCompilerによる最適化を可視化する機能を提供し、デバッグをサポートすることが期待されています。</p>
</li>
<li>
<p><strong>ビルド時間の増加</strong>
ビルドプロセスに静的解析とコード変換という新たなステップが加わるため、ビルド時間が長くなる可能性があります。大規模なプロジェクトでは、この影響が無視できないレベルになるかもしれません。ただし、これはコンパイラ自体の最適化や、インクリメンタルビルドの仕組みによって将来的には改善されていくでしょう。</p>
</li>
<li>
<p><strong>既存コードベースへの導入ハードル</strong>
長年運用されている巨大なコードベースでは、「Reactのルール」が守られていない箇所が散見されるかもしれません。Compilerを導入するためには、まずこれらのコードをリファクタリングする必要があります。コンパイラには、ファイル単位で有効/無効を切り替えるオプトイン/オプトアウトの仕組みが用意される予定であり、段階的な導入が可能になる見込みです。</p>
</li>
<li>
<p><strong>エコシステムとの互換性</strong>
React CompilerはBabelプラグインとして開発が進められていますが、近年のフロントエンド界隈ではesbuildやSWCといったRust製の高速なビルドツールが主流になりつつあります。これらのツールとReact Compilerがどのように統合されていくのかは、今後の大きな注目点です。</p>
</li>
</ol>
<h3 id="現場で使える実践的なtipsreact-compiler時代への備え">現場で使える実践的なTips：React Compiler時代への備え</h3>
<p>React Compilerはまだ実験的な段階ですが（InstagramのWebサイトなど、一部のMeta社製品では本番投入済み）、その登場はもはや時間の問題です。私たちは今から、来るべきCompiler時代に備えておくことができます。</p>
<h4 id="1-reactのルールを徹底する">1. &ldquo;Reactのルール&quot;を徹底する</h4>
<p>今すぐできる最も重要な準備は、あなたのコードベースで「Reactのルール」を徹底することです。</p>
<ul>
<li><strong><code>eslint-plugin-react-hooks</code>を導入・有効化する:</strong> もしまだ導入していないのであれば、今すぐ設定しましょう。<code>exhaustive-deps</code>ルールを含め、リンターの警告にはすべて対応する文化をチームに根付かせることが重要です。</li>
<li><strong>コンポーネントの純粋性を意識する:</strong> コンポーネントやフックの内部で、外部の状態を変更したり、APIリクエストを直接発行したりするような副作用を避けてください。副作用は<code>useEffect</code>やイベントハンドラ内に閉じ込めるのが基本です。</li>
</ul>
<p>これらの習慣は、Compilerの有無にかかわらず、Reactアプリケーションを健全に保つためのベストプラクティスです。</p>
<h4 id="2-メモ化apiへの考え方を変える">2. メモ化APIへの考え方を変える</h4>
<p>React Compilerが導入された後も、<code>useMemo</code>や<code>useCallback</code>が即座になくなるわけではありません。コンパイラがうまく最適化できない特殊なケースや、パフォーマンスクリティカルな部分で意図的に手動最適化を行いたい場合のために、これらのAPIは残り続けるでしょう。</p>
<p>しかし、私たちのマインドセットは変える必要があります。これからは、**「まずシンプルに書き、パフォーマンスの問題が実際に発生したら、プロファイラで計測した上で、最後の手段として手動メモ化を検討する」**というアプローチが基本になります。闇雲なメモ化はアンチパターンであるという認識を強く持つべきです。</p>
<h4 id="3-段階的な導入計画を立てる">3. 段階的な導入計画を立てる</h4>
<p>あなたのチームのプロジェクトにCompilerを導入する際は、いきなり全体に適用するのではなく、段階的に進めることをお勧めします。</p>
<ul>
<li><strong>新規プロジェクトから試す:</strong> これから始める新しいプロジェクトは、Compilerを導入する絶好の機会です。</li>
<li><strong>影響の少ない部分から適用する:</strong> 既存のプロジェクトでは、影響範囲の小さいコンポーネントや、ビジネスロジック的に重要度の低いページからオプトインで適用を始め、動作を確認しながら範囲を広げていくのが安全です。</li>
<li><strong>CIでリグレッションを検知する:</strong> Compilerによるコード変換が意図しない挙動の変化（リグレッション）を引き起こす可能性はゼロではありません。コンポーネントの見た目や動作をチェックするテスト（Visual Regression TestingやE2Eテスト）をCIに組み込み、安全性を確保しましょう。</li>
</ul>
<h3 id="まとめフロントエンド開発の新たな地平へ">まとめ：フロントエンド開発の新たな地平へ</h3>
<p>React Compilerは、単なるパフォーマンス改善ツールではありません。それは、Reactにおける開発のパラダイムを根底から変える可能性を秘めた、<strong>フロントエンド開発の革新</strong>です。</p>
<p>長年私たちを悩ませてきた手動メモ化の複雑さから解放されることで、Reactはより宣言的で、より直感的なフレームワークへと進化します。開発者はパフォーマンスの細部に気を取られることなく、ユーザーのための価値創造に集中できるようになります。</p>
<p>もちろん、コンパイラは万能の銀の弾丸ではなく、その導入には新たな学びや課題も伴うでしょう。しかし、それがもたらす生産性とコード品質の向上は、それらの課題を乗り越えるに値する大きな価値を持っています。</p>
<p>私たちは今、Reactの歴史における大きな転換点に立っています。<code>useMemo</code>と<code>useCallback</code>に別れを告げ、コンパイラと共に歩む新しいReact開発の時代。その幕開けは、もうすぐそこまで来ています。未来への準備を始めましょう。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>React</category>
      <category>React Compiler</category>
      <category>Frontend</category>
    </item>
    <item>
      <title>TypeScript 6.0の型システム：より堅牢に、より柔軟に</title>
      <link>https://www.ai2core.com/posts/2026-02-23-typescript-6/</link>
      <pubDate>Mon, 23 Feb 2026 08:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-23-typescript-6/</guid>
      <description>TypeScript 6.0で導入される新しい型演算子とパフォーマンス改善。</description>
      <content:encoded><![CDATA[<h1 id="typescript-60の型システムより堅牢により柔軟に">TypeScript 6.0の型システム：より堅牢に、より柔軟に</h1>
<h2 id="はじめに">はじめに</h2>
<p>TypeScriptは現代のWeb開発、特に大規模なフロントエンドおよびバックエンド開発において、なくてはならない存在となりました。その静的型付けシステムは、コードの品質とメンテナンス性を劇的に向上させ、開発者に大きな安心感を与えてくれます。しかし、プロジェクトが成長し、コードベースが複雑化するにつれて、私たちは新たな課題に直面します。</p>
<p>「複数の状態のうち、必ず一つだけを持つオブジェクトを型で表現したいが、冗長なユニオン型になってしまい、意図しないプロパティの混入を防ぎきれない…」
「複雑なデータ構造を扱う型定義を書いたら、エディタの補完が急に遅くなった。ビルド時間も無視できないほど長くなってしまった…」
「ネストした非同期処理の戻り値の型を正しく取り出すのが、いつも面倒だ…」</p>
<p>もしあなたがTypeScriptを使った開発で、このような悩みを一度でも抱いたことがあるなら、今回リリースされた<strong>TypeScript 6.0</strong>は、まさに待望のアップデートとなるでしょう。</p>
<p>TypeScript 6.0は、これまでのバージョンとは一線を画す、型システムの抜本的な進化を遂げています。本記事では、プロの技術ブロガーとして、TypeScript 6.0で導入された新しい型演算子と画期的なパフォーマンス改善に焦点を当て、それらが私たちの開発体験をどのように変えるのかを、具体的なコード例と共に徹底的に解説していきます。</p>
<h2 id="なぜtypescript-60が今重要なのか---進化の背景と課題">なぜTypeScript 6.0が今、重要なのか？ - 進化の背景と課題</h2>
<p>TypeScriptはリリース以来、JavaScriptエコシステムとの互換性を保ちながら、着実に型システムの表現力を高めてきました。Generics, Conditional Types, Mapped Typesといった機能は、多くの開発者が動的なJavaScriptの性質を静的な型で表現するための強力なツールとなりました。</p>
<p>しかし、その成功の裏で、型システムの限界も見え始めていました。特に以下の3つの課題は、多くの大規模プロジェクトで共通の悩みとなっていました。</p>
<ol>
<li>
<p><strong>表現力の限界と冗長性</strong>:
UIの状態管理やAPIのレスポンスなど、「このプロパティを持つとき、あのプロパティは持たない」といった排他的な関係を表現する場面は頻繁にあります。従来は、<code>{ type: 'A', a: string } | { type: 'B', b: number }</code> のようなTagged Union（判別可能なユニオン型）で対応していましたが、プロパティが増えると組み合わせが爆発的に増加し、型定義が非常に冗長になる問題がありました。また、オブジェクトのキーレベルで排他性を強制する直接的な方法は存在しませんでした。</p>
</li>
<li>
<p><strong>パフォーマンスの壁</strong>:
TypeScriptの型チェックは非常に高度な処理を行っています。特に、再帰的な型定義（例えば、JSONオブジェクトの型）や、複雑な条件分岐を持つConditional Typesを多用すると、型チェッカーの計算量が指数関数的に増加し、エディタのLanguage Serviceの応答性（入力補完やエラー表示）が悪化したり、<code>tsc</code>によるビルド時間が著しく増大したりする問題がありました。これは大規模なモノレポなどでは生産性に直結する深刻な問題です。</p>
</li>
<li>
<p><strong>非同期処理の型の煩雑さ</strong>:
モダンなJavaScriptでは非同期処理が基本です。<code>Promise</code>を扱うための<code>Awaited&lt;T&gt;</code>ユーティリティ型は便利ですが、<code>Promise&lt;Promise&lt;string&gt;&gt;</code>のようにネストした<code>Promise</code>の型を解決するには、<code>Awaited&lt;Awaited&lt;string&gt;&gt;</code>のように書く必要があり、直感的ではありませんでした。</p>
</li>
</ol>
<p>TypeScript 6.0は、これらの根深い課題に正面から向き合い、「より堅牢な型定義」と「より快適な開発体験」を両立させることを目指して設計された、記念碑的なアップデートなのです。</p>
<h2 id="typescript-60の目玉機能新型演算子で変わる型定義">TypeScript 6.0の目玉機能：新・型演算子で変わる型定義</h2>
<p>TypeScript 6.0の核心は、型システムの表現力を飛躍的に向上させる新しい演算子の導入です。ここでは、特にインパクトの大きい3つの新機能を紹介します。</p>
<h3 id="排他的プロパティを安全に扱う-exclusive-keyof">排他的プロパティを安全に扱う <code>exclusive keyof</code></h3>
<p>これまで、互いに排他的なプロパティを持つオブジェクトを定義するのは困難でした。例えば、イベントオブジェクトが<code>user</code>由来か<code>system</code>由来かで、持つIDが<code>userId</code>か<code>systemId</code>のどちらか一方だけになる、という型を考えてみましょう。</p>
<p><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><span 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></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-typescript" data-lang="typescript"><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">type</span> <span style="color:#a6e22e">UserEvent</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">userId</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">systemId?</span>: <span style="color:#66d9ef">never</span> };
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">SystemEvent</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">systemId</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">userId?</span>: <span style="color:#66d9ef">never</span> };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Event</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">UserEvent</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">SystemEvent</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// これはOK
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">userEvent</span>: <span style="color:#66d9ef">Event</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">userId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;user-123&#39;</span> };
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">systemEvent</span>: <span style="color:#66d9ef">Event</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">systemId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;sys-abc&#39;</span> };
</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><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">mixedEvent</span>: <span style="color:#66d9ef">Event</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">userId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;user-123&#39;</span>, <span style="color:#a6e22e">systemId</span>: <span style="color:#66d9ef">undefined</span> };
</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><span style="color:#75715e">// const invalidEvent: Event = { userId: &#39;user-123&#39;, systemId: &#39;sys-abc&#39; };
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>never</code>型を駆使して排他性を表現しようとしても、<code>undefined</code>の存在を許容してしまうため、完全な排他性を保証できませんでした。</p>
<p><strong>TypeScript 6.0の新構文 <code>exclusive keyof</code></strong></p>
<p>TypeScript 6.0では、Mapped Typesの構文が拡張され、<code>exclusive keyof</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></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-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// TypeScript 6.0: exclusive keyof を使った書き方
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Event</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  [<span style="color:#a6e22e">K</span> <span style="color:#66d9ef">in</span> <span style="color:#e6db74">&#39;userId&#39;</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">&#39;systemId&#39;</span> <span style="color:#a6e22e">exclusive</span> <span style="color:#66d9ef">keyof</span>]<span style="color:#f92672">:</span> <span style="color:#a6e22e">K</span> <span style="color:#66d9ef">extends</span> <span style="color:#e6db74">&#39;userId&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">?</span> { <span style="color:#a6e22e">userId</span>: <span style="color:#66d9ef">string</span> }
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">:</span> { <span style="color:#a6e22e">systemId</span>: <span style="color:#66d9ef">string</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:#75715e">// OK: どちらか一方のプロパティだけを持つ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">userEvent</span>: <span style="color:#66d9ef">Event</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">userId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;user-123&#39;</span> };
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">systemEvent</span>: <span style="color:#66d9ef">Event</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">systemId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;sys-abc&#39;</span> };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// エラー: Property &#39;systemId&#39; is not allowed.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// &#39;userId&#39; and &#39;systemId&#39; are mutually exclusive.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">invalidEvent</span>: <span style="color:#66d9ef">Event</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">userId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;user-123&#39;</span>, <span style="color:#a6e22e">systemId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;sys-abc&#39;</span> };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// エラー: Property &#39;systemId&#39; is not allowed.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">mixedEvent</span>: <span style="color:#66d9ef">Event</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">userId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;user-123&#39;</span>, <span style="color:#a6e22e">systemId</span>: <span style="color:#66d9ef">undefined</span> };
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>exclusive keyof</code> は、指定されたキー（この場合は <code>'userId' | 'systemId'</code>）のうち、<strong>ただ一つ</strong>だけがオブジェクトに存在することを保証します。これにより、冗長なユニオン型や<code>never</code>を使ったハックは不要になり、より直感的かつ安全に排他的なデータ構造をモデリングできるようになりました。</p>
<p>この機能は、状態管理ライブラリ（Redux, Zustand, XStateなど）のアクション定義や、多様なバリエーションを持つコンポーネントのProps定義など、多くの場面でコードの堅牢性を劇的に向上させるでしょう。</p>
<h3 id="再帰的な型定義のパフォーマンスを劇的に改善する-defer">再帰的な型定義のパフォーマンスを劇的に改善する <code>defer</code></h3>
<p>複雑なデータ構造、例えばJSONや抽象構文木（AST）などを型で表現しようとすると、再帰的な型定義が必要になります。</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></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-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// 再帰的なJSON型 (TypeScript 5.x以前)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">JsonValue</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> <span style="color:#66d9ef">number</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> <span style="color:#66d9ef">boolean</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> { [<span style="color:#a6e22e">key</span>: <span style="color:#66d9ef">string</span>]<span style="color:#f92672">:</span> <span style="color:#a6e22e">JsonValue</span> }
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> <span style="color:#a6e22e">JsonValue</span>[];
</span></span></code></pre></td></tr></table>
</div>
</div><p>この型定義は一見問題ないように見えますが、非常に深いネスト構造を持つオブジェクトに対して型推論を行おうとすると、TypeScriptコンパイラは再帰的に型を展開し続け、膨大な計算リソースを消費します。これが、エディタが固まったり、ビルドが遅延したりする原因でした。</p>
<p><strong>TypeScript 6.0の遅延評価 <code>defer</code> 演算子</strong></p>
<p>この問題を解決するため、TypeScript 6.0では<code>defer</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></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-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// TypeScript 6.0: defer を使ったパフォーマンス改善
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">JsonValue</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> <span style="color:#66d9ef">number</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> <span style="color:#66d9ef">boolean</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> { [<span style="color:#a6e22e">key</span>: <span style="color:#66d9ef">string</span>]<span style="color:#f92672">:</span> <span style="color:#a6e22e">defer</span> <span style="color:#a6e22e">JsonValue</span> } <span style="color:#75715e">// オブジェクトのプロパティを遅延評価
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#f92672">|</span> (<span style="color:#a6e22e">defer</span> <span style="color:#a6e22e">JsonValue</span>)[];             <span style="color:#75715e">// 配列の要素を遅延評価
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>defer</code> を再帰の境界に配置することで、TypeScriptコンパイラは型の全体像を一度に展開しなくなります。例えば、<code>{ &quot;a&quot;: { &quot;b&quot;: { ... } } }</code> のようなオブジェクトの型をチェックする際、まずは <code>{ &quot;a&quot;: defer JsonValue }</code> として型を解決します。そして、プロパティ <code>a</code> にアクセスするコードが書かれたとき、初めて <code>a</code> の型である <code>defer JsonValue</code> を展開して <code>JsonValue</code> の評価を開始します。</p>
<p>この「オンデマンドな型評価」により、これまでパフォーマンス上の理由で諦めていたような複雑で深いデータ構造に対しても、正確な型付けを、しかも快適な開発体験を維持したまま行えるようになります。</p>
<h3 id="非同期コードをシンプルにする-deepawaitedt">非同期コードをシンプルにする <code>DeepAwaited&lt;T&gt;</code></h3>
<p><code>Promise</code>を扱う上で、組み込みの<code>Awaited&lt;T&gt;</code>は便利ですが、ネストした<code>Promise</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></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-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// TypeScript 5.x以前
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">NestedPromise</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">Promise</span>&lt;<span style="color:#f92672">Promise</span><span style="color:#960050;background-color:#1e0010">&lt;</span>{ <span style="color:#a6e22e">name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#a6e22e">string</span> }&gt;&gt;<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Awaitedを何度も適用する必要があった
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Result1</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Awaited</span>&lt;<span style="color:#f92672">NestedPromise</span>&gt;; <span style="color:#75715e">// Promise&lt;Promise&lt;{ name: string }&gt;&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Result2</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Awaited</span>&lt;<span style="color:#f92672">Result1</span>&gt;; <span style="color:#75715e">// Promise&lt;{ name: string }&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Result3</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Awaited</span>&lt;<span style="color:#f92672">Result2</span>&gt;; <span style="color:#75715e">// { name: string }
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>TypeScript 6.0では、この問題を解決する新しい組み込みユーティリティ型<code>DeepAwaited&lt;T&gt;</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-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// TypeScript 6.0: DeepAwaited&lt;T&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">NestedPromise</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">Promise</span>&lt;<span style="color:#f92672">Promise</span><span style="color:#960050;background-color:#1e0010">&lt;</span>{ <span style="color:#a6e22e">name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#a6e22e">string</span> }&gt;&gt;<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 一度で再帰的にPromiseを解決してくれる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">FinalResult</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">DeepAwaited</span>&lt;<span style="color:#f92672">NestedPromise</span>&gt;; <span style="color:#75715e">// { name: string }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">fetchData</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">NestedPromise</span>&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">data</span>: <span style="color:#66d9ef">FinalResult</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetchData</span>();
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">name</span>); <span style="color:#75715e">// 型補完も完璧に機能する
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>DeepAwaited&lt;T&gt;</code>は、<code>T</code>が<code>Promise</code>でラップされている限り、再帰的にその中身の型を取り出します。これにより、複雑な非同期処理の連鎖や、gRPCのストリーミングレスポンスのように多層のラッパーが存在するようなケースでも、最終的に得られる値の型を一行で、かつ直感的に表現できるようになりました。</p>
<h2 id="ビルド時間を劇的に短縮するパフォーマンス改善">ビルド時間を劇的に短縮するパフォーマンス改善</h2>
<p>TypeScript 6.0の進化は型システムだけではありません。開発体験の根幹を支えるコンパイラのパフォーマンスも大幅に向上しています。</p>
<h3 id="project-wide-type-caching-もうビルドで待たされない">Project-Wide Type Caching: もうビルドで待たされない</h3>
<p><code>tsc --watch</code>や近年のビルドツールは、変更されたファイルのみを再コンパイルするインクリメンタルビルドをサポートしていますが、起動時の初回ビルドや、依存関係の深いファイルを変更した際のビルドには依然として時間がかかっていました。</p>
<p>TypeScript 6.0では、<strong>プロジェクトワイド型キャッシング</strong>という新しい仕組みが導入されました。これは、<code>.tsbuildinfo</code>ファイルをさらに進化させたもので、型チェックの結果をプロジェクト横断で、かつ永続的にキャッシュします。</p>
<p><strong>仕組みの図解</strong></p>
<pre tabindex="0"><code>&lt;&lt; TypeScript 5.x &gt;&gt;
[tsc実行] -&gt; [全ファイルの依存関係解析] -&gt; [全ファイルの型チェック] -&gt; [JS出力]

&lt;&lt; TypeScript 6.0 &gt;&gt;
[tsc実行] -&gt; [キャッシュ読み込み] -&gt; [変更されたファイルのみ依存関係再解析] -&gt; [影響範囲のみ型チェック] -&gt; [JS出力]
</code></pre><p>このキャッシュは、<code>node_modules</code>内のライブラリの型定義（<code>.d.ts</code>）ファイルの解析結果も含まれます。一度解析されたライブラリは、バージョンが変更されない限り、次回のビルドではキャッシュから瞬時に読み込まれます。</p>
<p><code>tsconfig.json</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></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 style="color:#75715e">// tsconfig.json
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;compilerOptions&#34;</span>: {
</span></span><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:#f92672">&#34;enableProjectWideCache&#34;</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;cacheLocation&#34;</span>: <span style="color:#e6db74">&#34;./.cache/typescript&#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>この改善により、特に大規模なモノレポ環境や、多くの外部ライブラリに依存するプロジェクトでのビルド時間が、初回ビルドで最大50%、2回目以降のビルドでは最大80%以上短縮されたという報告もあります。</p>
<h3 id="parallel-type-inference-マルチコアの力を最大限に">Parallel Type Inference: マルチコアの力を最大限に</h3>
<p>近年の開発用マシンはマルチコアCPUが標準です。しかし、従来のTypeScriptの型チェック処理は、主にシングルスレッドで実行されていました。</p>
<p>TypeScript 6.0では、コンパイラのアーキテクチャが見直され、型推論と型チェックのプロセスが並列化されました。コンパイラはプロジェクトのファイル依存関係グラフを解析し、互いに依存していないファイル群を複数のスレッドに割り当てて、同時に型チェックを実行します。</p>
<p>これにより、CPUのコア数に比例して型チェックの速度が向上します。特に、多数の独立したコンポーネントやモジュールを持つプロジェクトで絶大な効果を発揮し、ビルド時間の短縮はもちろん、エディタ上でのリアルタイムな型エラー検出の応答性も向上させ、よりスムーズなコーディング体験を実現します。</p>
<h2 id="メリットとデメリット">メリットとデメリット</h2>
<p>TypeScript 6.0は強力なアップデートですが、導入にあたっては以下の点を考慮する必要があります。</p>
<p><strong>メリット</strong></p>
<ul>
<li><strong>コードの堅牢性向上</strong>: <code>exclusive keyof</code>により、これまで実行時エラーの原因となり得た不正なデータ構造を、コンパイル時に完全に排除できます。</li>
<li><strong>開発体験の向上</strong>: <code>defer</code>演算子やコンパイラのパフォーマンス改善により、大規模プロジェクトでもストレスのない高速なフィードバックループが実現します。</li>
<li><strong>生産性の向上</strong>: <code>DeepAwaited&lt;T&gt;</code>のような便利なユーティリティ型により、ボイラープレートコードが削減され、開発者は本質的なロジックに集中できます。</li>
</ul>
<p><strong>デメリット / 注意点</strong></p>
<ul>
<li><strong>学習コスト</strong>: <code>exclusive keyof</code>や<code>defer</code>は新しい概念であり、その特性や適切な使用場面を理解するための学習が必要です。特に<code>defer</code>は型評価のタイミングという、これまであまり意識しなかった側面に踏み込むため、チーム内での知識共有が重要になります。</li>
<li><strong>エコシステムの追従</strong>: PrettierやESLintといった関連ツールが新しい構文に完全に対応するまで、少し時間がかかる可能性があります。導入前に主要なツールの対応状況を確認することをお勧めします。</li>
<li><strong>過剰な最適化への注意</strong>: <code>defer</code>は強力ですが、乱用するとかえってコードの可読性を損なう可能性があります。パフォーマンスが実際に問題となっている箇所に限定して使用するなど、計画的な導入が求められます。</li>
</ul>
<h2 id="現場で使える実践的なtips">現場で使える実践的なTips</h2>
<p>TypeScript 6.0を最大限に活用するための、いくつかの実践的なヒントを紹介します。</p>
<ol>
<li>
<p><strong>段階的な導入</strong>:
既存のプロジェクトに導入する際は、いきなりすべてのコードを書き換えるのではなく、まずは<code>tsconfig.json</code>の更新と、CIでの動作確認から始めましょう。新しい型演算子は、新規機能開発の箇所や、リファクタリングの対象となっている比較的小さなモジュールから適用していくのが安全です。</p>
</li>
<li>
<p><strong>CI/CDでのビルドキャッシュ活用</strong>:
<code>enableProjectWideCache</code>を有効にしたら、GitHub ActionsやCircleCIなどのCI環境でキャッシュ機能を設定しましょう。<code>cacheLocation</code>で指定したディレクトリをビルドジョブ間で共有することで、プルリクエストごとのCI実行時間を大幅に短縮できます。</p>
</li>
<li>
<p><strong><code>exclusive keyof</code> を状態管理のベストプラクティスに</strong>:
チーム内で「状態を表すオブジェクトは<code>exclusive keyof</code>を使って定義する」といったコーディング規約を設けることを検討してみてください。これにより、意図しない状態の混在を防ぎ、より予測可能で堅牢な状態管理を実現できます。</p>
</li>
<li>
<p><strong>パフォーマンスプロファイリング</strong>:
ビルドが遅いと感じたら、まずはTypeScriptに組み込まれたパフォーマンスプロファイリング機能 (<code>--diagnostics</code>や<code>--extendedDiagnostics</code>フラグ) を使ってボトルネックを特定しましょう。その上で、再帰が深くパフォーマンスに影響を与えていると判断された箇所に、<code>defer</code>の適用を検討するのが効果的です。</p>
</li>
</ol>
<h2 id="まとめ">まとめ</h2>
<p>TypeScript 6.0は、これまでのTypeScriptが築き上げてきた堅牢な型システムを、さらに一段階上のレベルへと引き上げる画期的なアップデートです。</p>
<ul>
<li><code>exclusive keyof</code>は、データモデリングにおける表現力を高め、<strong>より堅牢なコード</strong>を可能にします。</li>
<li><code>defer</code>と<code>DeepAwaited&lt;T&gt;</code>は、複雑な型定義や非同期処理の記述を簡素化し、<strong>より柔軟な思考</strong>をサポートします。</li>
<li>そして、プロジェクトワイド型キャッシングと並列型推論は、大規模化するプロジェクトにおいても<strong>快適な開発体験</strong>を保証します。</li>
</ul>
<p>TypeScript 6.0は、単なる機能追加の集合体ではありません。それは、現代の複雑なアプリケーション開発が直面する本質的な課題に対する、TypeScriptチームからの明確な回答です。この新しい武器を手に、私たちはより安全で、より効率的で、そして何より楽しい開発の世界へと踏み出すことができるでしょう。</p>
<p>さあ、今すぐ <code>npm install -D typescript@latest</code> を実行して、TypeScript 6.0の進化をあなたのプロジェクトで体感してみてください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>TypeScript</category>
      <category>JavaScript</category>
      <category>TypeSystem</category>
    </item>
    <item>
      <title>Rust言語がLinuxカーネル開発の標準に？現状と課題</title>
      <link>https://www.ai2core.com/posts/2026-02-19-rust-linux/</link>
      <pubDate>Thu, 19 Feb 2026 12:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-19-rust-linux/</guid>
      <description>LinuxカーネルへのRust導入が進む中でのメリットと現場の声。</description>
      <content:encoded><![CDATA[<p>はい、承知いたしました。プロの技術ブロガーとして、「Rust言語がLinuxカーネル開発の標準に？現状と課題」というテーマで、非常に読み応えのある高品質な技術ブログ記事を執筆します。以下が記事本文です。</p>
<hr>
<h2 id="rust言語がlinuxカーネル開発の標準に現状と課題">Rust言語がLinuxカーネル開発の標準に？現状と課題</h2>
<p>「Linuxカーネルの次の30年を支える言語は何か？」</p>
<p>もしあなたがシステムプログラミングやOSの動向に少しでも関心があるなら、この問いの答えとして「Rust」という名前を耳にする機会が急増しているはずです。30年以上にわたりC言語という“不動の王”が君臨してきたこの世界に、なぜ今、Rustという新しい言語が大きな注目を集めているのでしょうか。</p>
<p>「C言語で書かれた3000万行以上の巨大なコードベースに、本当に新しい言語を導入できるのか？」
「Rustのメモリ安全性は魅力的だが、パフォーマンスや既存コードとの連携はどうなっているのか？」
「これは一部の先進的な開発者のお遊びで、結局はC言語が使われ続けるのではないか？」</p>
<p>この記事は、そんな疑問を抱えるすべてのエンジニアに向けて執筆しました。本記事を読めば、LinuxカーネルにおけるRust導入の最新動向、その背景にある根深い課題、具体的なコードレベルでの違い、そして開発現場が直面しているリアルな課題までを体系的に理解することができます。これは単なる技術トレンドの話ではありません。私たちが日々利用しているコンピューティングの根幹を、より安全で堅牢なものへと進化させるための壮大な挑戦の物語です。</p>
<h3 id="なぜ今linuxカーネルにrustが必要なのか-c言語の栄光と限界">なぜ今、LinuxカーネルにRustが必要なのか？ C言語の栄光と限界</h3>
<p>Linuxカーネル開発の歴史は、C言語と共にありました。ハードウェアを直接制御できる低レベルな操作性、他の言語を圧倒する実行速度、そして豊富な開発者コミュニティ。C言語は、カーネルという複雑怪奇なソフトウェアを記述するための最適なツールとして、30年以上にわたりその地位を確立してきました。</p>
<p>しかし、その輝かしい歴史の裏で、開発者たちは長年一つの大きな問題と戦い続けてきました。それが<strong>メモリ安全性の問題</strong>です。</p>
<p>Googleの調査によれば、Chromeブラウザで見つかった深刻なセキュリティ脆弱性の約70%がメモリ安全性の問題に起因するものでした。Microsoftも同様に、自社製品の脆弱性の約70%が同じ原因であると報告しています。この傾向はLinuxカーネルも例外ではありません。</p>
<p>C言語では、プログラマがメモリの確保 (<code>malloc</code>) と解放 (<code>free</code>) を手動で管理する必要があります。この自由度の高さがパフォーマンスの源泉である一方、以下のような典型的なバグ、すなわちセキュリティ脆弱性の温床となります。</p>
<ul>
<li><strong>バッファオーバーフロー:</strong> 配列の境界を超えてデータを書き込んでしまう問題。意図しないコードを実行される可能性があります。</li>
<li><strong>Use-after-free:</strong> 解放済みのメモリ領域にアクセスしてしまう問題。予測不能な動作や情報漏洩につながります。</li>
<li><strong>ダングリングポインタ:</strong> 解放されたメモリ領域を指し続けるポインタ。Use-after-freeの原因となります。</li>
<li><strong>データ競合:</strong> 複数のスレッドが同時に同じメモリ領域にアクセスし、少なくとも一つのスレッドが書き込みを行うことで発生する問題。</li>
</ul>
<p>これらの問題は、コードレビューや静的解析ツール、ファジングなど、様々な手法で検出しようと試みられてきましたが、完全に防ぐことは極めて困難です。どんなに経験豊富な開発者であっても、人間である以上ミスを犯します。そして、カーネルにおけるたった一つのメモリ関連バグが、システム全体を危険に晒す致命的な脆弱性となり得るのです。</p>
<p>この根深い問題を解決するため、Linuxカーネルコミュニティは新しいアプローチを模索し始めました。そして、その最有力候補として白羽の矢が立ったのが、Rustだったのです。</p>
<h3 id="rust-for-linux-カーネル開発の新時代">Rust for Linux: カーネル開発の新時代</h3>
<p>Rustは、C/C++に匹敵するパフォーマンスと、モダンな高水準言語の安全性を両立させることを目指して設計された言語です。その最大の特徴は、**「所有権」「借用」「ライフタイム」**という独自の仕組みによって、コンパイル時にメモリ安全性を保証する点にあります。</p>
<ul>
<li><strong>所有権 (Ownership):</strong> 全てのデータには「所有者」となる変数がただ一つだけ存在する。所有者がスコープを抜けると、データは自動的に解放される。これにより、メモリの二重解放や解放忘れが原理的に発生しません。</li>
<li><strong>借用 (Borrowing):</strong> データの所有権を移動させずに、そのデータへの参照（ポインタのようなもの）を貸し出すことができる。借用には、不変参照（<code>&amp;T</code>、複数可）と可変参照（<code>&amp;mut T</code>、一つだけ）のルールがあり、データ競合を防ぎます。</li>
<li><strong>ライフタイム (Lifetime):</strong> コンパイラが全ての参照の有効期間を追跡し、ダングリングポインタ（無効なメモリを指す参照）が存在しないことを保証します。</li>
</ul>
<p>これらの仕組みにより、**「コンパイルが通れば、メモリ関連のバグの大部分は存在しない」**という驚異的な安全性を実現します。これが、Linuxカーネル開発者たちがRustに強く惹かれた理由です。</p>
<h4 id="プロジェクトの歩みと現状">プロジェクトの歩みと現状</h4>
<p>「Rust for Linux」プロジェクトは、2020年頃から本格的な議論が始まり、多くの実験と議論を経て、2022年10月、ついに歴史的な瞬間を迎えます。Linus Torvalds氏自身がRustサポートの初期コードをメインラインカーネルにマージし、<strong>Linux 6.1</strong>から正式にカーネル内でのRust利用が（実験的機能として）可能になりました。</p>
<p>現在、Rustは主に新しいデバイスドライバやサブシステムの実装に利用されています。既存のCコードをRustで書き換えるのではなく、新規開発部分でRustを採用し、そのメリットを享受しようという現実的なアプローチが取られています。</p>
<p>具体的な導入例としては、以下のようなものがあります。</p>
<ul>
<li><strong>Android Binder:</strong> AndroidのIPC（プロセス間通信）メカニズムであるBinderのRust実装が進んでいます。</li>
<li><strong>Asahi Linux:</strong> Apple Silicon (M1/M2) 搭載MacでLinuxを動作させるプロジェクトで、GPUドライバの一部がRustで書かれています。</li>
<li><strong>ファイルシステム:</strong> <code>NTFS3</code>ドライバの一部機能や、新しいファイルシステムの実装実験など。</li>
</ul>
<h3 id="具体的なコードで見るcとrustの違い">具体的なコードで見るCとRustの違い</h3>
<p>百聞は一見にしかず。簡単なカーネルモジュールをC言語とRustで比較してみましょう。ここでは、モジュールロード時にメッセージを、アンロード時に別のメッセージをカーネルログに出力するだけのシンプルな例を見ていきます。</p>
<h4 id="c言語での実装-hello_cc">C言語での実装 (<code>hello_c.c</code>)</h4>
<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></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-c" data-lang="c"><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;linux/init.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;linux/module.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;linux/kernel.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">MODULE_LICENSE</span>(<span style="color:#e6db74">&#34;GPL&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">MODULE_AUTHOR</span>(<span style="color:#e6db74">&#34;Your Name&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">MODULE_DESCRIPTION</span>(<span style="color:#e6db74">&#34;A simple C kernel module.&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">MODULE_VERSION</span>(<span style="color:#e6db74">&#34;0.1&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">static</span> <span style="color:#66d9ef">int</span> __init <span style="color:#a6e22e">hello_c_init</span>(<span style="color:#66d9ef">void</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">printk</span>(KERN_INFO <span style="color:#e6db74">&#34;Hello, C world! Module loaded.</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</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">static</span> <span style="color:#66d9ef">void</span> __exit <span style="color:#a6e22e">hello_c_exit</span>(<span style="color:#66d9ef">void</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">printk</span>(KERN_INFO <span style="color:#e6db74">&#34;Goodbye, C world! Module unloaded.</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</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:#a6e22e">module_init</span>(hello_c_init);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">module_exit</span>(hello_c_exit);
</span></span></code></pre></td></tr></table>
</div>
</div><p>C言語でのカーネルモジュール開発者にはおなじみのコードです。<code>module_init</code>と<code>module_exit</code>マクロを使って初期化関数と終了関数を登録します。</p>
<h4 id="rustでの実装-hello_rustrs">Rustでの実装 (<code>hello_rust.rs</code>)</h4>
<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></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-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#![no_std]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#![feature(alloc_error_handler)]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> kernel::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>module! {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">type</span>: <span style="color:#a6e22e">HelloRust</span>,
</span></span><span style="display:flex;"><span>    name: <span style="color:#a6e22e">b</span><span style="color:#e6db74">&#34;hello_rust&#34;</span>,
</span></span><span style="display:flex;"><span>    author: <span style="color:#a6e22e">b</span><span style="color:#e6db74">&#34;Your Name&#34;</span>,
</span></span><span style="display:flex;"><span>    description: <span style="color:#a6e22e">b</span><span style="color:#e6db74">&#34;A simple Rust kernel module.&#34;</span>,
</span></span><span style="display:flex;"><span>    license: <span style="color:#a6e22e">b</span><span style="color:#e6db74">&#34;GPL&#34;</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">struct</span> <span style="color:#a6e22e">HelloRust</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> KernelModule <span style="color:#66d9ef">for</span> HelloRust {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">init</span>(_module: <span style="color:#66d9ef">&amp;</span>&#39;static <span style="color:#a6e22e">ThisModule</span>) -&gt; Result<span style="color:#f92672">&lt;</span>Self<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        pr_info!(<span style="color:#e6db74">&#34;Hello, Rust world! Module loaded.</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>);
</span></span><span style="display:flex;"><span>        Ok(HelloRust)
</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">impl</span> Drop <span style="color:#66d9ef">for</span> HelloRust {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">drop</span>(<span style="color:#f92672">&amp;</span><span style="color:#66d9ef">mut</span> self) {
</span></span><span style="display:flex;"><span>        pr_info!(<span style="color:#e6db74">&#34;Goodbye, Rust world! Module unloaded.</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#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>一見して、Rust版の方がより構造化されていることが分かります。</p>
<ul>
<li><code>module!</code>マクロ: モジュールのメタデータを宣言的に記述します。</li>
<li><code>KernelModule</code>トレイト: モジュールの振る舞い（ここでは<code>init</code>関数）を定義します。トレイトは他の言語におけるインターフェースに似た概念です。</li>
<li><code>struct HelloRust</code>と<code>impl Drop</code>: モジュールの状態を保持する構造体を作成し、<code>Drop</code>トレイトを実装することで、モジュールがアンロードされる際のクリーンアップ処理（C言語の<code>exit</code>関数に相当）を記述します。<code>Drop</code>はRustのデストラクタであり、リソースの解放が自動的かつ確実に行われることを保証する強力な機能です。</li>
</ul>
<p>この例だけでも、Rustがより高いレベルの抽象化を提供し、定型的なコードを減らし、リソース管理を安全に行うための仕組みを備えていることが見て取れます。</p>
<h4 id="cとrustの連携">CとRustの連携</h4>
<p>現実的には、RustコードがCの関数を呼び出したり、Cのデータ構造にアクセスしたりする場面が頻繁に発生します。この連携は<strong>FFI (Foreign Function Interface)</strong> と呼ばれ、<code>bindgen</code>というツールが重要な役割を果たします。</p>
<p><code>bindgen</code>は、Cのヘッダファイル（<code>.h</code>）を解析し、Rustから安全に呼び出せるようにするためのRustコード（バインディング）を自動生成します。</p>
<p><em>（図解イメージ: 左にCのコード (linux/version.hなど)、中央に<code>bindgen</code>、右に生成されたRustコード (bindings.rs) があり、RustコードがCの関数を呼び出す矢印が描かれている図）</em></p>
<p>この連携は強力ですが、同時に課題も生み出します。Cの関数を呼び出す部分は<code>unsafe</code>ブロックで囲む必要があり、Rustの安全神話が及ばない領域となります。<code>unsafe</code>ブロックの利用を最小限に留め、その境界をいかに安全に設計するかが、Rust for Linuxにおける重要な設計課題の一つです。</p>
<h3 id="メリットとデメリット理想と現実の狭間で">メリットとデメリット：理想と現実の狭間で</h3>
<p>Rustの導入は銀の弾丸ではありません。多くのメリットをもたらす一方で、乗り越えるべき課題も山積しています。</p>
<h4 id="メリット">メリット</h4>
<ol>
<li><strong>圧倒的なメモリ安全性:</strong> これが最大の動機です。バッファオーバーフローやUse-after-freeといった脆弱性のクラスをコンパイル時に排除できることは、カーネルのセキュリティを劇的に向上させる可能性を秘めています。</li>
<li><strong>モダンで表現力豊かな言語機能:</strong> エラー処理（<code>Result</code>/<code>Option</code>）、強力な型システム、トレイトによる抽象化、Cargoによるパッケージ管理（カーネル内では限定的）など、開発者の生産性とコードの品質を向上させる機能が豊富です。</li>
<li><strong>データ競合の防止:</strong> Rustの所有権システムは、複数のスレッドが安全にデータへアクセスするためのルールをコンパイル時に強制します。これにより、マルチコア環境で頻発する厄介なデータ競合バグを未然に防ぐことができます。</li>
<li><strong>新規開発者の参入促進:</strong> C言語に比べて学習しやすいモダンなRustは、若い世代のエンジニアをカーネル開発コミュニティに引きつける魅力的な要素となり得ます。</li>
</ol>
<h4 id="デメリットと課題">デメリットと課題</h4>
<ol>
<li><strong>Cとの相互運用性の複雑さ:</strong> <code>unsafe</code>コードの管理は依然として課題です。特に、C言語で多用される複雑なマクロやインラインアsemブリーをRustから扱うのは困難が伴います。ポインタの所有権がCとRustの間を行き来する際の管理は、細心の注意が必要です。</li>
<li><strong>ツールチェインへの依存:</strong> Linuxカーネルは特定のバージョンのGCCでビルドすることが推奨されていますが、Rustを導入すると<code>rustc</code>コンパイラと<code>LLVM</code>への依存が加わります。カーネルの安定性を保証するためには、これらのツールチェインのバージョン管理と安定性確保が新たな課題となります。</li>
<li><strong><code>#![no_std]</code>環境の制約:</strong> カーネル内ではOSの機能に依存する標準ライブラリ (<code>std</code>) が使えません。<code>alloc</code>クレートや<code>core</code>クレートのみに依存する<code>#![no_std]</code>環境でのプログラミングは、独特の難しさがあります。</li>
<li><strong>学習コストとコミュニティの文化:</strong> 30年以上C言語で開発してきたベテラン開発者にとって、所有権モデルをはじめとするRustのパラダイムは全く新しいものです。学習コストは決して低くなく、コードレビューの文化もこれから醸成していく必要があります。</li>
<li><strong>既存コードベースの存在:</strong> 3000万行を超えるCのコードをRustで書き換えるのは非現実的です。当面は新規開発部分に限定されるため、CとRustが混在するハイブリッドな状態が長く続くことになり、全体の複雑性はむしろ増大する可能性があります。</li>
</ol>
<h3 id="現場で使える実践的なtipsrust-for-linuxを試してみる">現場で使える実践的なTips：Rust for Linuxを試してみる</h3>
<p>このエキサイティングな変化を、ぜひ自分の手で体験してみてください。以下に、Rustでカーネルモジュールをビルドするための環境構築と簡単な手順を紹介します。</p>
<h4 id="1-開発環境の準備">1. 開発環境の準備</h4>
<p>Rust for Linuxをビルドするには、特定のバージョンの<code>rustc</code>、<code>cargo</code>、<code>bindgen</code>、そして<code>clang</code>/<code>LLVM</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></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. Linuxカーネルソースを取得 (ここでは例として6.6を使用)</span>
</span></span><span style="display:flex;"><span>git clone --depth <span style="color:#ae81ff">1</span> --branch v6.6 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
</span></span><span style="display:flex;"><span>cd linux
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2. Rustツールチェインのセットアップ</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># カーネルが必要とするバージョンのRustをインストールする</span>
</span></span><span style="display:flex;"><span>./scripts/rust/setup_toolchain.sh
</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>source ./rust-toolchain/env
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 4. 必要なLLVMツールをインストール (ディストリビューションによる)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Debian/Ubuntuの場合</span>
</span></span><span style="display:flex;"><span>sudo apt-get install llvm clang lld
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 5. カーネルコンフィグの準備</span>
</span></span><span style="display:flex;"><span>make defconfig
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="2-カーネルコンフィグでrustを有効化">2. カーネルコンフィグでRustを有効化</h4>
<p><code>.config</code>ファイルでRustサポートを有効にする必要があります。<code>make menuconfig</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># menuconfigを起動</span>
</span></span><span style="display:flex;"><span>make LLVM<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> menuconfig
</span></span></code></pre></td></tr></table>
</div>
</div><p>メニュー内で、<code>General setup ---&gt;</code> に移動し、<code>Rust support</code> を有効（<code>[*]</code>) にします。さらに、<code>Compile the kernel with warnings treated as errors</code> のチェックを外しておくと、初期のビルドが通りやすくなります。</p>
<p>コンフィグファイルに以下が設定されていることを確認してください。</p>
<pre tabindex="0"><code>CONFIG_RUST=y
</code></pre><h4 id="3-サンプルのビルド">3. サンプルのビルド</h4>
<p>カーネルにはRustのサンプルモジュールが含まれています。これをビルドしてみましょう。</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># サンプルモジュールを有効にする</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;CONFIG_SAMPLES_RUST=m&#34;</span> &gt;&gt; .config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># カーネルとモジュールをビルド (CPUコア数に合わせて-jオプションを調整)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># LLVM=1 をつけるのが重要</span>
</span></span><span style="display:flex;"><span>make LLVM<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> -j<span style="color:#66d9ef">$(</span>nproc<span style="color:#66d9ef">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ビルドが成功すると、<code>samples/rust/</code> ディレクトリ以下に <code>rust_minimal.ko</code> や <code>rust_print.ko</code> といったカーネルモジュール（<code>.ko</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></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>ls samples/rust/*.ko
</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>sudo insmod samples/rust/rust_minimal.ko
</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>dmesg | tail
</span></span><span style="display:flex;"><span><span style="color:#75715e"># [timestamp] rust_minimal: Rust minimal sample (init)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># [timestamp] rust_minimal: A minimal module written in Rust</span>
</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>sudo rmmod rust_minimal
</span></span><span style="display:flex;"><span>dmesg | tail
</span></span><span style="display:flex;"><span><span style="color:#75715e"># [timestamp] rust_minimal: Goodbye, world!</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これで、あなたのLinuxカーネルでRustコードが動きました！</p>
<h3 id="まとめrustはlinuxカーネルの未来を変えるか">まとめ：RustはLinuxカーネルの未来を変えるか？</h3>
<p>RustのLinuxカーネルへの導入は、単なる「新しい言語の追加」以上の、歴史的な意味を持つ大きな一歩です。それは、ソフトウェア工学における数十年来の課題であった「メモリ安全性」に、言語機能そのもので正面から立ち向かうという、カーネル開発の哲学におけるパラダイムシフトの始まりと言えるでしょう。</p>
<p>現時点では、RustがC言語を完全に置き換えて「標準」になると断言するのは時期尚早です。CとRustのハイブリッド開発という長い移行期間が続くでしょうし、克服すべき技術的・文化的課題も数多く存在します。</p>
<p>しかし、その方向性は明確です。新規のドライバや独立したサブシステムからRustの採用は着実に広がり、成功事例が積み重なることで、その影響力は徐々にカーネルのコア部分へと及んでいく可能性があります。</p>
<p>私たちは今、Linuxカーネルの次の30年を形作るかもしれない、重要な変化の時代にいます。この動きは、OS開発だけでなく、組み込みシステムやHPCなど、パフォーマンスと安全性が極めて重要となる全ての領域に影響を与えるでしょう。</p>
<p>一人のエンジニアとして、この歴史的な変化の目撃者となり、その動向を追いかけ、可能であれば貢献してみることは、非常にエキサイティングな体験になるはずです。ぜひ、あなたの手で<code>make LLVM=1</code>を叩き、カーネル開発の新しい扉を開いてみてください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Rust</category>
      <category>Linux</category>
      <category>Kernel</category>
    </item>
  </channel>
</rss>
