<?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>Troubleshooting on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/troubleshooting/</link>
    <description>Recent content in Troubleshooting on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Thu, 05 Mar 2026 09:12:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/troubleshooting/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>PostgreSQL接続プール枯渇の実戦対処：再発防止までつなげる調査・改善プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-05-postgresql-connection-pool-exhaustion-playbook/</link>
      <pubDate>Thu, 05 Mar 2026 09:12:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-05-postgresql-connection-pool-exhaustion-playbook/</guid>
      <description>本番で頻発するPostgreSQL接続枯渇を、発生時の初動から原因切り分け、設定改善、監視強化まで具体手順で解説。</description>
      <content:encoded><![CDATA[<h1 id="postgresql接続プール枯渇の実戦対処再発防止までつなげる調査改善プレイブック">PostgreSQL接続プール枯渇の実戦対処：再発防止までつなげる調査・改善プレイブック</h1>
<p>本番障害でよくあるのが、<code>too many clients already</code> や <code>remaining connection slots are reserved</code> です。アプリ側から見ると「急にDBに繋がらない」、ユーザー側から見ると「全機能が遅い・失敗する」という最悪の体験になります。</p>
<p>厄介なのは、接続枯渇が「DBサーバー性能不足」だけで起こるわけではない点です。リーク、タイムアウト設定、長時間トランザクション、プールサイズ不整合など、複数要因が重なって起きます。</p>
<p>この記事では、接続枯渇に対して <strong>発生時の初動 → 根本原因の特定 → 恒久対策</strong> の順で、手順を実務レベルでまとめます。</p>
<h2 id="1-まず初動サービス継続を優先する">1. まず初動：サービス継続を優先する</h2>
<p>障害対応では、完璧な原因究明より「止血」が先です。以下を順番に実施します。</p>
<ol>
<li>直近リリース有無を確認（機能フラグ含む）</li>
<li>アプリの接続数・待機数・エラー率を確認</li>
<li>DB側で <code>pg_stat_activity</code> を取得</li>
<li>長時間実行クエリを必要に応じて停止</li>
<li>一時的にアプリ Pod 数を制限して雪だるま増幅を止める</li>
</ol>
<p><code>pg_stat_activity</code> の基本クエリ:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  pid,
</span></span><span style="display:flex;"><span>  usename,
</span></span><span style="display:flex;"><span>  application_name,
</span></span><span style="display:flex;"><span>  client_addr,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">state</span>,
</span></span><span style="display:flex;"><span>  wait_event_type,
</span></span><span style="display:flex;"><span>  wait_event,
</span></span><span style="display:flex;"><span>  now() <span style="color:#f92672">-</span> query_start <span style="color:#66d9ef">AS</span> query_duration,
</span></span><span style="display:flex;"><span>  now() <span style="color:#f92672">-</span> xact_start  <span style="color:#66d9ef">AS</span> xact_duration,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">left</span>(query, <span style="color:#ae81ff">120</span>) <span style="color:#66d9ef">AS</span> query_head
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_activity
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> datname <span style="color:#f92672">=</span> current_database()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> xact_start NULLS <span style="color:#66d9ef">LAST</span>, query_start NULLS <span style="color:#66d9ef">LAST</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここで見るべきは、<code>state='idle in transaction'</code> と異常に長い <code>xact_duration</code> です。これがあるとコネクションを握ったまま解放されず、枯渇の引き金になります。</p>
<h2 id="2-典型原因を4パターンで切り分ける">2. 典型原因を4パターンで切り分ける</h2>
<h3 id="パターンa-アプリ接続プールサイズが過大">パターンA: アプリ接続プールサイズが過大</h3>
<p>よくあるのが、以下のような構成です。</p>
<ul>
<li>Pod 20個</li>
<li>各Podのプール max 20</li>
<li>理論最大接続 400</li>
<li>PostgreSQL <code>max_connections=300</code></li>
</ul>
<p>この時点で設計破綻です。さらに管理接続やメンテ接続を引くと余裕ゼロになります。</p>
<p><strong>対策:</strong></p>
<ul>
<li>プールサイズ設計は「全インスタンス合計」で管理</li>
<li><code>max_connections</code> の70〜80%以内に通常運用を収める</li>
<li>ピーク時はワーカ数/Pod数で制御</li>
</ul>
<h3 id="パターンb-コネクションリーク">パターンB: コネクションリーク</h3>
<p><code>finally</code> で close していない、ORMセッションの寿命が長い、例外時に返却されない、といった実装ミスです。</p>
<p>Python（SQLAlchemy）の悪い例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>session <span style="color:#f92672">=</span> SessionLocal()
</span></span><span style="display:flex;"><span>user <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>query(User)<span style="color:#f92672">.</span>filter(User<span style="color:#f92672">.</span>id <span style="color:#f92672">==</span> user_id)<span style="color:#f92672">.</span>first()
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 例外時にcloseされない可能性</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> user
</span></span></code></pre></td></tr></table>
</div>
</div><p>改善例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> contextlib <span style="color:#f92672">import</span> contextmanager
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@contextmanager</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_session</span>():
</span></span><span style="display:flex;"><span>    session <span style="color:#f92672">=</span> SessionLocal()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">yield</span> session
</span></span><span style="display:flex;"><span>        session<span style="color:#f92672">.</span>commit()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span>:
</span></span><span style="display:flex;"><span>        session<span style="color:#f92672">.</span>rollback()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">finally</span>:
</span></span><span style="display:flex;"><span>        session<span style="color:#f92672">.</span>close()
</span></span></code></pre></td></tr></table>
</div>
</div><p>FastAPI なら dependency でセッションスコープを統一し、endpointごとに確実に返却させるのが安全です。</p>
<h3 id="パターンc-長時間トランザクション">パターンC: 長時間トランザクション</h3>
<p>バッチ処理で 1トランザクションに大量更新を詰め込むと、ロック競合と接続占有が同時発生します。</p>
<p><strong>対策:</strong></p>
<ul>
<li>バッチをチャンク分割（例: 500件単位）</li>
<li><code>statement_timeout</code>、<code>idle_in_transaction_session_timeout</code> を設定</li>
<li>長時間処理はキュー化して非同期実行</li>
</ul>
<h3 id="パターンd-プールとdbのタイムアウト不一致">パターンD: プールとDBのタイムアウト不一致</h3>
<p>アプリの接続再利用時間が長すぎると、DB側で切断済み接続を使って失敗し、リトライで更に接続圧を上げます。</p>
<p><strong>対策:</strong></p>
<ul>
<li>プールの <code>maxLifetime</code> を DB/NLB timeout より短く</li>
<li>接続取得待ち時間（acquire timeout）を短くし、早めに失敗させる</li>
<li>失敗時リトライは指数バックオフ + ジッター</li>
</ul>
<h2 id="3-具体的な設定例pgbouncer--postgresql">3. 具体的な設定例（PgBouncer + PostgreSQL）</h2>
<p>高トラフィック環境では、アプリ直結より PgBouncer を挟むのが安定します。</p>
<p><code>pgbouncer.ini</code> の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[pgbouncer]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen_addr</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">0.0.0.0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen_port</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">6432</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">auth_type</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">md5</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pool_mode</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">transaction</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">max_client_conn</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">5000</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">default_pool_size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">80</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">reserve_pool_size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">20</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server_reset_query</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">DISCARD ALL</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server_idle_timeout</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">30</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>PostgreSQL 側の最小設定例:</p>
<pre tabindex="0"><code class="language-conf" data-lang="conf">max_connections = 300
shared_buffers = 4GB
idle_in_transaction_session_timeout = 60000
statement_timeout = 30000
log_min_duration_statement = 1000
</code></pre><p><code>pool_mode=transaction</code> は接続効率が高い一方、セッション依存機能（一時テーブルや session variable）の扱いに注意が必要です。導入前に該当クエリを洗い出してください。</p>
<h2 id="4-kubernetes運用での落とし穴">4. Kubernetes運用での落とし穴</h2>
<p>Kubernetes では、HPA がスケールアウトすると同時に接続数が急増しやすいです。</p>
<h3 id="41-設計式を最初に決める">4.1 設計式を最初に決める</h3>
<p><code>最大接続見積もり = maxPods × perPodPoolMax + 管理余白</code></p>
<p>例:</p>
<ul>
<li>maxPods=30</li>
<li>perPodPoolMax=8</li>
<li>管理余白=30</li>
<li>合計 270 → <code>max_connections=320</code> なら許容</li>
</ul>
<p>この計算を IaC へコメントで残しておくと、後任が壊しにくくなります。</p>
<h3 id="42-readiness-に-db接続必須チェックを入れすぎない">4.2 readiness に DB接続必須チェックを入れすぎない</h3>
<p>Pod起動時に全Podが同時にDBへ接続テストすると、再起動時にスパイクが起きます。readiness は軽量化し、重い初期化はバックグラウンドへ逃がすのが無難です。</p>
<h2 id="5-監視項目再発防止の最低ライン">5. 監視項目：再発防止の最低ライン</h2>
<p>以下を可視化し、閾値アラートを設定します。</p>
<ul>
<li><code>numbackends / max_connections</code></li>
<li>接続待ち時間（アプリメトリクス）</li>
<li><code>idle in transaction</code> セッション数</li>
<li>95/99パーセンタイルのクエリ時間</li>
<li>DBエラー率（connection refused, timeout）</li>
</ul>
<p>推奨アラート例:</p>
<ul>
<li><code>numbackends &gt; 85%</code> が 5分継続で Warning</li>
<li><code>numbackends &gt; 92%</code> が 2分継続で Critical</li>
<li><code>idle in transaction &gt; 10</code> が 3分継続で調査開始</li>
</ul>
<h2 id="6-障害後レビューポストモーテムで必ず決めること">6. 障害後レビュー（ポストモーテム）で必ず決めること</h2>
<p>「接続が足りませんでした」で終えると再発します。次の観点を明文化します。</p>
<ol>
<li>なぜ早期検知できなかったか</li>
<li>どの設定が設計値と乖離していたか</li>
<li>コード修正・設定修正・監視修正の担当と期限</li>
<li>次回同様インシデント時の runbook 更新点</li>
</ol>
<p>運用改善は「学びをコード化する」ことです。ドキュメントだけでなく、Terraform や Helm values に反映して初めて再発率が下がります。</p>
<h2 id="7-実務向けチェックリスト">7. 実務向けチェックリスト</h2>
<p>最後に、現場で使いやすいチェックリストを置いておきます。</p>
<ul>
<li><input disabled="" type="checkbox"> <code>max_connections</code> と総プール上限の関係を計算済み</li>
<li><input disabled="" type="checkbox"> 接続リーク防止（close/return保証）が実装されている</li>
<li><input disabled="" type="checkbox"> 長時間トランザクションに timeout が設定済み</li>
<li><input disabled="" type="checkbox"> PgBouncer の pool_mode が要件に適合している</li>
<li><input disabled="" type="checkbox"> <code>numbackends</code> と接続待ち時間のアラートがある</li>
<li><input disabled="" type="checkbox"> 障害時 runbook に SQL コマンドと判断基準が書かれている</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>PostgreSQL 接続枯渇は、単なるパラメータ不足ではなく、アプリ実装・スケーリング設計・監視不足が重なって起こる複合障害です。だからこそ、</p>
<ul>
<li>初動で止血する</li>
<li>パターン別に切り分ける</li>
<li>設定と実装を同時に直す</li>
<li>監視と runbook に落とし込む</li>
</ul>
<p>この流れを徹底するだけで、同じ障害の再発率は大きく下げられます。次に障害が起きたとき、慌てず順番に潰せる状態を今日作っておくのが最善です。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>PostgreSQL</category>
      <category>Performance</category>
      <category>SRE</category>
      <category>Troubleshooting</category>
      <category>Backend</category>
    </item>
    <item>
      <title>PostgreSQLデッドロック調査プレイブック：再現・可視化・恒久対策までの実践手順</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>【運営報告】ブログ自動化システムの安定化に向けた取り組み</title>
      <link>https://www.ai2core.com/posts/2026-02-18-status-update/</link>
      <pubDate>Wed, 18 Feb 2026 19:15:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-18-status-update/</guid>
      <description>自動投稿システムの不具合と、リポジトリ再構築による解決策について。</description>
      <content:encoded><![CDATA[<h1 id="運営報告ブログ自動化システムの安定化に向けた取り組み">【運営報告】ブログ自動化システムの安定化に向けた取り組み</h1>
<h2 id="はじめにその自動化本当に自動ですか">はじめに：その自動化、本当に「自動」ですか？</h2>
<p>「ブログの自動投稿システムを組んだけど、なぜか時々ビルドに失敗する…」
「CI/CDパイプラインがエラーで止まるたびに、原因究明に数十分も費やしている…」
「最初はシンプルだったのに、機能を追加していくうちにリポジトリがカオスになってきた…」</p>
<p>もしあなたが個人ブログや技術ドキュメントサイトをGitHub Actionsなどで自動化していて、このような悩みを抱えているなら、この記事はあなたのためのものです。</p>
<p>こんにちは。当ブログを運営している筆者です。私もかつて、まさにこの問題の渦中にいました。Markdownで記事を書いてGitHubにプッシュすれば、あとは魔法のようにサイトが更新される──そんな夢のような自動化システムを構築したはずが、いつしかそれは「時々機嫌を損ねる気難しい同居人」のような存在になっていました。依存関係のエラー、ローカルとCI環境での挙動の違い、肥大化したワークフローファイル…。これらは、自動化による恩恵を帳消しにするほどのストレスと時間的損失をもたらしていました。</p>
<p>この記事では、私が直面したブログ自動化システムの不安定化問題と、その根本原因を深く掘り下げ、<strong>リポジトリの再構築とビルド環境のコンテナ化</strong>というアプローチでいかにして安定稼働を実現したか、その全貌を余すところなくお伝えします。</p>
<p>この記事を読み終える頃には、あなたは以下の知識とテクニックを手にしているはずです。</p>
<ul>
<li>不安定なCI/CDパイプラインの根本原因を診断するための着眼点</li>
<li>モノレポとマルチレポの考え方を適用した、メンテナンス性の高いリポジトリ設計</li>
<li>Dockerを活用して「どこでも同じように動く」ビルド環境を構築する方法</li>
<li>堅牢で再利用性の高いGitHub Actionsワークフローを設計するための具体的なベストプラクティス</li>
<li>技術的負債と向き合い、システムを長期的に健全な状態に保つためのマインドセット</li>
</ul>
<p>単なる対症療法ではない、根本からのシステム改善に興味のある方は、ぜひ最後までお付き合いください。</p>
<h2 id="なぜブログ自動化システムは不安定になったのか---課題の深掘り">なぜブログ自動化システムは不安定になったのか？ - 課題の深掘り</h2>
<p>解決策を語る前に、まずは私のブログシステムがどのような問題を抱えていたのか、その背景と原因を詳しく見ていきましょう。問題を正しく理解することが、正しい解決策への第一歩です。</p>
<h3 id="当初のシステム構成">当初のシステム構成</h3>
<p>私のブログは、多くの技術ブログで採用されているであろう、ごく一般的な構成でした。</p>
<ul>
<li><strong>コンテンツ管理</strong>: Markdownファイル</li>
<li><strong>静的サイトジェネレーター(SSG)</strong>: Hugo</li>
<li><strong>ソースコード管理</strong>: GitHub</li>
<li><strong>CI/CD</strong>: GitHub Actions</li>
<li><strong>ホスティング</strong>: GitHub Pages</li>
</ul>
<p>この構成における自動投稿の基本的な流れは、以下の図のようになります。</p>
<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">graph TD
    A[記事(Markdown)をpush] --&gt; B{GitHub Actions};
    B --&gt; C[Hugoでビルド];
    C --&gt; D[GitHub Pagesへデプロイ];
</code></pre><p>非常にシンプルで、最初はこれで何の問題もありませんでした。しかし、ブログ運営を続けるうちに、様々な機能を追加したくなり、システムは徐々に複雑化していきました。そして、以下の3つの大きな問題が顕在化したのです。</p>
<h3 id="問題1依存関係地獄-dependency-hell">問題1：依存関係地獄 (Dependency Hell)</h3>
<p>「私のローカル環境ではちゃんとビルドできるのに、なぜかGitHub Actions上では失敗する」。この現象に、あなたも見覚えがないでしょうか。これは、開発環境と実行環境の間に存在する「差異」が原因で発生します。</p>
<p>私の場合、具体的には以下のような問題に悩まされていました。</p>
<ul>
<li><strong>Hugoのバージョン不整合</strong>: ローカルで使っているHugoのバージョンと、GitHub ActionsのワークフローでセットアップされるHugoのバージョンが微妙に異なり、テンプレートの仕様変更などでビルドエラーが発生する。</li>
<li><strong>Node.js依存ツールのバージョン問題</strong>: 私が使っていたHugoテーマは、CSSのトランスパイルにSass（Dart Sass）を利用しており、これはNode.jsに依存していました。ローカルのNode.js/npmバージョンとCI環境のバージョンが異なると、<code>npm install</code>が失敗したり、Sassのコンパイル結果が変わってしまったりしました。</li>
<li><strong>Go Modulesの混乱</strong>: HugoはGoで書かれているため、テーマによってはGo Modules (<code>go.mod</code>, <code>go.sum</code>) を利用します。CI環境のGoのバージョンが古いと、これもまたエラーの原因となりました。</li>
</ul>
<p>これらのバージョンを<code>package.json</code>やワークフローファイルで固定しようと試みましたが、複数の依存関係が絡み合うと管理が非常に煩雑になり、根本的な解決には至りませんでした。</p>
<h3 id="問題2ワークフローの肥大化と複雑化">問題2：ワークフローの肥大化と複雑化</h3>
<p>当初はHugoでビルドしてデプロイするだけだったGitHub Actionsのワークフローファイルは、時を経て巨大な怪物へと変貌していました。</p>
<ul>
<li><strong>記事のリンク切れをチェックするジョブ</strong></li>
<li><strong>画像形式をWebPに変換し、最適化するジョブ</strong></li>
<li><strong>SEOのために構造化データを検証するジョブ</strong></li>
<li><strong>サイトマップを自動生成して検索エンジンに通知するジョブ</strong></li>
</ul>
<p>これらの便利な機能を次々と追加した結果、一つのYAMLファイルが数百行にも及び、誰にも全体像が把握できない状態になってしまいました。</p>
<p>この「モノリシック・ワークフロー」は、以下のような弊害を生み出しました。</p>
<ul>
<li><strong>可読性の低下</strong>: ワークフロー全体の流れを追うのが困難で、新しいジョブを追加したり、既存のジョブを修正したりするのが怖い。</li>
<li><strong>デバッグの困難さ</strong>: どこか一つのジョブが失敗すると、他の無関係なジョブまで影響を受け、デプロイ全体が停止してしまう。エラーの原因特定にも時間がかかりました。</li>
<li><strong>実行効率の悪化</strong>: 単に記事のタイポを一行修正しただけのpushでも、画像最適化やリンクチェックなど、時間のかかる全てのジョブが毎回実行され、CIのリソースと時間を無駄に消費していました。</li>
</ul>
<h3 id="問題3モノリシックなリポジトリ構造">問題3：モノリシックなリポジトリ構造</h3>
<p>最大の問題は、これら全ての要素が<strong>単一のリポジトリ</strong>に混在していることでした。</p>
<ul>
<li>記事のMarkdownファイル (<code>content/</code>)</li>
<li>Hugoのソースコードと設定ファイル (<code>layouts/</code>, <code>static/</code>, <code>hugo.toml</code>)</li>
<li>カスタマイズしたテーマのコード (<code>themes/my-theme/</code>)</li>
<li>自動化スクリプトやワークフローファイル (<code>.github/workflows/</code>)</li>
<li>Node.jsの依存関係ファイル (<code>package.json</code>, <code>node_modules/</code>)</li>
</ul>
<p>この「何でもアリ」なリポジトリ構造は、<strong>関心の分離</strong>というソフトウェア設計の基本原則に反しており、メンテナンス性を著しく低下させていました。</p>
<p>例えば、記事を執筆するライター（私自身ですが）は、本来Markdownファイルのことだけを気にしていれば良いはずです。しかし、この構造では、HugoのビルドロジックやCIの設定ファイルまで目に入ってしまい、誤って変更してしまうリスクがありました。</p>
<p>逆に、サイトのデザインを修正したい場合、テーマのCSSファイルを変更するだけなのに、記事コンテンツも一緒に管理されているため、リポジトリが肥大化し、クローンや操作が重くなっていました。</p>
<p>これらの問題が絡み合い、私のブログ自動化システムは、もはや「自動」と呼ぶには程遠い、手のかかる不安定な代物になってしまったのです。</p>
<h2 id="解決策リポジトリの再構築とcicdパイプラインの刷新">解決策：リポジトリの再構築とCI/CDパイプラインの刷新</h2>
<p>問題の根源が見えてきました。それは突き詰めると、**「環境の不一致」<strong>と</strong>「関心の分離の欠如」**という、2つの古典的な課題に集約されます。</p>
<p>この根本原因を解消するために、私は以下の2つの大きな方針を立て、システムの全面的な再構築に踏み切りました。</p>
<ol>
<li><strong>ビルド環境のコンテナ化 (Docker)</strong>: 開発環境とCI環境の差異を撲滅し、完全な再現性を確保する。</li>
<li><strong>リポジトリの分割 (マルチレポ戦略)</strong>: 関心事ごとにリポジトリを分割し、それぞれの責務を明確にする。</li>
</ol>
<p>ここからは、この2つの方針に基づいた具体的な改善策を、コードや図を交えて詳細に解説していきます。</p>
<h3 id="1-dockerによるビルド環境の再現性確保">1. Dockerによるビルド環境の再現性確保</h3>
<p>「ローカルでは動くのにCIではコケる」問題を撲滅する最も確実な方法は、ローカルとCIで<strong>全く同じ環境</strong>を使うことです。これを実現するのがDockerです。</p>
<p>私は、Hugo、Node.js、その他ビルドに必要なツールをすべて含んだカスタムDockerイメージを作成することにしました。これにより、ビルド環境そのものをコード（Dockerfile）として管理できるようになります。</p>
<h4 id="dockerfileの作成">Dockerfileの作成</h4>
<p>以下が、私のブログ用に作成した<code>Dockerfile</code>です。Hugoの拡張版公式イメージをベースに、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><span 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></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-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#75715e"># ベースイメージにはHugoの公式拡張版イメージを利用</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># ARGでバージョンを外部から指定できるようにしておく</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ARG</span> HUGO_VERSION<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>.125.4<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> klakegg/hugo:${HUGO_VERSION}-ext-alpine</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># ビルドに必要なツールをインストール</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># alpineベースなのでapkコマンドを使用</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apk add --no-cache nodejs npm git<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># 作業ディレクトリを設定</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span><span style="color:#e6db74"> /src</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># package.jsonとpackage-lock.jsonを先にコピーする</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># これにより、ソースコードが変更されても、依存関係が変わらなければ</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># `npm ci`のレイヤーはキャッシュが利用され、ビルドが高速化する</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> package.json package-lock.json ./<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># npm ciで依存関係を厳密にインストール</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> npm ci<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># プロジェクトのソースコード全体をコピー</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> . .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># hugoコマンドでビルドを実行</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># --minifyオプションで生成されるファイルを圧縮</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> hugo --minify<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># --- 以下はローカル開発用の設定 ---</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># ポートを公開 (hugo server用)</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">EXPOSE</span><span style="color:#e6db74"> 1313</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># デフォルトのコンテナ起動コマンド</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># ローカルでhugo serverを起動する際に使用</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">CMD</span> [<span style="color:#e6db74">&#34;hugo&#34;</span>, <span style="color:#e6db74">&#34;server&#34;</span>, <span style="color:#e6db74">&#34;-D&#34;</span>, <span style="color:#e6db74">&#34;--bind&#34;</span>, <span style="color:#e6db74">&#34;0.0.0.0&#34;</span>, <span style="color:#e6db74">&#34;--baseURL&#34;</span>, <span style="color:#e6db74">&#34;http://localhost:1313/&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>このDockerfileのポイントは、<code>npm ci</code>をソースコード全体の<code>COPY</code>より前に実行している点です。これにより、Dockerのレイヤーキャッシュが効率的に機能し、依存関係に変更がない限り、<code>npm ci</code>は再実行されず、イメージビルドの時間を短縮できます。</p>
<h4 id="ローカル開発環境での利用">ローカル開発環境での利用</h4>
<p>ローカルでの執筆・プレビュー時にもこのDockerイメージを使うことで、環境の差異を完全になくします。そのために<code>docker-compose.yml</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#e6db74">&#39;3.8&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hugo</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># カレントディレクトリのDockerfileを使ってイメージをビルド</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">build</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">context</span>: <span style="color:#ae81ff">.</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dockerfile</span>: <span style="color:#ae81ff">Dockerfile</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ホストマシンのカレントディレクトリをコンテナの/srcにマウント</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># これにより、ローカルでファイルを編集すると即座にコンテナ内に反映される</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">.:/src</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ホストの1313番ポートをコンテナの1313番ポートにフォワーディング</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;1313:1313&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この設定により、ターミナルで<code>docker-compose up</code>というコマンドを一つ実行するだけで、ローカルにHugoやNode.jsがインストールされていなくても、誰でも全く同じ開発環境を起動できるようになりました。</p>
<p>これで、「環境の不一致」という最大の問題は解決です。</p>
<h3 id="2-マルチレポ戦略による関心の分離">2. マルチレポ戦略による関心の分離</h3>
<p>次に、「関心の分離の欠如」という問題に取り組みます。私は、巨大化したモノリシックなリポジトリを、責務に基づいて以下の3つのリポジトリに分割しました。</p>
<ol>
<li><code>blog-contents</code>: <strong>記事のMarkdownファイルのみ</strong>を管理するリポジトリ。執筆活動の拠点です。</li>
<li><code>blog-theme</code>: Hugoのテーマ、サイト設定 (<code>hugo.toml</code>)、ビルド設定 (<code>Dockerfile</code>, <code>package.json</code>など)、GitHub Actionsワークフローなど、<strong>サイトの骨格とビルドロジック</strong>を管理するリポジトリ。</li>
<li><code>blog-deploy</code>: Hugoが生成した静的ファイル（HTML, CSS, JS）を格納し、<strong>GitHub Pagesで公開する</strong>ためのリポジトリ。</li>
</ol>
<p>この新しい構成を図にすると、以下のようになります。</p>
<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">graph TD
    subgraph &#34;執筆リポジトリ (blog-contents)&#34;
        A[記事(Markdown)をpush]
    end

    subgraph &#34;テーマ/ビルド リポジトリ (blog-theme)&#34;
        B[Dockerfile]
        C[hugo.toml]
        D[テーマファイル]
        W[.github/workflows/deploy.yml]
    end

    subgraph &#34;デプロイリポジトリ (blog-deploy)&#34;
        F[生成されたHTML/CSS/JS]
    end

    A -- トリガー --&gt; E{GitHub Actions};
    E -- ワークフロー定義の読み込み --&gt; W;
    E -- 1. blog-themeをチェックアウト --&gt; D;
    E -- 2. blog-contentsをチェックアウト --&gt; A_content[Markdown];
    E -- 3. Dockerイメージビルド --&gt; B;
    E -- 4. Hugoビルド実行 --&gt; G[ビルド処理];
    G -- 5. 生成物をpush --&gt; F;

    F --&gt; H[GitHub Pages];
</code></pre><p>この構成の肝は、GitHub Actionsのワークフローです。<code>blog-contents</code>リポジトリへのpushをトリガーに、<code>blog-theme</code>リポジトリで定義されたワークフローが実行されます。ワークフローは、<code>blog-theme</code>自身と<code>blog-contents</code>の両方をチェックアウトし、<code>blog-theme</code>内のDockerfileを使ってビルドを実行。最後に、生成物を<code>blog-deploy</code>リポジトリにプッシュします。</p>
<h4 id="新しいgithub-actionsワークフロー">新しいGitHub Actionsワークフロー</h4>
<p>以下は、この新しい構成を実現するための<code>blog-theme</code>リポジトリに配置するワークフローファイル (<code>.github/workflows/deploy.yml</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><span 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></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">Build and Deploy Blog</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:#75715e"># blog-contentsリポジトリのmainブランチへのpushをトリガーにする</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># repository_dispatchイベントを利用</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">repository_dispatch</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">types</span>: [<span style="color:#ae81ff">build-blog]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># 手動実行も可能にする</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">build-and-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">steps</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Step 1: テーマとビルドロジックのリポジトリをチェックアウト</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Checkout theme and build repository</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">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">repository</span>: <span style="color:#ae81ff">your-username/blog-theme</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">path</span>: <span style="color:#ae81ff">blog-theme</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Step 2: 記事コンテンツのリポジトリをチェックアウト</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Checkout contents repository</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">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">repository</span>: <span style="color:#ae81ff">your-username/blog-contents</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">path</span>: <span style="color:#ae81ff">blog-contents</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Step 3: Dockerイメージをビルド</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># キャッシュを活用してビルドを高速化</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Docker Buildx</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/setup-buildx-action@v3</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build Docker image</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/build-push-action@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">context</span>: <span style="color:#ae81ff">./blog-theme</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">load</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">tags</span>: <span style="color:#ae81ff">blog-builder:latest</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">cache-from</span>: <span style="color:#ae81ff">type=gha</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">cache-to</span>: <span style="color:#ae81ff">type=gha,mode=max</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Step 4: Dockerコンテナ内でHugoビルドを実行</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build Hugo site</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">          docker run --rm \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -v $(pwd)/blog-contents:/src/content \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            -v $(pwd)/public:/src/public \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            blog-builder:latest</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Step 5: 生成された静的ファイルをデプロイリポジトリにプッシュ</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy to GitHub Pages</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">peaceiris/actions-gh-pages@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">github_token</span>: <span style="color:#ae81ff">${{ secrets.GITHUB_TOKEN }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#75715e"># デプロイ先のリポジトリとブランチを指定</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">external_repository</span>: <span style="color:#ae81ff">your-username/blog-deploy</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">publish_branch</span>: <span style="color:#ae81ff">main</span>
</span></span><span style="display:flex;"><span>          <span style="color:#75715e"># デプロイディレクトリを指定</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">publish_dir</span>: <span style="color:#ae81ff">./public</span>
</span></span><span style="display:flex;"><span>          <span style="color:#75715e"># コミットユーザー情報を設定</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">user_name</span>: <span style="color:#e6db74">&#39;github-actions[bot]&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">user_email</span>: <span style="color:#e6db74">&#39;github-actions[bot]@users.noreply.github.com&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><em>注: <code>repository_dispatch</code>を外部リポジトリからトリガーするには、<code>blog-contents</code>リポジトリ側でPAT（Personal Access Token）を使いAPIを叩くワークフローが別途必要になります。よりシンプルな構成としては、<code>blog-theme</code>リポジトリに<code>on.push</code>トリガーを設定し、<code>blog-contents</code>をGit Submoduleとして管理する方法も考えられます。</em></p>
<p>このリポジトリ分割により、関心事が明確に分離され、それぞれの役割に集中できる環境が整いました。</p>
<h2 id="改善によるメリットとデメリット">改善によるメリットとデメリット</h2>
<p>この大掛かりな再構築によって、何が良くなり、そしてどのような新たなトレードオフが生まれたのでしょうか。</p>
<h3 id="メリット">メリット</h3>
<ol>
<li><strong>絶大な安定性と再現性</strong>: Docker化により、「私のマシンでは動くのに」問題は完全に過去のものとなりました。CIは常に期待通りに動作し、ビルド失敗に悩まされる時間はゼロに近くなりました。</li>
<li><strong>劇的に向上したメンテナンス性</strong>:
<ul>
<li><strong>執筆者</strong>: Markdownを書くことだけに集中できます。ビルドの仕組みを意識する必要はありません。</li>
<li><strong>開発者</strong>: サイトのデザイン変更や機能追加は<code>blog-theme</code>リポジトリで完結します。記事コンテンツに影響を与える心配なく、大胆なリファクタリングも可能です。</li>
</ul>
</li>
<li><strong>効率化されたCI/CDパイプライン</strong>: Dockerレイヤーキャッシュの活用により、依存関係に変更がない限りビルドは高速です。また、記事の更新とテーマの更新で関心が分離されているため、不要なジョブが実行されることもありません。</li>
<li><strong>将来の拡張性 (スケーラビリティ)</strong>: もし将来、HugoからAstroやNext.jsのような別のSSGに乗り換えたくなったとしても、<code>blog-contents</code>リポジトリには一切手を加える必要がありません。<code>blog-theme</code>リポジトリを新しい技術スタックで再構築するだけで移行が完了します。これは非常に大きな利点です。</li>
</ol>
<h3 id="デメリット">デメリット</h3>
<ol>
<li><strong>構成の複雑化</strong>: リポジトリが1つから3つに増え、全体のアーキテクチャを理解するための初期学習コストは確実に上がりました。新しいメンバーが参加した際の説明も、以前より少し手間がかかります。</li>
<li><strong>初期セットアップの手間</strong>: Dockerfileやdocker-compose.yml、リポジトリ間を連携させるためのGitHub Actionsワークフローの初期設定は、それなりに知識と時間を要します。</li>
<li><strong>リソース消費</strong>: Dockerイメージをビルド・保存するために、GitHub Actionsの実行時間や、GHCR (GitHub Container Registry) などのストレージ容量を消費します。小規模なブログでは過剰装備と感じるかもしれません。</li>
</ol>
<p>これらのデメリットは存在しますが、長期的な運用を見据えた場合、得られる安定性とメンテナンス性のメリットはそれを遥かに上回ると私は確信しています。</p>
<h2 id="現場で使える実践的なtips">現場で使える実践的なTips</h2>
<p>今回の再構築を通して得られた、さらに一歩進んだ知見やテクニックをいくつかご紹介します。</p>
<h3 id="tip-1-dependabotによる依存関係の自動更新">Tip 1: Dependabotによる依存関係の自動更新</h3>
<p>安定性を追求するあまり、各種ライブラリのバージョンを塩漬けにしてしまうのは良いプラクティスではありません。セキュリティ脆弱性に対応するためにも、依存関係は定期的に更新すべきです。
<code>blog-theme</code>リポジトリに<strong>Dependabot</strong>を設定しましょう。以下の<code>.github/dependabot.yml</code>をリポジトリに追加するだけで、Dockerfile内のHugoバージョンや、<code>package.json</code>内のnpmパッケージが古くなった場合に、自動で更新のプルリクエストを作成してくれます。</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">version</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">updates</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Dockerfileのバージョンをチェック</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">package-ecosystem</span>: <span style="color:#e6db74">&#34;docker&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">directory</span>: <span style="color:#e6db74">&#34;/&#34;</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">interval</span>: <span style="color:#e6db74">&#34;weekly&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># npmの依存関係をチェック</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">package-ecosystem</span>: <span style="color:#e6db74">&#34;npm&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">directory</span>: <span style="color:#e6db74">&#34;/&#34;</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">interval</span>: <span style="color:#e6db74">&#34;weekly&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="tip-2-makefileで開発体験を向上させる">Tip 2: Makefileで開発体験を向上させる</h3>
<p><code>docker-compose up</code> や <code>docker exec ...</code> のようなコマンドを毎回入力するのは面倒です。<strong>Makefile</strong>を使って、よく使う操作をシンプルなコマンドにラップしましょう。</p>
<p><code>blog-theme</code>リポジトリのルートに以下のような<code>Makefile</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></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-makefile" data-lang="makefile"><span style="display:flex;"><span><span style="color:#a6e22e">.PHONY</span><span style="color:#f92672">:</span> help setup server build clean
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">help</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span>	@echo <span style="color:#e6db74">&#34;Usage: make [target]&#34;</span>
</span></span><span style="display:flex;"><span>	@echo <span style="color:#e6db74">&#34;targets:&#34;</span>
</span></span><span style="display:flex;"><span>	@echo <span style="color:#e6db74">&#34;  setup     Install dependencies&#34;</span>
</span></span><span style="display:flex;"><span>	@echo <span style="color:#e6db74">&#34;  server    Start development server&#34;</span>
</span></span><span style="display:flex;"><span>	@echo <span style="color:#e6db74">&#34;  build     Build static files for production&#34;</span>
</span></span><span style="display:flex;"><span>	@echo <span style="color:#e6db74">&#34;  clean     Remove generated files and node_modules&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">setup</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span>	docker-compose build
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span>	docker-compose up
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">build</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span>	docker-compose run --rm hugo hugo --minify
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">clean</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span>	rm -rf public resources node_modules
</span></span></code></pre></td></tr></table>
</div>
</div><p>これにより、<code>make server</code>で開発サーバーを起動、<code>make build</code>で本番用のビルドを実行、といった直感的な操作が可能になり、開発体験が大きく向上します。</p>
<h3 id="tip-3-プルリクエストでプレビュー環境を自動構築">Tip 3: プルリクエストでプレビュー環境を自動構築</h3>
<p><code>blog-contents</code>リポジトリに新しい記事のプルリクエストが作成された際、変更内容を実際のサイトで確認できるプレビュー環境が自動で立ち上がると、レビューが格段に捗ります。
Cloudflare PagesやVercel、Netlifyといったホスティングサービスは、このプレビューデプロイ機能に優れています。GitHub Actionsのワークフローを少し変更し、PRイベントをトリガーにしてこれらのサービスにデプロイするジョブを追加するだけで、魔法のようなプレビュー体験が実現できます。</p>
<h2 id="まとめ">まとめ</h2>
<p>今回は、不安定化したブログ自動化システムを、根本原因から見直して再構築した道のりをご紹介しました。</p>
<p>改めて、今回の取り組みの要点を振り返ります。</p>
<ul>
<li><strong>問題</strong>: システムの不安定性は、「環境の不一致」と「関心の分離の欠如」という2つの根深い問題に起因していた。</li>
<li><strong>解決策</strong>:
<ol>
<li><strong>Dockerによるビルド環境のコンテナ化</strong>で、完全な「再現性」を確保した。</li>
<li><strong>マルチレポ戦略によるリポジトリ分割</strong>で、「関心の分離」を徹底し、メンテナンス性を向上させた。</li>
</ol>
</li>
</ul>
<p>この取り組みから得られた最大の教訓は、<strong>自動化システム（CI/CDパイプライン）もまた、一つの重要なアプリケーションである</strong>ということです。「とりあえず動けばいい」という場当たり的な実装は、必ず将来の技術的負債となって自分に返ってきます。アプリケーションコードと同様に、クリーンな設計、リファクタリング、そして継続的な改善が不可欠なのです。</p>
<p>もし今、あなたの自動化システムが悲鳴を上げているなら、それはアーキテクチャを見直す絶好の機会かもしれません。この記事で紹介した「再現性の確保」と「関心の分離」という2つの原則が、あなたのシステムの安定化に向けた確かな道しるべとなることを願っています。</p>
<p>Happy Automating</p>
]]></content:encoded>
      <category>Status</category>
      <category>OpenClaw</category>
      <category>DevOps</category>
      <category>Troubleshooting</category>
    </item>
  </channel>
</rss>
