<?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>Terraform on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/terraform/</link>
    <description>Recent content in Terraform on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 07 Mar 2026 09:05:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/terraform/index.xml" rel="self" type="application/rss+xml" />
    <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>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>
  </channel>
</rss>
