<?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>DevOps on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/devops/</link>
    <description>Recent content in DevOps on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 07 Mar 2026 13:00:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/devops/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>GitHub Actionsでモノレポを安全に自動リリースする設計: 変更検知・段階配布・失敗復旧</title>
      <link>https://www.ai2core.com/posts/2026-03-07-github-actions-monorepo-release-automation-guide/</link>
      <pubDate>Sat, 07 Mar 2026 13:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-07-github-actions-monorepo-release-automation-guide/</guid>
      <description>モノレポ運用でGitHub Actionsを使い、変更検知から段階リリース、ロールバック、監査まで実装する具体手順を解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actionsでモノレポを安全に自動リリースする設計-変更検知段階配布失敗復旧">GitHub Actionsでモノレポを安全に自動リリースする設計: 変更検知・段階配布・失敗復旧</h1>
<p>モノレポのCI/CDは、単一リポジトリだから楽になる一方で、リリース設計を誤ると一気に難しくなります。</p>
<ul>
<li>1つの変更で全サービスを再デプロイしてしまう</li>
<li>並列ジョブが増えてキュー渋滞する</li>
<li>どのコミットがどのサービスへ反映されたか追跡できない</li>
<li>一部失敗時のロールバックが曖昧</li>
</ul>
<p>本記事では、GitHub Actionsでモノレポを運用しているチーム向けに、実務で耐えるリリース自動化の構成を具体的に説明します。</p>
<h2 id="1-モノレポcicdで先に決める設計原則">1. モノレポCI/CDで先に決める設計原則</h2>
<p>最初に次の原則を明文化します。</p>
<ol>
<li>変更のないサービスはデプロイしない</li>
<li>リリース対象は機械的に決定する</li>
<li>本番反映は段階的（canary/割合配布）</li>
<li>失敗時の復旧手順を自動化する</li>
<li>監査ログ（誰が何をいつ）を残す</li>
</ol>
<p>この5つがないと、運用が属人化し、障害時対応が遅れます。</p>
<h2 id="2-変更検知をワークフローの入口に置く">2. 変更検知をワークフローの入口に置く</h2>
<p>モノレポでは「どのディレクトリが変わったか」を最初に判定し、対象サービスだけを処理します。</p>
<h3 id="21-changed-filesの例">2.1 changed-filesの例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">detect</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">outputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">matrix</span>: <span style="color:#ae81ff">${{ steps.set-matrix.outputs.matrix }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">changed</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">tj-actions/changed-files@v45</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">files_yaml</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            api:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              - services/api/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            web:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              - services/web/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            worker:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              - services/worker/**</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">set-matrix</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          python .github/scripts/build_matrix.py &#39;${{ toJson(steps.changed.outputs) }}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここでmatrixを作り、後続ジョブを <code>fromJson</code> で動的展開します。</p>
<h2 id="3-reusable-workflowで共通化する">3. reusable workflowで共通化する</h2>
<p>各サービスごとにworkflowを複製すると、保守コストが急上昇します。<code>workflow_call</code> で共通化します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># .github/workflows/release-service.yml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_call</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">string</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">string</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">release</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">./scripts/release.sh ${{ inputs.service }} ${{ inputs.environment }}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>呼び出し側はサービス名だけ渡せばよくなり、ガバナンスが効きます。</p>
<h2 id="4-同時実行制御concurrencyで事故防止">4. 同時実行制御（concurrency）で事故防止</h2>
<p>同一サービスへの並列デプロイは事故のもとです。<code>concurrency</code> を必ず設定します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">concurrency</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">group</span>: <span style="color:#ae81ff">release-${{ github.ref }}-${{ matrix.service }}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cancel-in-progress</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>cancel-in-progress</code> は本番では通常 <code>false</code> が安全です。途中キャンセルで半端状態を作らないためです。</p>
<h2 id="5-環境ごとの保護ルールを使う">5. 環境ごとの保護ルールを使う</h2>
<p>GitHub Environmentsを使えば、本番前レビューやシークレット分離を標準機能で実装できます。</p>
<ul>
<li>staging: 自動デプロイ</li>
<li>production: 承認必須</li>
<li>緊急時のみ管理者がoverride</li>
</ul>
<p>加えて、OIDCでクラウド認証し、長期鍵（固定アクセスキー）を排除します。</p>
<h2 id="6-段階リリースcanaryを組み込む">6. 段階リリース（Canary）を組み込む</h2>
<p>本番へ一気に100%展開は避け、段階配布をワークフローに組み込みます。</p>
<p>例:</p>
<ol>
<li>10%トラフィックへ5分</li>
<li>エラーレート確認</li>
<li>問題なければ50%</li>
<li>最終的に100%</li>
</ol>
<p>疑似コード例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>deploy --service api --traffic <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>check_slo api <span style="color:#f92672">||</span> rollback api
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>deploy --service api --traffic <span style="color:#ae81ff">50</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>check_slo api <span style="color:#f92672">||</span> rollback api
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>deploy --service api --traffic <span style="color:#ae81ff">100</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>check_slo</code> はp95レイテンシ・5xx率・ビジネスKPIを含めるのが理想です。</p>
<h2 id="7-失敗復旧を手順ではなく機能にする">7. 失敗復旧を「手順」ではなく「機能」にする</h2>
<p>運用で強いチームは、ロールバックを手作業Runbookではなくワークフロー化しています。</p>
<h3 id="71-rollback-workflow例">7.1 rollback workflow例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">target_sha</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rollback</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">./scripts/rollback.sh ${{ github.event.inputs.service }} ${{ github.event.inputs.target_sha }}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>「事故時はこのボタンを押す」で復旧できる状態を目指してください。</p>
<h2 id="8-リリース証跡の自動生成">8. リリース証跡の自動生成</h2>
<p>監査対応やトラブル解析で必須になるのが証跡です。</p>
<p>最低限、以下をアーティファクト化します。</p>
<ul>
<li>対象サービス一覧</li>
<li>対象コミットSHA</li>
<li>実行者</li>
<li>実行時刻</li>
<li>結果（成功/失敗）</li>
<li>ロールバック有無</li>
</ul>
<p>さらにSlack/Discord通知で、サービス単位の反映結果を即時共有すると現場が安定します。</p>
<h2 id="9-コスト最適化も同時に行う">9. コスト最適化も同時に行う</h2>
<p>モノレポCIは規模が大きくなるほどコストが効いてきます。</p>
<ul>
<li>キャッシュ（依存/ビルド成果物）を徹底</li>
<li>変更のないサービスはskip</li>
<li>夜間バッチと競合しない時間帯に重いjobを寄せる</li>
<li>自己ホストランナーはautoscaling前提で運用</li>
</ul>
<p>「全サービス毎回build」は最初は簡単ですが、半年後に必ず負債化します。</p>
<h2 id="10-導入チェックリスト">10. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 変更検知のmatrix生成がある</li>
<li><input disabled="" type="checkbox"> reusable workflowに統一されている</li>
<li><input disabled="" type="checkbox"> concurrency制御がサービス単位で設定済み</li>
<li><input disabled="" type="checkbox"> production環境に承認ルールあり</li>
<li><input disabled="" type="checkbox"> canary + 自動SLO判定がある</li>
<li><input disabled="" type="checkbox"> rollback workflowが存在する</li>
<li><input disabled="" type="checkbox"> リリース証跡を保存している</li>
<li><input disabled="" type="checkbox"> 通知連携（Slack/Discord）が整備されている</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>GitHub Actionsでのモノレポ自動リリースは、単にworkflowを書く作業ではありません。<strong>変更検知・段階配布・復旧自動化・監査</strong> を含む運用設計です。</p>
<ul>
<li>デプロイ対象を最小化する</li>
<li>本番反映は必ず段階化する</li>
<li>失敗時の復旧を自動化する</li>
<li>証跡を残して再現性を確保する</li>
</ul>
<p>この設計を最初に固めると、サービス数が増えても運用は破綻しません。まずは「変更検知 + reusable workflow + rollback workflow」の3点から着手するのが実装コストと効果のバランスが良いです。</p>
<h2 id="付録-失敗しにくい運用設計テンプレート">付録: 失敗しにくい運用設計テンプレート</h2>
<p>最後に、運用へ落とし込むときに有効なテンプレートを示します。実際にはこの3点をチーム標準にするだけで、リリース事故率が下がります。</p>
<ul>
<li><strong>PRテンプレートに「影響サービス」欄を必須化</strong>
<ul>
<li><code>service-a, service-b</code> のように宣言させ、変更検知結果と突合する</li>
</ul>
</li>
<li><strong>デプロイ承認時の確認項目を固定化</strong>
<ul>
<li>監視グラフURL、ロールバック手順URL、オンコール担当を毎回入力</li>
</ul>
</li>
<li><strong>失敗後レビューのフォーマット統一</strong>
<ul>
<li>失敗原因、検知時刻、復旧時刻、再発防止策を必ず残す</li>
</ul>
</li>
</ul>
<p>特にモノレポでは「誰がどこに責任を持つか」が曖昧になりやすいです。workflowの技術実装だけでなく、<strong>承認と振り返りの運用ルール</strong>をセットにして初めて、自動リリースは安定運用に乗ります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>Monorepo</category>
      <category>CI/CD</category>
      <category>Release</category>
      <category>DevOps</category>
    </item>
    <item>
      <title>Terraform→OpenTofu移行実践ガイド: 既存IaCを止めずに移行するエンタープライズ手順</title>
      <link>https://www.ai2core.com/posts/2026-03-07-terraform-opentofu-migration-enterprise-playbook/</link>
      <pubDate>Sat, 07 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-07-terraform-opentofu-migration-enterprise-playbook/</guid>
      <description>Terraform資産をOpenTofuへ安全に移行するための、互換性確認、state管理、CI更新、段階的切替の実践手順を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="terraformopentofu移行実践ガイド-既存iacを止めずに移行するエンタープライズ手順">Terraform→OpenTofu移行実践ガイド: 既存IaCを止めずに移行するエンタープライズ手順</h1>
<p>Terraformのライセンス変更以降、OpenTofuへ移行したいという相談は確実に増えています。とはいえ現場の本音は「理屈は分かるが、stateが壊れたら終わる」「本番を止めずに移行できるのか不安」です。</p>
<p>結論から言うと、移行は十分可能です。ただし「CLIを置き換えるだけ」で済むケースは限定的で、実際は <strong>providerバージョン整合・state lock・CI/CD・運用Runbook</strong> までまとめて整える必要があります。</p>
<p>本記事では、既にTerraformを本番運用しているチーム向けに、OpenTofuへ段階移行する実践手順をまとめます。</p>
<h2 id="1-移行方針を先に決める">1. 移行方針を先に決める</h2>
<p>最初に決めるべきは「一気に切り替えるか」「ワークスペース単位で段階移行するか」です。実務では次の方針が安全です。</p>
<ol>
<li>低リスク環境（dev/sandbox）から先行</li>
<li>本番は最終フェーズで移行</li>
<li>旧TerraformとOpenTofuを一定期間並行運用</li>
<li>ロールバック手順を文書化してから実施</li>
</ol>
<p>この順序を守るだけで、移行事故の大半を避けられます。</p>
<h2 id="2-互換性の棚卸し最重要">2. 互換性の棚卸し（最重要）</h2>
<p>まずは現状のIaC資産を棚卸しします。</p>
<ul>
<li>Terraformバージョン（例: 1.5.x / 1.6.x）</li>
<li>使用provider（AWS/Azure/GCP/Kubernetes等）</li>
<li>backend（S3 + DynamoDB lock、Terraform Cloud、GCSなど）</li>
<li>moduleの参照方式（registry / git / local）</li>
<li>CI実行環境（GitHub Actions, GitLab CI, Jenkins）</li>
</ul>
<h3 id="21-依存を固定化してから移行する">2.1 依存を固定化してから移行する</h3>
<p><code>.terraform.lock.hcl</code> を必ずコミットし、providerを固定します。移行時にproviderまで同時更新すると、差分原因の切り分けが困難になります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">terraform</span> {
</span></span><span style="display:flex;"><span>  required_version <span style="color:#f92672">=</span> &#34;&gt;<span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>.<span style="color:#ae81ff">6</span>.<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">required_providers</span> {
</span></span><span style="display:flex;"><span>    aws <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      source  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hashicorp/aws&#34;</span>
</span></span><span style="display:flex;"><span>      version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 5.40&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>移行フェーズでは「ツール差分」と「provider差分」を分離してください。</p>
<h2 id="3-ローカル検証の基本手順">3. ローカル検証の基本手順</h2>
<p>OpenTofuを導入したら、既存プロジェクトで次を実行します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 現状確認</span>
</span></span><span style="display:flex;"><span>terraform version
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># OpenTofu側確認</span>
</span></span><span style="display:flex;"><span>tofu version
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 初期化（既存backendを利用）</span>
</span></span><span style="display:flex;"><span>tofu init -upgrade<span style="color:#f92672">=</span>false
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 差分確認</span>
</span></span><span style="display:flex;"><span> tofu plan -out<span style="color:#f92672">=</span>tfplan
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>plan</code> が完全一致しない場合、いきなり適用してはいけません。主な原因は以下です。</p>
<ul>
<li>providerの暗黙更新</li>
<li>data sourceの評価タイミング差</li>
<li>標準関数・型変換の微差</li>
<li>module内部のversion制約</li>
</ul>
<p>差分は「構文差分」より「実行時評価差分」で出ることが多い点に注意してください。</p>
<h2 id="4-stateを守る設計壊したら復旧が重い">4. Stateを守る設計（壊したら復旧が重い）</h2>
<p>IaC移行の本質的リスクはstateです。ここを守るために、以下を移行前チェックリストに組み込みます。</p>
<ul>
<li>state backendのバックアップ取得</li>
<li>lock機構が有効か確認（DynamoDB等）</li>
<li><code>apply</code> 権限を移行担当者に限定</li>
<li>並列実行ジョブを停止（同時apply禁止）</li>
</ul>
<h3 id="41-s3-backendの例">4.1 S3 backendの例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">terraform</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">backend</span> <span style="color:#e6db74">&#34;s3&#34;</span> {
</span></span><span style="display:flex;"><span>    bucket         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prod-iac-state&#34;</span>
</span></span><span style="display:flex;"><span>    key            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;network/prod.tfstate&#34;</span>
</span></span><span style="display:flex;"><span>    region         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ap-northeast-1&#34;</span>
</span></span><span style="display:flex;"><span>    dynamodb_table <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-locks&#34;</span>
</span></span><span style="display:flex;"><span>    encrypt        <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>OpenTofuでもこのbackend構成は継続利用できます。重要なのは「誰がいつlockするか」を運用で統制することです。</p>
<h2 id="5-cicdをopentofu対応に置換">5. CI/CDをOpenTofu対応に置換</h2>
<p>CLI切替はローカルだけでは不十分です。実運用ではCIがapply主体です。</p>
<h3 id="51-github-actions移行例">5.1 GitHub Actions移行例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">iac-plan</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull_request</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">paths</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;infra/**&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">plan</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">defaults</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">run</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">working-directory</span>: <span style="color:#ae81ff">infra</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">opentofu/setup-opentofu@v1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">tofu_version</span>: <span style="color:#ae81ff">1.8.5</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tofu init -input=false</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tofu validate</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tofu plan -input=false -no-color</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは、<code>tofu_version</code> を固定し、突然のツール更新を防ぐことです。</p>
<h3 id="52-planの品質ゲート">5.2 Planの品質ゲート</h3>
<ul>
<li><code>tofu fmt -check</code></li>
<li><code>tofu validate</code></li>
<li><code>tflint</code></li>
<li><code>checkov</code> や <code>tfsec</code> によるセキュリティ検査</li>
</ul>
<p>この4点をPRゲート化すると、移行直後の品質低下を防げます。</p>
<h2 id="6-段階移行の実践フロー">6. 段階移行の実践フロー</h2>
<p>本番で安全に移行するなら、次の順番が現実的です。</p>
<ol>
<li>dev workspaceをOpenTofuに切替</li>
<li>1週間運用し、差分・事故を記録</li>
<li>stg workspaceを切替</li>
<li>本番はメンテウィンドウで切替</li>
<li>切替後48時間は変更凍結（緊急以外apply禁止）</li>
</ol>
<p>移行で一番危険なのは「切替当日に通常の機能変更を混ぜる」ことです。必ず分離してください。</p>
<h2 id="7-よくある障害と対策">7. よくある障害と対策</h2>
<h3 id="71-plan-差分が毎回揺れる">7.1 <code>plan</code> 差分が毎回揺れる</h3>
<p>原因:</p>
<ul>
<li>data sourceに非決定的値（timestamp等）が混在</li>
<li>providerバージョン不統一</li>
</ul>
<p>対策:</p>
<ul>
<li>変動値をlocalsに隔離</li>
<li>provider lockを全環境で統一</li>
</ul>
<h3 id="72-import済み資源が再作成扱いになる">7.2 import済み資源が再作成扱いになる</h3>
<p>原因:</p>
<ul>
<li>module path変更</li>
<li>resource address変更（<code>count</code>→<code>for_each</code>）</li>
</ul>
<p>対策:</p>
<ul>
<li><code>moved</code> ブロックを使い論理移動を明示</li>
<li>必要時のみ <code>state mv</code> を計画的に実施</li>
</ul>
<h3 id="73-lock解放漏れでapplyが詰まる">7.3 lock解放漏れでapplyが詰まる</h3>
<p>原因:</p>
<ul>
<li>CI異常終了</li>
<li>手動中断</li>
</ul>
<p>対策:</p>
<ul>
<li>lock timeoutを定義</li>
<li>強制unlock手順をRunbook化</li>
<li>Slack通知でlock継続を可視化</li>
</ul>
<h2 id="8-運用runbookに必ず入れる項目">8. 運用Runbookに必ず入れる項目</h2>
<ul>
<li>OpenTofuバージョン更新手順</li>
<li>backend障害時の復旧手順</li>
<li>lock競合時の対応フロー</li>
<li>監査ログ（誰がいつapplyしたか）</li>
<li>ロールバック条件（差分件数・重大リスク判定）</li>
</ul>
<p>「ツール移行成功」は、CLIが動くことではなく、運用が回ることです。</p>
<h2 id="9-具体的な移行チェックリスト">9. 具体的な移行チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> <code>.terraform.lock.hcl</code> を全リポジトリで固定</li>
<li><input disabled="" type="checkbox"> OpenTofu版CIテンプレート作成</li>
<li><input disabled="" type="checkbox"> dev/stg/prodの順に移行計画作成</li>
<li><input disabled="" type="checkbox"> stateバックアップの自動化</li>
<li><input disabled="" type="checkbox"> lock競合監視の通知導入</li>
<li><input disabled="" type="checkbox"> apply実行権限を限定</li>
<li><input disabled="" type="checkbox"> 切替後の変更凍結ルールを明文化</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>TerraformからOpenTofuへの移行は、単純なCLI置換ではなく <strong>IaC運用基盤の再整備</strong> です。</p>
<ul>
<li>差分原因を分離して検証する</li>
<li>state保護を最優先に設計する</li>
<li>CIとRunbookをセットで更新する</li>
<li>段階移行でリスクを局所化する</li>
</ul>
<p>この4点を守れば、既存システムを止めずに移行できます。まずはdev環境で「同一plan再現性」を検証し、そこから本番へ広げるのが最短かつ安全です。</p>
<h2 id="付録-切替当日の実行チェック時系列">付録: 切替当日の実行チェック（時系列）</h2>
<p>移行当日は、作業手順よりも「順番管理」が事故防止に効きます。以下は実務で使える時系列テンプレートです。</p>
<ul>
<li><strong>T-30分</strong>: 変更凍結を全チームへ告知、未マージPRを棚卸し</li>
<li><strong>T-20分</strong>: <code>tofu version</code> と lockfile整合性確認、CI変数最終確認</li>
<li><strong>T-10分</strong>: backend到達確認、stateバックアップ取得</li>
<li><strong>T+0分</strong>: dev→stg→prodの順で <code>tofu plan</code> 実行（差分をレビュー）</li>
<li><strong>T+15分</strong>: 許容差分のみ <code>tofu apply</code> 実行、結果を監査ログへ転記</li>
<li><strong>T+30分</strong>: 主要監視（APIエラー率、レイテンシ、コスト）を15分監視</li>
</ul>
<p>さらに、切替時に最も有効なのは「中止判断ライン」を先に決めることです。例えば、<code>plan差分件数が想定の2倍以上</code> や <code>apply失敗が2連続</code> の場合は即時中断し、旧フローへ戻す、といった基準を明文化しておくと現場判断がぶれません。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>OpenTofu</category>
      <category>Terraform</category>
      <category>IaC</category>
      <category>DevOps</category>
      <category>Platform Engineering</category>
    </item>
    <item>
      <title>Kubernetes環境でDBスキーマ変更を止めずに進める：ゼロダウンタイム移行の実践戦略</title>
      <link>https://www.ai2core.com/posts/2026-03-05-kubernetes-zero-downtime-db-migration-strategy/</link>
      <pubDate>Thu, 05 Mar 2026 09:16:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-05-kubernetes-zero-downtime-db-migration-strategy/</guid>
      <description>本番KubernetesでDBマイグレーションを安全に実施するためのExpand-Contract戦略、手順、ロールバック設計を具体例つきで解説。</description>
      <content:encoded><![CDATA[<h1 id="kubernetes環境でdbスキーマ変更を止めずに進めるゼロダウンタイム移行の実践戦略">Kubernetes環境でDBスキーマ変更を止めずに進める：ゼロダウンタイム移行の実践戦略</h1>
<p>「カラムを追加するだけだから大丈夫」──この油断が、本番障害の入口になります。Kubernetes のように複数バージョンの Pod が同時に存在する環境では、DB スキーマ変更はアプリ変更よりも慎重に扱う必要があります。</p>
<p>本記事では、<strong>Expand-Contract パターン</strong>を中心に、ゼロダウンタイムを目指すための具体手順を解説します。実際の運用では、DDLの速さより「互換性のある期間をどう作るか」が勝負です。</p>
<h2 id="1-なぜkubernetesでdb移行が難しいのか">1. なぜKubernetesでDB移行が難しいのか</h2>
<p>Kubernetesでは、ローリングアップデート中に新旧Podが混在します。つまり次の状態が同時に発生します。</p>
<ul>
<li>新アプリは新スキーマを期待</li>
<li>旧アプリは旧スキーマしか知らない</li>
<li>DBは1つしかない</li>
</ul>
<p>このとき破綻するのが「破壊的変更を先に適用する」ケースです。たとえば旧カラムを即削除すると、旧Podがエラーを連発します。</p>
<h2 id="2-基本戦略expand--migrate--contract">2. 基本戦略：Expand → Migrate → Contract</h2>
<p>ゼロダウンタイム移行の原則はこの3段階です。</p>
<ol>
<li><strong>Expand</strong>: 互換性を壊さない変更を先に入れる（新カラム追加など）</li>
<li><strong>Migrate</strong>: アプリを段階的に切替え、データを移行する</li>
<li><strong>Contract</strong>: 旧仕様を最終削除する（十分な監視後）</li>
</ol>
<p>この順序なら、どの時点でも旧新どちらのアプリも動作可能にできます。</p>
<h2 id="3-具体例usersfull_name-を-first_name--last_name-へ分割">3. 具体例：<code>users.full_name</code> を <code>first_name</code> / <code>last_name</code> へ分割</h2>
<h3 id="31-expand-フェーズ">3.1 Expand フェーズ</h3>
<p>まず破壊的でないDDLを適用します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> users <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> first_name text;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> users <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> last_name text;
</span></span></code></pre></td></tr></table>
</div>
</div><p>この時点で旧アプリは <code>full_name</code> を使い続けられます。新アプリは新カラムに対応した実装を持っていても、まだ必須にしません。</p>
<h3 id="32-アプリを両対応にする">3.2 アプリを「両対応」にする</h3>
<p>書き込み時は両方へ保存（dual write）し、読み込み時は新カラム優先 + 旧カラムフォールバックにします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">save_user_name</span>(user_id: str, full_name: str):
</span></span><span style="display:flex;"><span>    first, last <span style="color:#f92672">=</span> split_name(full_name)
</span></span><span style="display:flex;"><span>    db<span style="color:#f92672">.</span>execute(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        UPDATE users
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        SET full_name = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            first_name = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            last_name = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        WHERE id = </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>        (full_name, first, last, user_id),
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">read_user_display_name</span>(row):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> row<span style="color:#f92672">.</span>first_name <span style="color:#f92672">and</span> row<span style="color:#f92672">.</span>last_name:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>row<span style="color:#f92672">.</span>first_name<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>row<span style="color:#f92672">.</span>last_name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> row<span style="color:#f92672">.</span>full_name
</span></span></code></pre></td></tr></table>
</div>
</div><p>この両対応期間を作るのが、ゼロダウンタイムの本質です。</p>
<h3 id="33-バックフィル既存データ移行">3.3 バックフィル（既存データ移行）</h3>
<p>大規模テーブルでは一括更新を避け、チャンク更新します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- 疑似コードイメージ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> users
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SET</span> first_name <span style="color:#f92672">=</span> split_part(full_name, <span style="color:#e6db74">&#39; &#39;</span>, <span style="color:#ae81ff">1</span>),
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> split_part(full_name, <span style="color:#e6db74">&#39; &#39;</span>, <span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">&gt;</span> :last_id
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> id <span style="color:#f92672">&lt;=</span> :last_id <span style="color:#f92672">+</span> <span style="color:#ae81ff">10000</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> (first_name <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> last_name <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>ジョブ実装時のポイント:</p>
<ul>
<li>1チャンクごとに commit</li>
<li>再開可能なチェックポイント（last_id）を保存</li>
<li>実行時間帯を制御（ピーク時間回避）</li>
</ul>
<h3 id="34-contract-フェーズ">3.4 Contract フェーズ</h3>
<p>全Podが新実装になり、フォールバックが不要と判断できたら旧カラム削除へ進みます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> users <span style="color:#66d9ef">DROP</span> <span style="color:#66d9ef">COLUMN</span> full_name;
</span></span></code></pre></td></tr></table>
</div>
</div><p>削除は必ず最後です。ここを急ぐとロールバック不能になります。</p>
<h2 id="4-マイグレーション実行方式の選び方">4. マイグレーション実行方式の選び方</h2>
<p>Kubernetesでは主に3パターンがあります。</p>
<h3 id="a-cicdで先行実行推奨">A. CI/CDで先行実行（推奨）</h3>
<p>デプロイ前に migration Job を走らせ、成功後にアプリを更新。</p>
<ul>
<li>メリット: 実行順序を固定しやすい</li>
<li>デメリット: 長時間 migration の扱いが難しい</li>
</ul>
<h3 id="b-initcontainer-実行">B. initContainer 実行</h3>
<p>Pod起動時に migration を走らせる方式。</p>
<ul>
<li>メリット: 実装が単純</li>
<li>デメリット: 複数Pod同時起動で競合しやすい</li>
</ul>
<h3 id="c-専用-migration-controller--job">C. 専用 Migration Controller / Job</h3>
<p>Argo Workflows などで明示的に管理。</p>
<ul>
<li>メリット: 大規模運用で監査しやすい</li>
<li>デメリット: 初期構築コストが高い</li>
</ul>
<p>中規模までなら A が最も事故が少ないです。</p>
<h2 id="5-alembicflyway運用の実務ポイント">5. Alembic/Flyway運用の実務ポイント</h2>
<h3 id="51-1-migration--1責務">5.1 1 migration = 1責務</h3>
<p>「追加 + データ移行 + 削除」を1ファイルに詰め込むと失敗時に戻しづらくなります。Expand/Migrate/Contract を別 migration に分けて、各段階を検証可能にしてください。</p>
<h3 id="52-lock-timeout-を設定する">5.2 lock timeout を設定する</h3>
<p>DDL はロック待ちでアプリを止めることがあります。PostgreSQL なら <code>lock_timeout</code> を設定して、危険な待機を避けます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SET</span> lock_timeout <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;5s&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> ...;
</span></span></code></pre></td></tr></table>
</div>
</div><p>失敗時は即中断し、メンテナンス時間帯に再実行する判断が可能です。</p>
<h3 id="53-破壊的変更前に計測窓を設ける">5.3 破壊的変更前に計測窓を設ける</h3>
<p>旧カラム参照がゼロになったことを、メトリクスやログで一定期間確認してから削除します。目安として 7〜14日程度の観測期間を取ると安全です。</p>
<h2 id="6-ロールバック戦略">6. ロールバック戦略</h2>
<p>ゼロダウンタイム設計では、ロールバック可能性を最初に決めます。</p>
<ul>
<li>Expand段階: ほぼ即ロールバック可能</li>
<li>Migrate段階: dual write を維持していればロールバック可能</li>
<li>Contract段階: 削除後は復元コスト高（バックアップ依存）</li>
</ul>
<p>したがって、Contract 実施前に以下を満たす必要があります。</p>
<ul>
<li>直近スナップショット取得済み</li>
<li>復元手順を演習済み</li>
<li>影響範囲（API・バッチ・BI）を棚卸し済み</li>
</ul>
<h2 id="7-監視項目と成功判定">7. 監視項目と成功判定</h2>
<p>移行中は「成功したか」より「安全に進んでいるか」を見ます。</p>
<ul>
<li>APIエラー率（4xx/5xx）</li>
<li>DBロック待ち時間</li>
<li>スロークエリ件数</li>
<li>migration ジョブ進捗（処理済み件数、残件数）</li>
<li>旧カラム参照回数</li>
</ul>
<p>成功判定の例:</p>
<ol>
<li>新旧Pod混在中のエラー率に有意な悪化なし</li>
<li>バックフィル完了率100%</li>
<li>旧カラム参照0が連続7日</li>
<li>Contract後24時間で異常なし</li>
</ol>
<h2 id="8-よくある失敗と回避策">8. よくある失敗と回避策</h2>
<h3 id="失敗1-非null制約を早く付けすぎる">失敗1: 非NULL制約を早く付けすぎる</h3>
<p>バックフィル完了前に <code>NOT NULL</code> を付けると、旧データで失敗します。まずは nullable で追加し、データ移行後に制約追加が正解です。</p>
<h3 id="失敗2-インデックス作成で書き込み停止">失敗2: インデックス作成で書き込み停止</h3>
<p>大きいテーブルで通常インデックス作成を行うとロックが重くなります。PostgreSQL では <code>CREATE INDEX CONCURRENTLY</code> を使って影響を下げます。</p>
<h3 id="失敗3-migration-の実行主体が複数">失敗3: migration の実行主体が複数</h3>
<p>同時に2つのJobが走ると競合します。Kubernetes Job は排他制御（Lease/Lock）を持たせるか、CI側で単一実行を保証してください。</p>
<h2 id="9-実運用テンプレートmigration-job-と段階リリース">9. 実運用テンプレート：Migration Job と段階リリース</h2>
<p>最後に、現場でそのまま使える最小テンプレートを示します。ポイントは「DDLを一気にやらない」「アプリ側フラグで読取切替を制御する」の2点です。</p>
<h3 id="91-migration-jobexpand専用">9.1 Migration Job（Expand専用）</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">batch/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Job</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">users-expand-20260305</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">backoffLimit</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">restartPolicy</span>: <span style="color:#ae81ff">Never</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">migrate</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">ghcr.io/example/api:1.42.0</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">command</span>: [<span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-lc&#34;</span>, <span style="color:#e6db74">&#34;alembic upgrade 20260305_expand_users_name&#34;</span>]
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">envFrom</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">secretRef</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-db-secret</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Expand 用 Job を分離しておけば、失敗時にアプリデプロイを止める判断が明確になります。<code>backoffLimit: 0</code> にして「失敗を隠さない」運用にするのも実務で有効です。</p>
<h3 id="92-段階リリース手順例">9.2 段階リリース手順（例）</h3>
<ol>
<li>Expand Job 実行（DDLのみ）</li>
<li>アプリ v1（dual write + fallback read）を10%配信</li>
<li>エラー率・ロック待ちを30分監視</li>
<li>50% → 100%へ段階拡大</li>
<li>バックフィル Job 実行</li>
<li>旧参照ゼロ確認後に Contract 実施</li>
</ol>
<p>Kubernetes では <code>kubectl rollout status deployment/api -n prod</code> を必ず監視に組み込み、配信完了判定を人間が明示的に確認する運用が安全です。</p>
<h2 id="まとめ">まとめ</h2>
<p>KubernetesでのDBマイグレーションは、DDLテクニックだけでは成功しません。重要なのは、</p>
<ul>
<li>互換性期間を意図的に作る</li>
<li>段階を分けて進める</li>
<li>ロールバック可能性を先に設計する</li>
<li>監視で「削除してよい」根拠を取る</li>
</ul>
<p>この4点です。Expand-Contract を守るだけで、移行の失敗率は目に見えて下がります。スキーマ変更は怖い作業ですが、手順化すれば再現可能な運用にできます。次回の変更から、ぜひこの流れで試してみてください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>Database</category>
      <category>Migration</category>
      <category>SRE</category>
      <category>DevOps</category>
    </item>
    <item>
      <title>GitHub Actions高速化実践：Matrix戦略・依存キャッシュ・失敗切り分けの設計ガイド</title>
      <link>https://www.ai2core.com/posts/2026-03-04-github-actions-matrix-cache-strategy/</link>
      <pubDate>Wed, 04 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-04-github-actions-matrix-cache-strategy/</guid>
      <description>GitHub Actionsの実行時間と失敗率を同時に改善するためのmatrix設計、キャッシュ戦略、並列最適化、トラブルシューティング手順を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions高速化実践matrix戦略依存キャッシュ失敗切り分けの設計ガイド">GitHub Actions高速化実践：Matrix戦略・依存キャッシュ・失敗切り分けの設計ガイド</h1>
<p>GitHub Actions は便利ですが、プロジェクトが成長すると「遅い」「不安定」「原因が分かりにくい」という三重苦になりがちです。特に monorepo や複数ランタイム対応（Node/Python/Go など）では、ワークフローの設計次第で CI 時間が 2〜3 倍変わります。</p>
<p>本記事では、<strong>実行時間を短くしながら失敗時の調査コストも下げる</strong>ために、matrix 設計・キャッシュ設計・障害時の確認順序を具体的に整理します。</p>
<h2 id="1-まず何を並列化するかを決める">1. まず「何を並列化するか」を決める</h2>
<p>Actions の高速化は、いきなりキャッシュ最適化から入るより、先にジョブ分解を決める方が効きます。原則は次の通りです。</p>
<ul>
<li>並列化すべき: 独立テスト（OS/バージョン別、サービス別）</li>
<li>直列にすべき: デプロイ、DB マイグレーション、本番反映</li>
<li>依存を分ける: lint/typecheck/test/build を一つに詰め込まない</li>
</ul>
<p>悪い例は、1ジョブに全部詰め込み、失敗時に最初から再実行するパターンです。良い設計では「lint は通るが test だけ失敗」のように切り分けできます。</p>
<h2 id="2-matrix-を作るときの実践ルール">2. matrix を作るときの実践ルール</h2>
<p>matrix は便利ですが、組み合わせ爆発で逆に遅くなることがあります。例えば <code>os x runtime x db</code> をすべて直積にすると、不要なジョブが大量発生します。そこで <code>include/exclude</code> を活用します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">strategy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">fail-fast</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matrix</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">os</span>: [<span style="color:#ae81ff">ubuntu-latest, macos-latest]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">node</span>: [<span style="color:#ae81ff">20</span>, <span style="color:#ae81ff">22</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">include</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">os</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">node</span>: <span style="color:#ae81ff">22</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">coverage</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">exclude</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">os</span>: <span style="color:#ae81ff">macos-latest</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">node</span>: <span style="color:#ae81ff">20</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは次です。</p>
<ol>
<li><strong>基準環境を1つ決める</strong>（例: ubuntu + latest）</li>
<li>カバレッジ計測や重い E2E は基準環境だけで実施</li>
<li>互換性確認は軽量テスト中心にする</li>
</ol>
<p>この設計にすると、品質を落とさずに全体時間を短縮できます。</p>
<h2 id="3-キャッシュは鍵設計が9割">3. キャッシュは「鍵設計」が9割</h2>
<p><code>actions/cache</code> や <code>setup-node</code> / <code>setup-python</code> のキャッシュを入れても、キー設計が甘いとヒット率が低く、逆に復元時間だけ増えます。</p>
<p>Node.js の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-node@v4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">node-version</span>: <span style="color:#ae81ff">${{ matrix.node }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#e6db74">&#39;npm&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache-dependency-path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      package-lock.json
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      packages/*/package-lock.json</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Python (pip) の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-python@v5</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">python-version</span>: <span style="color:#e6db74">&#39;3.12&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#e6db74">&#39;pip&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache-dependency-path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      requirements.txt
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      requirements-dev.txt</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>実務で効くコツ:</p>
<ul>
<li>lockfile をキーに含める（依存変化に追従）</li>
<li>OS・ランタイムバージョンをキーに含める</li>
<li>monorepo は対象サブディレクトリ単位でキー分割</li>
<li>restore-keys を入れすぎない（古いキャッシュ復元で不整合）</li>
</ul>
<h2 id="4-concurrency-で古い実行を止める">4. concurrency で「古い実行を止める」</h2>
<p>PR に連続 push されると、古い CI が残り続けてランナー枯渇を起こします。<code>concurrency</code> を入れて、最新コミットだけ走らせる構成にします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">concurrency</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">group</span>: <span style="color:#ae81ff">ci-${{ github.workflow }}-${{ github.ref }}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cancel-in-progress</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これだけで無駄実行を大きく減らせます。特にレビュー中に細かい修正を重ねるチームほど効果が高いです。</p>
<h2 id="5-paths-filter-で不要ジョブを起動しない">5. paths-filter で不要ジョブを起動しない</h2>
<p>ドキュメント更新だけなのに全テストが走る、という状態はよくあります。<code>dorny/paths-filter</code> で変更範囲に応じてジョブを分岐します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">dorny/paths-filter@v3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id</span>: <span style="color:#ae81ff">changes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">filters</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      backend:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        - &#39;backend/**&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      frontend:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        - &#39;frontend/**&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 例: backend が変わった時だけ実行</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">if</span>: <span style="color:#ae81ff">steps.changes.outputs.backend == &#39;true&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これにより、実行時間だけでなくランナーコストも下げられます。</p>
<h2 id="6-失敗時の調査を速くするログ設計">6. 失敗時の調査を速くするログ設計</h2>
<p>CI が遅い組織は、だいたい「失敗調査も遅い」です。改善するには、次の3点を標準化します。</p>
<ul>
<li>失敗したジョブで artifact（ログ、スクリーンショット、coverage）を必ず保存</li>
<li>重要ステップに <code>::group::</code> を付けてログを畳む</li>
<li>flaky テスト検出用に rerun 情報を残す</li>
</ul>
<p>artifact 例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload test reports</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">if</span>: <span style="color:#ae81ff">always()</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-artifact@v4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">test-report-${{ matrix.os }}-${{ matrix.node }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      reports/
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      coverage/</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>if: always()</code> を忘れると、失敗時ほど証跡が残らないので注意です。</p>
<h2 id="7-self-hosted-runner-を使う場合の注意点">7. self-hosted runner を使う場合の注意点</h2>
<p>高速化目的で self-hosted runner を導入する場合、運用事故が増えやすい領域です。</p>
<ul>
<li>毎回クリーンワークスペース化（残骸で再現不能バグ）</li>
<li>シークレットを runner に永続化しない</li>
<li>パッチ適用・再起動の定期メンテをスケジュール化</li>
<li>runner ラベルを用途別に分離（deploy と test を混在させない）</li>
</ul>
<p>また、デプロイ権限を持つ runner と、PR 由来コードを実行する runner は分離するのが基本です。</p>
<h2 id="8-実際の改善ステップ2週間">8. 実際の改善ステップ（2週間）</h2>
<h3 id="day-1-2-現状計測">Day 1-2: 現状計測</h3>
<ul>
<li>平均実行時間、p95 実行時間、失敗率を取得</li>
<li>一番遅いジョブ上位3つを特定</li>
</ul>
<h3 id="day-3-5-ジョブ分割と-matrix-整理">Day 3-5: ジョブ分割と matrix 整理</h3>
<ul>
<li>lint/typecheck/test/build を分離</li>
<li>matrix の組み合わせを include/exclude で整理</li>
</ul>
<h3 id="day-6-8-キャッシュ最適化">Day 6-8: キャッシュ最適化</h3>
<ul>
<li>lockfile ベースキーへ統一</li>
<li>キャッシュヒット率を可視化</li>
</ul>
<h3 id="day-9-10-無駄実行削減">Day 9-10: 無駄実行削減</h3>
<ul>
<li>concurrency + cancel-in-progress</li>
<li>paths-filter で対象限定</li>
</ul>
<h3 id="day-11-14-運用ルール化">Day 11-14: 運用ルール化</h3>
<ul>
<li>失敗時 artifact を全ジョブ標準化</li>
<li>flaky テスト記録のテンプレート化</li>
</ul>
<p>この手順で進めると、速度改善だけでなく再発防止まで一気に整います。</p>
<h2 id="9-よくある失敗パターン">9. よくある失敗パターン</h2>
<h3 id="パターンa-キャッシュを入れたのに遅い">パターンA: キャッシュを入れたのに遅い</h3>
<p>原因:</p>
<ul>
<li>キーが細かすぎて毎回ミスヒット</li>
<li>圧縮/復元コストが大きいディレクトリを丸ごとキャッシュ</li>
</ul>
<p>対処:</p>
<ul>
<li>依存に限定してキャッシュ</li>
<li>キーに lockfile ハッシュを利用</li>
</ul>
<h3 id="パターンb-matrix-失敗がノイズ化">パターンB: matrix 失敗がノイズ化</h3>
<p>原因:</p>
<ul>
<li><code>fail-fast: true</code> で他環境の情報が取れない</li>
<li>ログ命名が統一されず比較困難</li>
</ul>
<p>対処:</p>
<ul>
<li><code>fail-fast: false</code></li>
<li>artifact 命名規則を統一</li>
</ul>
<h3 id="パターンc-pr-の待ち時間が長い">パターンC: PR の待ち時間が長い</h3>
<p>原因:</p>
<ul>
<li>古いコミットの CI が走り続ける</li>
<li>変更範囲に関係ないジョブが常時起動</li>
</ul>
<p>対処:</p>
<ul>
<li>concurrency で古い実行を停止</li>
<li>paths-filter 導入</li>
</ul>
<h2 id="10-運用チェックリスト">10. 運用チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> ジョブは責務別に分離されている</li>
<li><input disabled="" type="checkbox"> matrix は必要最小限に絞られている</li>
<li><input disabled="" type="checkbox"> 依存キャッシュのキーに lockfile が含まれる</li>
<li><input disabled="" type="checkbox"> concurrency で古い実行をキャンセルしている</li>
<li><input disabled="" type="checkbox"> 変更範囲に応じたジョブ起動制御がある</li>
<li><input disabled="" type="checkbox"> 失敗時 artifact が必ず残る</li>
</ul>
<p>GitHub Actions は「機能を使う」だけでは速くなりません。<strong>実行単位の設計、キャッシュ鍵設計、無駄実行抑制、証跡設計</strong>をセットで行うと、初めて安定した CI 基盤になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>CI</category>
      <category>DevOps</category>
      <category>Node.js</category>
      <category>Python</category>
    </item>
    <item>
      <title>Terraformドリフト検知プレイブック：本番事故を防ぐCI設計と運用手順</title>
      <link>https://www.ai2core.com/posts/2026-03-03-terraform-drift-detection-playbook/</link>
      <pubDate>Tue, 03 Mar 2026 09:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-03-terraform-drift-detection-playbook/</guid>
      <description>Terraformのドリフト検知を実運用に載せるための設計指針、GitHub Actions実装例、誤検知の減らし方、復旧フローまで具体的に解説。</description>
      <content:encoded><![CDATA[<h1 id="terraformドリフト検知プレイブック本番事故を防ぐci設計と運用手順">Terraformドリフト検知プレイブック：本番事故を防ぐCI設計と運用手順</h1>
<p>Terraform を導入していても、運用が進むほど「実環境がいつの間にかコードとズレる」問題にぶつかります。いわゆるドリフトです。最初は小さな差分でも、放置すると本番変更時に予期せぬ差分が混ざり、障害やリリース遅延の原因になります。</p>
<p>本記事では、Terraform ドリフト検知を単なる <code>terraform plan</code> 実行で終わらせず、<strong>継続運用できる仕組み</strong>として実装するための具体策をまとめます。対象は AWS を例にしますが、考え方は他クラウドでも共通です。</p>
<h2 id="1-ドリフト検知で最初に決めるべきこと">1. ドリフト検知で最初に決めるべきこと</h2>
<p>多くのチームが失敗するのは、実装前に運用設計を決めないことです。まず以下を決めます。</p>
<ol>
<li>どの環境をいつ検知するか（prod は毎日、stg は平日など）</li>
<li>検知結果をどこに通知するか（Slack/Discord/Issue）</li>
<li>誰がいつまでに対応するか（当番制、SLA）</li>
<li>「意図した手動変更」をどう扱うか（例外ラベル、期限付き）</li>
</ol>
<p>ここを決めずに CI だけ作ると、通知がノイズ化して無視されます。ドリフト検知は技術課題より運用課題です。</p>
<h2 id="2-リポジトリ構成と-state-分離">2. リポジトリ構成と state 分離</h2>
<p>最小限、次のような構成を推奨します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>infra/
</span></span><span style="display:flex;"><span>  modules/
</span></span><span style="display:flex;"><span>    vpc/
</span></span><span style="display:flex;"><span>    ecs/
</span></span><span style="display:flex;"><span>    rds/
</span></span><span style="display:flex;"><span>  envs/
</span></span><span style="display:flex;"><span>    prod/
</span></span><span style="display:flex;"><span>      main.tf
</span></span><span style="display:flex;"><span>      backend.hcl
</span></span><span style="display:flex;"><span>      variables.tf
</span></span><span style="display:flex;"><span>    stg/
</span></span><span style="display:flex;"><span>      main.tf
</span></span><span style="display:flex;"><span>      backend.hcl
</span></span><span style="display:flex;"><span>.github/
</span></span><span style="display:flex;"><span>  workflows/
</span></span><span style="display:flex;"><span>    terraform-drift.yml
</span></span></code></pre></td></tr></table>
</div>
</div><p>環境ごとに backend と state を分けることが重要です。ドリフト検知ジョブが state を誤って参照すると、存在しない差分が出ます。S3 backend + DynamoDB lock を使う場合は、<code>bucket/key/region/table</code> の整合性を必ず固定化します。</p>
<h2 id="3-ci-での検知フローgithub-actions">3. CI での検知フロー（GitHub Actions）</h2>
<p>以下は、毎朝実行してドリフトを検知する最小構成です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">terraform-drift</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">schedule</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">cron</span>: <span style="color:#e6db74">&#34;0 22 * * *&#34;</span> <span style="color:#75715e"># JST 07:00</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">drift-prod</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">defaults</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">run</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">working-directory</span>: <span style="color:#ae81ff">infra/envs/prod</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">id-token</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">contents</span>: <span style="color:#ae81ff">read</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">hashicorp/setup-terraform@v3</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">terraform_version</span>: <span style="color:#ae81ff">1.9.8</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configure AWS credentials (OIDC)</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">aws-actions/configure-aws-credentials@v4</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">role-to-assume</span>: <span style="color:#ae81ff">arn:aws:iam::123456789012:role/github-terraform-readonly</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">aws-region</span>: <span style="color:#ae81ff">ap-northeast-1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Init</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">terraform init -backend-config=backend.hcl</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Drift check</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">terraform plan -detailed-exitcode -out=tfplan</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>-detailed-exitcode</code> は実務で必須です。終了コードの意味は以下です。</p>
<ul>
<li>0: 差分なし</li>
<li>1: エラー</li>
<li>2: 差分あり（ドリフト含む）</li>
</ul>
<p>これを使えば、差分ありと失敗を明確に分けられます。</p>
<h2 id="4-終了コードを正しく扱う">4. 終了コードを正しく扱う</h2>
<p>Actions で単純に <code>terraform plan</code> を実行すると、終了コード 2 が failure 扱いになるため、実際には「検知成功」なのに赤く見えます。以下のように分岐して扱います。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>set +e
</span></span><span style="display:flex;"><span>terraform plan -detailed-exitcode -out<span style="color:#f92672">=</span>tfplan
</span></span><span style="display:flex;"><span>code<span style="color:#f92672">=</span>$?
</span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$code<span style="color:#e6db74">&#34;</span> -eq <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;DRIFT=false&#34;</span> &gt;&gt; $GITHUB_ENV
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$code<span style="color:#e6db74">&#34;</span> -eq <span style="color:#ae81ff">2</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;DRIFT=true&#34;</span> &gt;&gt; $GITHUB_ENV
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Terraform plan failed&#34;</span>
</span></span><span style="display:flex;"><span>  exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この実装にすると、ジョブ自体は成功させたまま「ドリフト有無」を後段に渡せます。</p>
<h2 id="5-通知設計差分サマリを人間が読める形にする">5. 通知設計：差分サマリを人間が読める形にする</h2>
<p>ドリフト検知で最も大事なのは、通知が読みやすいことです。plan 全文を貼ると誰も読みません。次の 3 つだけ通知します。</p>
<ol>
<li>どの環境で</li>
<li>何が（resource type + name）</li>
<li>どう変わるか（create/update/delete）</li>
</ol>
<p>例: <code>terraform show -json tfplan</code> を <code>jq</code> で要約</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>terraform show -json tfplan &gt; plan.json
</span></span><span style="display:flex;"><span>jq -r <span style="color:#e6db74">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  .resource_changes[] |
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;- \(.type).\(.name): \(.change.actions | join(&#34;,&#34;))&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;</span> plan.json | head -n <span style="color:#ae81ff">50</span> &gt; summary.txt
</span></span></code></pre></td></tr></table>
</div>
</div><p>通知文は長すぎると切れるため、冒頭 50 件だけに制限し、全文は artifacts に添付する運用が実践的です。</p>
<h2 id="6-誤検知を減らす具体策">6. 誤検知を減らす具体策</h2>
<p>ドリフト検知の信頼性を下げる典型例は次です。</p>
<ul>
<li><code>timestamp</code> 的な値を管理対象にしている</li>
<li>provider バージョン差で毎回微差分が出る</li>
<li>autoscaling 系設定が外部コントローラで更新される</li>
</ul>
<p>対策として、<code>lifecycle { ignore_changes = [...] }</code> を乱用せず、<strong>変化が許容される属性だけ</strong>を限定的に無視します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_ecs_service&#34; &#34;app&#34;</span> {
</span></span><span style="display:flex;"><span>  name            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;app&#34;</span>
</span></span><span style="display:flex;"><span>  cluster         <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_ecs_cluster</span>.<span style="color:#66d9ef">main</span>.<span style="color:#66d9ef">id</span>
</span></span><span style="display:flex;"><span>  task_definition <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_ecs_task_definition</span>.<span style="color:#66d9ef">app</span>.<span style="color:#66d9ef">arn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">lifecycle</span> {
</span></span><span style="display:flex;"><span>    ignore_changes <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">desired_count</span>
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>ignore_changes</code> は便利ですが、広げすぎると本当に危険なドリフトを見逃します。四半期ごとに見直しを入れてください。</p>
<h2 id="7-復旧フロー検知後に迷わない手順">7. 復旧フロー：検知後に迷わない手順</h2>
<p>ドリフトを検知したら、毎回議論しないように Runbook 化します。</p>
<h3 id="ステップa-変更の意図確認">ステップA: 変更の意図確認</h3>
<ul>
<li>直近の障害対応や緊急作業がなかったか</li>
<li>コンソール手動変更のチケットがあるか</li>
</ul>
<h3 id="ステップb-差分分類">ステップB: 差分分類</h3>
<ul>
<li>許容（意図あり）</li>
<li>要コード反映（意図あり、IaC未反映）</li>
<li>要即時修正（意図なし）</li>
</ul>
<h3 id="ステップc-実行方針">ステップC: 実行方針</h3>
<ul>
<li>IaC 正とする場合: コード更新 → PR → apply</li>
<li>実環境正としない場合: 手動変更を戻す、または Terraform apply で整合</li>
</ul>
<p>このフローを当番が 30 分以内に回せるようにすると、検知の価値が跳ね上がります。</p>
<h2 id="8-iam-最小権限の実例">8. IAM 最小権限の実例</h2>
<p>ドリフト検知専用ロールは read-only を原則にします。<code>plan</code> だけなら多くのサービスで読み取り権限で足ります。apply 権限を同じロールに入れると、漏えい時のリスクが大きくなります。</p>
<ul>
<li><code>github-terraform-readonly</code>: drift check 用</li>
<li><code>github-terraform-apply</code>: manual approval 後に限定実行</li>
</ul>
<p>この分離は監査対応でも説明しやすく、セキュリティレビューで通りやすいです。</p>
<h2 id="9-週次レポートで改善を回す">9. 週次レポートで改善を回す</h2>
<p>検知を入れたら終わりではありません。週次で次を見ます。</p>
<ul>
<li>ドリフト検知件数</li>
<li>うち誤検知件数</li>
<li>MTTR（検知から解消まで）</li>
<li>原因カテゴリ（手動運用、自動スケール、設定ミス）</li>
</ul>
<p>誤検知率が 30% を超えるなら通知設計か ignore ポリシーが過剰です。逆に検知ゼロが長すぎる場合は、ジョブ自体が死んでいる可能性もあります。</p>
<h2 id="10-導入チェックリスト">10. 導入チェックリスト</h2>
<p>最後に、導入時のチェックリストを置いておきます。</p>
<ul>
<li><input disabled="" type="checkbox"> env ごとに backend/state 分離済み</li>
<li><input disabled="" type="checkbox"> <code>terraform plan -detailed-exitcode</code> を採用</li>
<li><input disabled="" type="checkbox"> 終了コード 2 を正常系として処理</li>
<li><input disabled="" type="checkbox"> 通知は summary + artifacts の二段構成</li>
<li><input disabled="" type="checkbox"> read-only ロールで drift check を実行</li>
<li><input disabled="" type="checkbox"> 復旧 Runbook を 1 ページ化</li>
<li><input disabled="" type="checkbox"> 週次で誤検知率と MTTR をレビュー</li>
</ul>
<p>Terraform ドリフト検知は、単なる監視ではなく「IaC の信頼性を維持する運用基盤」です。最初から完璧を狙うより、通知品質と対応フローを小さく回して改善する方が、結果的に強い運用になります。</p>
<h2 id="11-すぐ使える運用コマンド当番向け">11. すぐ使える運用コマンド（当番向け）</h2>
<p>最後に、当番が迷わず実行できる最小コマンドをまとめます。どれも「まず状況把握」を優先する順序です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1) 直近のドリフトジョブ確認（GitHub CLI）</span>
</span></span><span style="display:flex;"><span>gh run list --workflow terraform-drift.yml --limit <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2) 失敗/差分あり実行のログ確認</span>
</span></span><span style="display:flex;"><span>gh run view &lt;run-id&gt; --log
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3) 対象環境で再現確認（ローカル）</span>
</span></span><span style="display:flex;"><span>cd infra/envs/prod
</span></span><span style="display:flex;"><span>terraform init -backend-config<span style="color:#f92672">=</span>backend.hcl
</span></span><span style="display:flex;"><span>terraform plan -detailed-exitcode
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 4) 差分要約を作成</span>
</span></span><span style="display:flex;"><span>terraform plan -out<span style="color:#f92672">=</span>tfplan
</span></span><span style="display:flex;"><span>terraform show -json tfplan | jq -r <span style="color:#e6db74">&#39;.resource_changes[] | &#34;- \(.type).\(.name): \(.change.actions | join(&#34;,&#34;))&#34;&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この 4 ステップを運用手順書の先頭に置くだけで、一次対応の速度と品質が安定します。特に「誰が見ても同じ順序で確認できる」状態を作ることが、夜間障害時の認知負荷を大きく下げます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Terraform</category>
      <category>IaC</category>
      <category>DevOps</category>
      <category>SRE</category>
      <category>CI/CD</category>
    </item>
    <item>
      <title>GitHub Actions再利用ワークフロー運用設計：属人化を防ぎつつ開発速度を上げる実践ガイド</title>
      <link>https://www.ai2core.com/posts/2026-03-02-github-actions-reusable-workflows-governance/</link>
      <pubDate>Mon, 02 Mar 2026 09:08:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-02-github-actions-reusable-workflows-governance/</guid>
      <description>GitHub Actionsの再利用ワークフローを本番運用するための設計・権限管理・移行手順を、具体的なYAML例とチェックリスト付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions再利用ワークフロー運用設計属人化を防ぎつつ開発速度を上げる実践ガイド">GitHub Actions再利用ワークフロー運用設計：属人化を防ぎつつ開発速度を上げる実践ガイド</h1>
<p>複数リポジトリを運用していると、ほぼ同じ CI 設定を各リポジトリにコピーし続ける状態になりがちです。最初は早く見えますが、半年後には「どこに正解があるのかわからない」状態になります。セキュリティパッチを当てたいだけなのに 20 リポジトリを横断修正し、1つだけ取りこぼして監査で指摘される、というのは珍しくありません。</p>
<p>この問題に効くのが GitHub Actions の <code>workflow_call</code> を使った再利用ワークフローです。ただし、単に共通化するだけでは逆に運用事故が増えることがあります。重要なのは、<strong>共通化の粒度、権限境界、変更リリース方法</strong>を最初に設計することです。</p>
<p>本記事では、実運用で詰まりやすいポイントを中心に、導入から定着までを具体的に解説します。</p>
<h2 id="1-再利用ワークフローの基本方針">1. 再利用ワークフローの基本方針</h2>
<p>まず押さえるべき方針は次の 3 つです。</p>
<ol>
<li><strong>再利用ワークフローは「プラットフォーム製品」として扱う</strong></li>
<li><strong>呼び出し側（各アプリ repo）は薄く保つ</strong></li>
<li><strong>破壊的変更はバージョンを切って段階移行する</strong></li>
</ol>
<p>「共通化＝1ファイルに全部詰め込む」ではありません。lint, test, build, deploy を1個にまとめると、対象外プロジェクトまで影響します。まずは小さく分割し、必要なものだけ組み合わせられる構造にします。</p>
<h2 id="2-推奨ディレクトリ構成">2. 推奨ディレクトリ構成</h2>
<p>共通ワークフローを専用 repo に分離しておくと、監査や変更履歴の管理が容易になります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>org-ci-workflows/
</span></span><span style="display:flex;"><span>  .github/workflows/
</span></span><span style="display:flex;"><span>    ci-node.yml
</span></span><span style="display:flex;"><span>    ci-python.yml
</span></span><span style="display:flex;"><span>    security-scan.yml
</span></span><span style="display:flex;"><span>    release-tag.yml
</span></span><span style="display:flex;"><span>  docs/
</span></span><span style="display:flex;"><span>    onboarding.md
</span></span><span style="display:flex;"><span>    migration-checklist.md
</span></span></code></pre></td></tr></table>
</div>
</div><p>呼び出し側は以下のように最小化します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">ci</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull_request</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#ae81ff">main]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">node-ci</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">your-org/org-ci-workflows/.github/workflows/ci-node.yml@v1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">node-version</span>: <span style="color:#e6db74">&#34;22&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">run-e2e</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">secrets</span>: <span style="color:#ae81ff">inherit</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この形の利点は、アプリチームが理解すべき YAML が減ることです。CI の標準知識が全員に伝播し、レビューも速くなります。</p>
<h2 id="3-実務で効く入力設計inputs">3. 実務で効く入力設計（inputs）</h2>
<p><code>inputs</code> は API 設計だと考えるべきです。曖昧な真偽値が増えると、挙動が読めなくなります。</p>
<h3 id="悪い例">悪い例</h3>
<ul>
<li><code>enable_special_mode: true</code></li>
<li><code>quick: true</code></li>
</ul>
<h3 id="良い例">良い例</h3>
<ul>
<li><code>test-scope: unit|integration|full</code></li>
<li><code>build-target: web|api|worker</code></li>
<li><code>upload-artifact: true|false</code></li>
</ul>
<p>再利用ワークフロー側では型とデフォルト値を明示します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_call</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">test-scope</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">string</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>: <span style="color:#e6db74">&#34;unit&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">upload-artifact</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">boolean</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>入力名を自然言語に寄せすぎない</strong>のがコツです。将来の運用者が見て意味がぶれない命名に寄せます。</p>
<h2 id="4-セキュリティ設計permissions-を最小化する">4. セキュリティ設計：permissions を最小化する</h2>
<p>再利用ワークフローの導入後に増える事故が「権限過剰」です。<code>permissions: write-all</code> を残したまま運用すると、意図しないトークン利用で供給網リスクが高まります。</p>
<p>推奨はジョブ単位での最小権限化です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">contents</span>: <span style="color:#ae81ff">read</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull-requests</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">actions</span>: <span style="color:#ae81ff">read</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>デプロイやタグ作成など書き込みが必要な処理だけ、専用ジョブに分離して <code>contents: write</code> を付与します。CI と CD の境界を分けるだけで、攻撃面積をかなり削減できます。</p>
<h2 id="5-キャッシュ設計でハマらないための実践">5. キャッシュ設計でハマらないための実践</h2>
<p>Actions の高速化でよく使う <code>actions/cache</code> は、キー設計を誤ると逆効果になります。</p>
<ul>
<li>キーが粗い: 依存不整合で不安定化</li>
<li>キーが細かすぎる: キャッシュヒット率低下</li>
</ul>
<p>Node の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-node@v4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">node-version</span>: <span style="color:#ae81ff">${{ inputs.node-version }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#ae81ff">npm</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm ci</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm test</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>まずは <code>setup-*</code> の組み込みキャッシュを優先し、複雑な手動キャッシュは必要になってから追加する方が安全です。</p>
<h2 id="6-導入手順3週間での現実的移行">6. 導入手順（3週間での現実的移行）</h2>
<h3 id="week-1-現状分析">Week 1: 現状分析</h3>
<ol>
<li>全 repo の workflow を収集</li>
<li>共通ジョブを抽出（lint/test/build/security）</li>
<li>失敗率の高い処理を特定</li>
</ol>
<h3 id="week-2-共通-repo-構築">Week 2: 共通 repo 構築</h3>
<ol>
<li><code>org-ci-workflows</code> を作成</li>
<li>2〜3 種類の再利用ワークフローを作る</li>
<li>テンプレート repo で先行検証</li>
</ol>
<h3 id="week-3-段階移行">Week 3: 段階移行</h3>
<ol>
<li>低リスク repo から順次移行</li>
<li>失敗ログを収集し API（inputs）を微調整</li>
<li><code>v1</code> タグを固定し、全体展開</li>
</ol>
<p>重要なのは、一気に全 repo を置き換えないことです。先行導入で「どの入力が不足しているか」を把握し、後続 repo の移行コストを下げます。</p>
<h2 id="7-変更管理main-直参照を禁止する">7. 変更管理：main 直参照を禁止する</h2>
<p>呼び出し側で <code>@main</code> を使うと、共通 repo の更新が即時反映されます。これは便利ですが、本番では危険です。事故時に「いつ壊れたか」が追跡しづらくなります。</p>
<p>実運用では次を推奨します。</p>
<ul>
<li>呼び出しは <code>@v1</code> などのタグ固定</li>
<li>破壊的変更は <code>v2</code> を新規発行</li>
<li>重大修正のみ <code>v1.x</code> に backport</li>
</ul>
<p>さらに、変更通知テンプレートを用意しておくと移行が速くなります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[CI Workflow Update]
</span></span><span style="display:flex;"><span>- Target: ci-node.yml
</span></span><span style="display:flex;"><span>- Change: npm audit step added
</span></span><span style="display:flex;"><span>- Impact: build time +20-40 sec
</span></span><span style="display:flex;"><span>- Action Required: none (v1.3.0 auto pickup)
</span></span><span style="display:flex;"><span>- Rollback: use v1.2.4
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="8-監視指標導入効果を数字で見る">8. 監視指標（導入効果を数字で見る）</h2>
<p>再利用ワークフローを入れたら、以下を最低でも計測します。</p>
<ul>
<li>CI 平均実行時間</li>
<li>PR あたり失敗回数</li>
<li>ワークフロー設定変更に伴う障害件数</li>
<li>セキュリティ警告検出率</li>
</ul>
<p>導入後 1 か月で、<code>設定差分の削減</code> と <code>障害切り分け時間の短縮</code> が見えてくるはずです。数字が出ない場合は、共通化の粒度が粗すぎる可能性があります。</p>
<h2 id="9-よくある失敗と対策">9. よくある失敗と対策</h2>
<h3 id="失敗1-共通化しすぎて柔軟性が消える">失敗1: 共通化しすぎて柔軟性が消える</h3>
<p>対策: 80%共通 + 20%ローカル上書きの構造にする。例外を許容する。</p>
<h3 id="失敗2-例外入力が増えて-api-が壊れる">失敗2: 例外入力が増えて API が壊れる</h3>
<p>対策: 四半期ごとに <code>inputs</code> を棚卸し。未使用入力を削除し、破壊的変更はメジャーバージョンへ。</p>
<h3 id="失敗3-platform-team-しか触れない">失敗3: Platform Team しか触れない</h3>
<p>対策: docs/onboarding.md を整備し、アプリチームが PR で改善できる運用にする。</p>
<h2 id="10-そのまま使える導入チェックリスト">10. そのまま使える導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 共通 repo を作成し、責任者を明確化した</li>
<li><input disabled="" type="checkbox"> <code>workflow_call</code> の入力仕様を文書化した</li>
<li><input disabled="" type="checkbox"> <code>permissions</code> を最小権限に設定した</li>
<li><input disabled="" type="checkbox"> 呼び出し側でタグ固定（@v1）を徹底した</li>
<li><input disabled="" type="checkbox"> 先行 repo で 1 週間運用検証した</li>
<li><input disabled="" type="checkbox"> ロールバック手順を README に記載した</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>GitHub Actions の再利用ワークフローは、単なる YAML 共通化ではなく、組織の開発基盤を製品として運用する取り組みです。導入の成否は、技術よりも運用設計に左右されます。</p>
<ul>
<li>入力仕様を API として設計する</li>
<li>権限を最小化し事故半径を縮小する</li>
<li>バージョン固定で変更を可観測にする</li>
</ul>
<p>この3点を守るだけで、CI/CD は「壊れやすい魔法」から「改善可能な仕組み」に変わります。まずは 1 つの再利用ワークフローから始め、3 週間で小さく成功を作るのが最短ルートです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>CI/CD</category>
      <category>DevOps</category>
      <category>Platform Engineering</category>
    </item>
    <item>
      <title>GitHub Copilot Workspace活用術：仕様書から実装まで一気通貫</title>
      <link>https://www.ai2core.com/posts/2026-02-20-copilot-workspace/</link>
      <pubDate>Fri, 20 Feb 2026 08:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-20-copilot-workspace/</guid>
      <description>Copilot Workspaceを使って、IssueからPR作成までを自動化するフロー。</description>
      <content:encoded><![CDATA[<h1 id="github-copilot-workspace活用術仕様書から実装まで一気通貫">GitHub Copilot Workspace活用術：仕様書から実装まで一気通貫</h1>
<h2 id="はじめに">はじめに</h2>
<p>ソフトウェア開発の現場にいる皆さん、日々の業務フローを思い返してみてください。</p>
<ol>
<li>GitHub Issue や Jira のチケットで仕様を確認する。</li>
<li>内容を理解し、ローカルで新しいブランチを作成する。</li>
<li>関連ファイルを特定し、エディタで開く。</li>
<li>仕様に沿ってコードを書き、修正する。</li>
<li>動作確認のためのテストコードを記述する。</li>
<li>ローカルでテストを実行し、問題ないことを確認する。</li>
<li>変更をコミットし、リモートにプッシュする。</li>
<li>ブラウザで GitHub を開き、Pull Request (PR) を作成する。</li>
</ol>
<p>この一連の流れは、私たちエンジニアにとって日常茶飯事です。しかし、このプロセスには多くのコンテキストスイッチ（思考の切り替え）が含まれており、特にプロジェクトの初期段階や、新しいコードベースに触れる際には、仕様の理解や関連ファイルの特定といった「準備運動」に多くの時間が費やされます。</p>
<p>「Issue の内容を理解したら、あとはAIが自動でコードを書いてPRまで作ってくれたら最高なのに…」</p>
<p>そんな未来の夢物語が、<strong>GitHub Copilot Workspace</strong> の登場によって、現実のものとなりつつあります。この記事では、GitHub がテクニカルプレビューとして公開した Copilot Workspace を徹底的に解説し、Issue（仕様書）を起点として、AIと共に実装計画を立て、コードを生成し、Pull Request を作成するまでの一気通貫の開発フローを実践的なハンズオン形式で紹介します。</p>
<p>この記事を読み終える頃には、あなたは Copilot Workspace の強力な機能を理解し、自身の開発プロセスを劇的に効率化させるための具体的なイメージを描けるようになっているでしょう。</p>
<hr>
<h2 id="なぜ今copilot-workspaceが重要なのか">なぜ今、Copilot Workspaceが重要なのか？</h2>
<p>GitHub Copilot が登場し、私たちのコーディング体験は大きく変わりました。関数名やコメントから意図を汲み取り、驚くほど的確なコードを補完してくれる Copilot は、もはや手放せない相棒となっているエンジニアも多いでしょう。その後、Copilot Chat が登場し、エディタ内で対話形式でコードに関する質問やリファクタリングの相談ができるようになりました。</p>
<p>しかし、これまでの Copilot は、あくまで「コーディング」という実装フェーズに特化した支援ツールでした。開発プロセス全体を見渡すと、実装の前には「仕様の理解」「設計」「実装計画」といった上流工程が存在し、実装の後には「テスト」「PR作成」「レビュー」といった下流工程が存在します。</p>
<p>ここに、現代の開発プロセスが抱える根深い課題があります。</p>
<ul>
<li><strong>コンテキストスイッチのオーバーヘッド:</strong> Issue、設計ドキュメント、エディタ、ターミナル、ブラウザ… 開発者は複数のツールやウィンドウを絶えず行き来する必要があり、そのたびに集中力が削がれます。</li>
<li><strong>定型作業の繰り返し:</strong> ブランチ作成、ボイラープレートコードの記述、基本的なテストケースの作成など、創造的とは言えない定型作業に多くの時間が奪われています。</li>
<li><strong>オンボーディングの壁:</strong> 新しいメンバーがプロジェクトに参加した際、広大なコードベースのどこから手をつければ良いのかを把握するのは非常に困難です。</li>
</ul>
<p>GitHub Copilot Workspace は、これらの課題を解決するために生まれました。それは単なるコード補完ツールではありません。<strong>Issue を起点として、開発タスクの全体像をAIが把握し、人間と対話しながら「仕様の明確化」「実装計画の立案」「コード生成」「テスト」までを半自動的に実行する、いわば「AI搭載の開発環境」</strong> なのです。</p>
<p>Copilot Workspace は、開発プロセスの上流から下流までをシームレスに繋ぎ、エンジニアを煩雑な作業から解放します。これにより、エンジニアはより創造的で、本質的な価値を生み出す「設計」や「アーキテクチャの検討」「複雑なビジネスロジックの実装」に集中できるようになるのです。これは、開発の生産性を根底から覆すポテンシャルを秘めた、大きなパラダイムシフトと言えるでしょう。</p>
<hr>
<h2 id="copilot-workspaceによる開発フロー徹底解説">Copilot Workspaceによる開発フロー徹底解説</h2>
<p>それでは、実際に Copilot Workspace を使って Issue から PR 作成までを駆け抜けるプロセスを、具体的なハンズオン形式で見ていきましょう。</p>
<h3 id="copilot-workspace-の全体像">Copilot Workspace の全体像</h3>
<p>Workspace のフローは、大きく分けて以下の3つのステップで構成されています。この概念を理解することが、Workspace を使いこなすための鍵となります。</p>
<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">graph TD;
    A[📝 GitHub Issue] --&gt; B(🚀 Copilot Workspaceを起動);
    B --&gt; C{1. Spec&lt;br&gt;(仕様の確認・編集)};
    C -- AIと対話し仕様を洗練 --&gt; D{2. Plan&lt;br&gt;(実行計画の生成・編集)};
    D -- 計画に基づき実行 --&gt; E{3. Implementation&lt;br&gt;(コードの自動生成・修正)};
    E -- 人間によるレビュー・微修正 --&gt; F[✅ Pull Request作成];
</code></pre><ol>
<li><strong>Spec (仕様):</strong> Workspace はまず Issue の内容を読み込み、タスクの目的や要件を自然言語でまとめた「仕様」を生成します。開発者はこの仕様を確認し、AI とのチャットを通じて内容をより正確かつ詳細なものに洗練させていきます。</li>
<li><strong>Plan (計画):</strong> 洗練された仕様に基づき、AI は「どのファイルを新規作成するか」「どのファイルをどう変更するか」「どのコマンドを実行するか」といった具体的な「実行計画」を立案します。この計画はステップバイステップで表示され、開発者は内容を確認・編集できます。</li>
<li><strong>Implementation (実装):</strong> 開発者が計画を承認すると、Workspace は計画に沿ってファイルの作成、コードの生成・編集、コマンドの実行などを自動で行います。生成されたコードの差分（diff）は即座に確認でき、必要に応じて Workspace 内のエディタやターミナルで直接修正を加えることが可能です。</li>
</ol>
<p>この**「仕様定義 → 計画立案 → 実装」**という反復的なプロセスこそが、Copilot Workspace の中核です。</p>
<h3 id="実践issueからpr作成までのハンズオン">実践！IssueからPR作成までのハンズオン</h3>
<p>今回は、簡単な REST API を提供する Node.js (Express) プロジェクトを例に、新しいエンドポイントを追加するタスクを Workspace で実行してみましょう。</p>
<p><strong>前提:</strong></p>
<ul>
<li>GitHub Copilot Workspace のテクニカルプレビューにアクセスできること。</li>
<li>Node.js と Express で作られたシンプルな Web API のリポジトリが存在すること。</li>
</ul>
<h4 id="step-0-課題となる-issue-の準備">Step 0: 課題となる Issue の準備</h4>
<p>まずは、開発の起点となる Issue を作成します。AI が理解しやすいように、具体的かつ明確に記述することがポイントです。</p>
<p><strong>Issue Title:</strong> <code>feat: GET /api/v1/users エンドポイントを追加</code></p>
<p><strong>Issue Body:</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span></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-markdown" data-lang="markdown"><span style="display:flex;"><span><span style="color:#75715e">### 概要
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>ユーザーの一覧を取得するための新しいAPIエンドポイント <span style="color:#e6db74">`GET /api/v1/users`</span> を追加します。
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">### 背景
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>クライアントアプリケーションがユーザー情報を一覧表示するために、このAPIが必要です。
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">### 受け入れ条件
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> HTTP GETリクエストを <span style="color:#e6db74">`/api/v1/users`</span> パスで受け付けること。
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> レスポンスはJSON形式であること。
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> レスポンスボディは、<span style="color:#e6db74">`data`</span> というキーを持つオブジェクトであること。
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`data`</span> の値は、以下の構造を持つユーザーオブジェクトの配列であること。
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">-</span> <span style="color:#e6db74">`id`</span> (number)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">-</span> <span style="color:#e6db74">`name`</span> (string)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">-</span> <span style="color:#e6db74">`email`</span> (string)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> 現状、データベース連携は不要。モックデータとして3人分のユーザー情報を返すこと。
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> エラーハンドリングは考慮しなくてよい。
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> 既存のルーティングファイル (<span style="color:#e6db74">`src/routes/index.js`</span>) に新しいルートを追加すること。
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="step-1-workspace-の起動">Step 1: Workspace の起動</h4>
<p>作成した Issue ページにアクセスすると、Copilot のアイコンと共に「Open in Workspace」というボタンが表示されます（プレビュー有効時）。これをクリックして Workspace を起動します。</p>
<p><em>(画像はイメージです)</em></p>
<h4 id="step-2-spec---仕様の定義と洗練">Step 2: &ldquo;Spec&rdquo; - 仕様の定義と洗練</h4>
<p>Workspace が起動すると、AI が Issue の内容を解析し、以下のような「Spec」を自動生成します。</p>
<blockquote>
<p><strong>Initial Specification</strong></p>
<p>The goal is to add a new API endpoint <code>GET /api/v1/users</code>.
This endpoint should return a mocked list of users in a specific JSON format.</p>
<p><strong>Acceptance Criteria:</strong></p>
<ul>
<li>The endpoint must handle GET requests to <code>/api/v1/users</code>.</li>
<li>The response should be a JSON object with a <code>data</code> key.</li>
<li>The <code>data</code> key should contain an array of user objects.</li>
<li>Each user object must have <code>id</code>, <code>name</code>, and <code>email</code>.</li>
<li>The implementation should use mock data for three users.</li>
<li>The new route should be added to the existing file <code>src/routes/index.js</code>.</li>
</ul></blockquote>
<p>この時点で、AI がタスクを正しく理解していることが分かります。もし、何か補足したい情報があれば、下部のチャットボックスから AI に指示を出します。</p>
<p>例えば、返すモックデータに具体的な名前を指定したい場合、以下のようにチャットします。</p>
<p><strong>あなた:</strong></p>
<blockquote>
<p><code>Use the following names for the mock data: Alice, Bob, and Charlie.</code></p></blockquote>
<p>すると、AI はこの指示を理解し、Spec を更新します。この対話を通じて、実装に入る前に AI との認識合わせを完璧に行うことができます。</p>
<h4 id="step-3-plan---実行計画の立案">Step 3: &ldquo;Plan&rdquo; - 実行計画の立案</h4>
<p>Spec が固まったら、AI はそれを実現するための具体的な「Plan」を作成します。</p>
<blockquote>
<p><strong>Execution Plan</strong></p>
<p>I will perform the following steps to implement the feature:</p>
<ol>
<li>
<p><strong>Modify file <code>src/routes/index.js</code></strong>: I will add a new route handler for <code>GET /api/v1/users</code>. This handler will define the mock user data and send it as a JSON response in the required format.</p>
</li>
<li>
<p><strong>Create file <code>tests/users.test.js</code></strong>: To ensure the new endpoint works correctly, I will create a new test file. This test will use a library like <code>supertest</code> to make a request to the new endpoint and assert that the response status is 200 and the body matches the expected structure.</p>
</li>
</ol></blockquote>
<p>この計画は非常に理にかなっています。実装ファイルの変更だけでなく、その動作を保証するためのテストファイルの作成まで提案してくれている点は素晴らしいです。もし、テストが不要な場合や、別のファイルにロジックを分割したい場合は、この Plan を手動で編集したり、AI に修正を依頼したりすることも可能です。</p>
<h4 id="step-4-implementation---aiによる自動実装">Step 4: &ldquo;Implementation&rdquo; - AIによる自動実装</h4>
<p>Plan に納得したら、「Run」ボタンをクリックします。すると、Workspace が計画に沿ってコードの生成とファイルの作成を自動的に開始します。処理が完了すると、変更内容が差分（diff）形式で表示されます。</p>
<p><strong><code>src/routes/index.js</code> の変更差分:</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>  const express = require(&#39;express&#39;);
</span></span><span style="display:flex;"><span>  const router = express.Router();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  /* GET home page. */
</span></span><span style="display:flex;"><span>  router.get(&#39;/&#39;, function(req, res, next) {
</span></span><span style="display:flex;"><span>    res.json({ title: &#39;Express&#39; });
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ router.get(&#39;/api/v1/users&#39;, (req, res) =&gt; {
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+   const mockUsers = [
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+     { id: 1, name: &#39;Alice&#39;, email: &#39;alice@example.com&#39; },
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+     { id: 2, name: &#39;Bob&#39;, email: &#39;bob@example.com&#39; },
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+     { id: 3, name: &#39;Charlie&#39;, email: &#39;charlie@example.com&#39; },
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+   ];
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+   res.status(200).json({ data: mockUsers });
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ });
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>  module.exports = router;
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong><code>tests/users.test.js</code> の新規作成:</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">request</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;supertest&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">app</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;../src/app&#39;</span>); <span style="color:#75715e">// Express app instance
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">describe</span>(<span style="color:#e6db74">&#39;GET /api/v1/users&#39;</span>, () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">it</span>(<span style="color:#e6db74">&#39;should return a list of users&#39;</span>, <span style="color:#66d9ef">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">res</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">request</span>(<span style="color:#a6e22e">app</span>).<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;/api/v1/users&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">statusCode</span>).<span style="color:#a6e22e">toEqual</span>(<span style="color:#ae81ff">200</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">body</span>).<span style="color:#a6e22e">toHaveProperty</span>(<span style="color:#e6db74">&#39;data&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(Array.<span style="color:#a6e22e">isArray</span>(<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">body</span>.<span style="color:#a6e22e">data</span>)).<span style="color:#a6e22e">toBe</span>(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">body</span>.<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">length</span>).<span style="color:#a6e22e">toBe</span>(<span style="color:#ae81ff">3</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">body</span>.<span style="color:#a6e22e">data</span>[<span style="color:#ae81ff">0</span>]).<span style="color:#a6e22e">toHaveProperty</span>(<span style="color:#e6db74">&#39;id&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">body</span>.<span style="color:#a6e22e">data</span>[<span style="color:#ae81ff">0</span>]).<span style="color:#a6e22e">toHaveProperty</span>(<span style="color:#e6db74">&#39;name&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">body</span>.<span style="color:#a6e22e">data</span>[<span style="color:#ae81ff">0</span>]).<span style="color:#a6e22e">toHaveProperty</span>(<span style="color:#e6db74">&#39;email&#39;</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></td></tr></table>
</div>
</div><p>生成されたコードは、概ね Issue の要件を満たしています。ここで、Workspace 内に組み込まれたターミナルを開き、テストを実行してみましょう。</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-bash" data-lang="bash"><span style="display:flex;"><span>$ npm install
</span></span><span style="display:flex;"><span>$ npm test
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>&gt; tests/users.test.js
</span></span><span style="display:flex;"><span>✓ should <span style="color:#66d9ef">return</span> a list of users <span style="color:#f92672">(</span>5ms<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Test Suites: <span style="color:#ae81ff">1</span> passed, <span style="color:#ae81ff">1</span> total
</span></span><span style="display:flex;"><span>Tests:       <span style="color:#ae81ff">1</span> passed, <span style="color:#ae81ff">1</span> total
</span></span><span style="display:flex;"><span>Snapshots:   <span style="color:#ae81ff">0</span> total
</span></span><span style="display:flex;"><span>Time:        0.5s
</span></span></code></pre></td></tr></table>
</div>
</div><p>テストが成功しました。もしコードに修正が必要な場合は、Workspace 上のエディタで直接編集できます。例えば、レスポンスコードを <code>201</code> に変えたい場合、エディタで <code>res.status(200)</code> を <code>res.status(201)</code> に書き換えるだけです。</p>
<h4 id="step-5-prの作成">Step 5: PRの作成</h4>
<p>実装とテストに問題がないことを確認したら、画面上部にある「Create Pull Request」ボタンをクリックします。</p>
<p>すると、PR 作成画面に遷移します。驚くべきことに、PR のタイトルと説明文も、AI が Issue とコードの変更内容を基に自動で生成してくれます。</p>
<p><strong>PR Title (自動生成):</strong> <code>feat: Add GET /api/v1/users endpoint</code></p>
<p><strong>PR Body (自動生成):</strong></p>
<blockquote>
<p>This pull request introduces a new endpoint <code>GET /api/v1/users</code> to fetch a list of users.</p>
<p><strong>Changes:</strong></p>
<ul>
<li>Added a new route to <code>src/routes/index.js</code> to handle the request.</li>
<li>The endpoint returns a hardcoded list of three users as mock data.</li>
<li>Added a new test file <code>tests/users.test.js</code> to verify the functionality of the new endpoint.</li>
</ul>
<p>Closes #123 (Issue番号)</p></blockquote>
<p>内容を確認し、必要であれば追記して PR を作成します。これで、Issue の起票から PR のマージ準備まで、ほとんどの作業を GitHub の中で、AI の支援を受けながら完結させることができました。</p>
<hr>
<h2 id="メリットとデメリット">メリットとデメリット</h2>
<p>Copilot Workspace は革命的なツールですが、万能ではありません。そのメリットと、現時点でのデメリットや注意点を冷静に評価してみましょう。</p>
<h3 id="メリット">メリット</h3>
<ul>
<li><strong>圧倒的な生産性向上:</strong> これまで手作業で行っていたブランチ作成、ファイル検索、ボイラープレートの記述、テストの雛形作成、PR の文章作成といった一連の定型作業がほぼ自動化されます。これにより、エンジニアはタスクの本質的な部分に集中できます。</li>
<li><strong>コンテキストスイッチの撲滅:</strong> Issue の確認から PR 作成まで、すべての作業が Workspace という単一の環境で完結します。ツール間を行き来する必要がなくなり、思考の分断が起こりにくくなります。</li>
<li><strong>スムーズなオンボーディング:</strong> 新しいプロジェクトに参加した際、Issue を Workspace で開けば、AI が関連ファイルを特定し、変更計画まで立ててくれます。これにより、コードベースの全体像を把握し、最初の貢献を行うまでの時間を大幅に短縮できます。</li>
<li><strong>設計と実装の分離:</strong> 「Spec」と「Plan」のステップを踏むことで、いきなりコーディングを始めるのではなく、「何を」「どのように」作るのかを明確にしてから実装に進むという、良い開発習慣が自然と身につきます。</li>
</ul>
<h3 id="デメリット--注意点">デメリット / 注意点</h3>
<ul>
<li><strong>AI が生成するコードの品質:</strong> 生成されるコードは常に完璧ではありません。バグを含んでいたり、プロジェクトのコーディング規約に沿っていなかったり、最適な設計でない場合もあります。<strong>最終的な品質を担保するのは、あくまで人間のエンジニアの役割</strong>であり、コードレビューの重要性は変わりません。</li>
<li><strong>複雑なタスクへの対応限界:</strong> 複数のファイルを横断する大規模なリファクタリングや、ドメイン知識が深く要求される複雑なビジネスロジックの実装など、高度な抽象的思考が必要なタスクにはまだ対応しきれない場面があります。</li>
<li><strong>思考停止のリスク:</strong> AI に頼りすぎることで、エンジニア自身が「なぜこの変更が必要なのか」「他にどんな実装方法があるか」といったことを深く考えなくなる危険性があります。Workspace はあくまで「優秀なアシスタント」であり、思考の主体は人間であるべきです。</li>
<li><strong>テクニカルプレビュー段階:</strong> 現在はまだプレビュー版であり、機能が変更されたり、動作が不安定になったりする可能性があります。本番の重要なプロジェクトに全面的に導入するには、今後の動向を注視する必要があります。</li>
</ul>
<hr>
<h2 id="現場で使える実践的なtips">現場で使える実践的なTips</h2>
<p>Copilot Workspace のポテンシャルを最大限に引き出すための、いくつかの実践的なヒントを紹介します。</p>
<h3 id="1-良いissueが良い実装を生む">1. 「良いIssue」が「良い実装」を生む</h3>
<p>Workspace の出発点は Issue です。AI の性能はインプットの質に大きく左右されるため、精度の高い実装を引き出すためには、AI が理解しやすい Issue を書くことが非常に重要です。</p>
<ul>
<li><strong>背景 (Why) を書く:</strong> 「何をするか (What)」だけでなく、「なぜそれが必要なのか (Why)」を記述することで、AI がタスクの文脈をより深く理解し、適切な実装を提案しやすくなります。</li>
<li><strong>受け入れ条件 (Acceptance Criteria) を明確にする:</strong> 「〜であること」という形式で、タスク完了の定義を箇条書きで具体的に示しましょう。これは AI にとっての仕様書そのものになります。</li>
<li><strong>専門用語やファイル名をヒントとして与える:</strong> 「<code>UserService</code> クラスを使ってデータを取得すること」「設定は <code>config/features.json</code> を参照すること」のように、コードベース固有の情報を与えると、AI の計画 (Plan) の精度が劇的に向上します。</li>
</ul>
<h3 id="2-plan-を対話的に育てる">2. &ldquo;Plan&rdquo; を対話的に育てる</h3>
<p>AI が生成した最初の Plan が完璧であることは稀です。Plan を鵜呑みにせず、レビューし、AI との対話を通じて改善していくプロセスが重要です。</p>
<ul>
<li><strong>ステップの分割・追加を指示する:</strong> 「ステップ2を、ロジック部分とルーティング部分の2つに分割して」「テストを実行するステップを最後に追加して」のように、計画をより細かく、より安全に実行できるように指示します。</li>
<li><strong>代替案を尋ねる:</strong> 「この実装方法以外に、もっと良い方法はありますか？」と尋ねることで、自分では思いつかなかったアプローチを AI が提案してくれることがあります。</li>
</ul>
<h3 id="3-ローカル環境とのハイブリッド活用">3. ローカル環境とのハイブリッド活用</h3>
<p>Workspace は強力ですが、全ての作業をそこで完結させる必要はありません。特にデバッグや複雑な修正には、使い慣れたローカルのエディタが適している場合もあります。</p>
<p>Workspace で大枠の実装（80%）を自動生成させ、その後ローカルにブランチをチェックアウトして、細部の調整やデバッグ（残りの20%）を行う、というハイブリッドな使い方が非常に効率的です。</p>
<hr>
<h2 id="まとめ">まとめ</h2>
<p>GitHub Copilot Workspace は、単なるコーディング支援ツールではなく、<strong>開発プロセスそのものを再定義する可能性を秘めたゲームチェンジャー</strong>です。Issue という「要求」から Pull Request という「成果物」までを、AI と人間が協調しながらシームレスに繋ぐことで、開発の生産性と体験を新たな次元へと引き上げます。</p>
<p>もちろん、このツールはまだ発展途上であり、人間のエンジニアによるレビューや設計、最終的な意思決定の重要性が失われることはありません。むしろ、AI をいかにうまく「操縦」し、その能力を最大限に引き出すかが、これからのエンジニアに求められる新しいスキルセットになるでしょう。</p>
<p>私たちの役割は、「一行一行コードを書く作業者」から、<strong>「AI という優秀な部下を持つプロジェクトマネージャー兼アーキテクト」</strong> へと変化していくのかもしれません。</p>
<p>GitHub Copilot Workspace がもたらす未来の開発スタイルは、もうすぐそこまで来ています。ぜひテクニカルプレビューに登録し、この新しい開発体験をいち早くご自身のプロジェクトで試してみてください。きっと、開発の未来を垣間見ることができるはずです。</p>
]]></content:encoded>
      <category>Coding AI</category>
      <category>GitHub Copilot</category>
      <category>Workspace</category>
      <category>DevOps</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>
