<?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>Opentofu on Tarragon</title><link>https://tarrragon.github.io/blog/tags/opentofu/</link><description>Recent content in Opentofu on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/opentofu/index.xml" rel="self" type="application/rss+xml"/><item><title>Terraform → OpenTofu：HCL 跟 state file 級 drop-in、CI runner 切 binary 完成</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/migrate-to-opentofu/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/migrate-to-opentofu/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform&lt;/a>（source）跟 OpenTofu（target）。Type B drop-in migration 標準形態、跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>6 維皆 Low → Type B drop-in&lt;/em>；本文驗證 skill 的 Type B anatomy 在 IaC 領域成立。&lt;/p>&lt;/blockquote>
&lt;h2 id="hcl--state-file--provider-三層-diff-sample">HCL / state file / provider 三層 diff sample&lt;/h2>
&lt;p>跟前批 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> 同為 Type B drop-in、本文用 code-led entry — 直接給 3 種 diff sample 證明「真 drop-in」：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. HCL syntax: 完全相同 (Terraform 1.5.x baseline)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket&amp;#34; &amp;#34;logs&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;myapp-logs&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n"> tags&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="n"> Env&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;production&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="err">#&lt;/span> &lt;span class="k">兩家&lt;/span> &lt;span class="k">binary&lt;/span> &lt;span class="k">都接受&lt;/span>&lt;span class="err">、&lt;/span>&lt;span class="k">執行結果一致&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. State file: 完全相同 schema&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">$ cat terraform.tfstate &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.version, .terraform_version&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="m">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s2">&amp;#34;1.5.7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 切 OpenTofu 後 re-init、state 保留&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">$ tofu init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">$ cat terraform.tfstate &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.version, .terraform_version&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="m">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s2">&amp;#34;1.6.0&amp;#34;&lt;/span> &lt;span class="c1"># tool version 標記變、其他不變&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. Provider: registry 路徑唯一明顯差異
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">terraform&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">required_providers&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> aws&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="n"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;hashicorp/aws&amp;#34;&lt;/span>&lt;span class="c1"> # 兩家共用 source 字串
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n"> version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;~&amp;gt; 5.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">}&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># Terraform 從 registry.terraform.io 拉
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="err">#&lt;/span> &lt;span class="k">OpenTofu&lt;/span> &lt;span class="k">預設從&lt;/span> &lt;span class="k">registry&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">opentofu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">org&lt;/span> &lt;span class="k">拉&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">fallback&lt;/span> &lt;span class="k">到&lt;/span> &lt;span class="k">terraform&lt;/span> &lt;span class="k">registry&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>3 層 diff sample 顯示：HCL / state schema / 主流 provider 配置完全相容；唯一明顯差異在 &lt;em>registry routing&lt;/em>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform</a>（source）跟 OpenTofu（target）。Type B drop-in migration 標準形態、跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>6 維皆 Low → Type B drop-in</em>；本文驗證 skill 的 Type B anatomy 在 IaC 領域成立。</p></blockquote>
<h2 id="hcl--state-file--provider-三層-diff-sample">HCL / state file / provider 三層 diff sample</h2>
<p>跟前批 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 同為 Type B drop-in、本文用 code-led entry — 直接給 3 種 diff sample 證明「真 drop-in」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. HCL syntax: 完全相同 (Terraform 1.5.x baseline)
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket&#34; &#34;logs&#34;</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="s2">&#34;myapp-logs&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">    Env</span> <span class="o">=</span> <span class="s2">&#34;production&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  }
</span></span><span class="line"><span class="ln">7</span><span class="cl">}
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="err">#</span> <span class="k">兩家</span> <span class="k">binary</span> <span class="k">都接受</span><span class="err">、</span><span class="k">執行結果一致</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 2. State file: 完全相同 schema</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">$ cat terraform.tfstate <span class="p">|</span> jq <span class="s1">&#39;.version, .terraform_version&#39;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="m">4</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s2">&#34;1.5.7&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 切 OpenTofu 後 re-init、state 保留</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">$ tofu init
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">$ cat terraform.tfstate <span class="p">|</span> jq <span class="s1">&#39;.version, .terraform_version&#39;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="m">4</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s2">&#34;1.6.0&#34;</span>  <span class="c1"># tool version 標記變、其他不變</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 3. Provider: registry 路徑唯一明顯差異
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">terraform</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">required_providers</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    aws</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">      source</span>  <span class="o">=</span> <span class="s2">&#34;hashicorp/aws&#34;</span><span class="c1">     # 兩家共用 source 字串
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="n">      version</span> <span class="o">=</span> <span class="s2">&#34;~&gt; 5.0&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    }
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  }
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">}<span class="c1">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># Terraform 從 registry.terraform.io 拉
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="err">#</span> <span class="k">OpenTofu</span> <span class="k">預設從</span> <span class="k">registry</span><span class="p">.</span><span class="k">opentofu</span><span class="p">.</span><span class="k">org</span> <span class="k">拉</span> <span class="p">(</span><span class="k">fallback</span> <span class="k">到</span> <span class="k">terraform</span> <span class="k">registry</span><span class="p">)</span></span></span></code></pre></div><p>3 層 diff sample 顯示：HCL / state schema / 主流 provider 配置完全相容；唯一明顯差異在 <em>registry routing</em>。</p>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>HCL 完全相容、CLI command 對映 (terraform → tofu)</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 workflow (init / plan / apply)</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>同 IaC declarative</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 single binary</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>無（不是 application、是 infrastructure tool）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同 single state file backend</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>6 維皆 Low → Type B drop-in。</p>
<h2 id="為什麼遷license--governance--community-三條-driver">為什麼遷：license / governance / community 三條 driver</h2>
<p>跟前批 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 不同（cost / performance driver）、Terraform → OpenTofu 主要 driver 在 governance：</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>License</strong></td>
          <td>Terraform 在 2023-08 改 BSL（Business Source License）、商業使用限制；OpenTofu 維持 MPL 2.0 開源</td>
      </tr>
      <tr>
          <td><strong>Vendor neutrality</strong></td>
          <td>多雲 / 多客戶情境想避免 HashiCorp lock-in、用 Linux Foundation 治理的 OpenTofu</td>
      </tr>
      <tr>
          <td><strong>Community / feature</strong></td>
          <td>OpenTofu 1.6+ 加 state encryption、跟 Terraform 商業版差異化、社群驅動 feature</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（OpenTofu → Terraform）：</p>
<ul>
<li>Terraform Cloud / Enterprise 特定 feature 依賴（policy as code 用 Sentinel、跟 OpenTofu 自家 OPA 不對等）</li>
<li>既有 module 在 Terraform registry 維護、未同步 OpenTofu registry</li>
</ul>
<h2 id="相容性-audit">相容性 audit</h2>
<p>Pre-cutover 必跑：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Terraform version pin（<code>required_version = &quot;&gt;= 1.5.0, &lt; 1.6.0&quot;</code>）</td>
          <td>改 <code>&gt;= 1.6.0</code> 涵蓋 OpenTofu / 移除 upper bound</td>
      </tr>
      <tr>
          <td>Provider 來源 (registry path)</td>
          <td>主流 provider（aws / azurerm / gcp / k8s）都同源、自家 / 第三方 provider 確認 OpenTofu registry mirror</td>
      </tr>
      <tr>
          <td>Terraform Cloud / Enterprise feature</td>
          <td>Sentinel policy → OpenTofu OPA / Conftest；workspace API 對等性逐項 check</td>
      </tr>
      <tr>
          <td>CLI binary name 在 CI pipeline</td>
          <td><code>terraform plan</code> → <code>tofu plan</code>、或 alias <code>terraform=tofu</code> 保留兼容</td>
      </tr>
      <tr>
          <td>State backend (S3 / GCS / Azure / Consul / Terraform Cloud)</td>
          <td>S3/GCS/Azure 完全相容；Consul backend 兩家都支援；Terraform Cloud 走自家 remote backend、不直通</td>
      </tr>
      <tr>
          <td>Module source</td>
          <td>git-based module 完全相容；registry module 確認 OpenTofu registry 有 mirror</td>
      </tr>
  </tbody>
</table>
<p>Audit output：列「100% drop-in」block + 「需處理」block；後者通常 &lt; 5% 範圍。</p>
<h2 id="step-by-step-cutover">Step-by-step cutover</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. Install OpenTofu (跨 OS)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">brew install opentofu                <span class="c1"># macOS</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">snap install --classic opentofu      <span class="c1"># Ubuntu</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># https://opentofu.org/docs/intro/install/</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 2. 在 workspace 跑 tofu init</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">$ <span class="nb">cd</span> terraform-workspace/
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">$ tofu init -upgrade
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 升級 provider / module、re-init backend、保留 state</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 3. Plan diff（應該 = 0 changes）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">$ tofu plan
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># Plan: 0 to add, 0 to change, 0 to destroy.</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 如果有 diff、表示 provider version 不對齊、檢查 lock file</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 4. Apply（保險起見、staging 先跑）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">$ tofu apply
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 5. CI / CD pipeline 切 binary</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># Before</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">terraform init
</span></span><span class="line"><span class="ln">22</span><span class="cl">terraform plan -out<span class="o">=</span>tfplan
</span></span><span class="line"><span class="ln">23</span><span class="cl">terraform apply tfplan
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"># After</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">tofu init
</span></span><span class="line"><span class="ln">27</span><span class="cl">tofu plan -out<span class="o">=</span>tfplan
</span></span><span class="line"><span class="ln">28</span><span class="cl">tofu apply tfplan
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="c1"># 或保留 terraform 字面、用 alias / symlink</span></span></span></code></pre></div><p>整個 cutover 通常 &lt; 1 天（單 workspace）；多 workspace organization 視規模 1-4 週逐個切。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1provider-version-driftstaging-plan-出現意外-diff">Case 1：Provider version drift、staging plan 出現意外 diff</h3>
<p><strong>徵兆</strong>：<code>tofu plan</code> 顯示 100+ resource 有 in-place update、實際業務沒改任何 config。</p>
<p><strong>根因</strong>：<code>.terraform.lock.hcl</code> 鎖的 provider version 在 Terraform / OpenTofu registry 不一致（同 version 但 binary checksum 微差）；OpenTofu 在 init 時拉新 checksum、視為「provider 變了」。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預先對齊</strong>：<code>tofu init -upgrade</code> 重建 lock file、把 OpenTofu 端 checksum 寫進去</li>
<li><strong>CI lockfile commit</strong>：lock file 進版控、不同 binary 端跑前先 lockfile 對齊</li>
<li><strong>若 plan 仍有差異</strong>：通常是 provider 內部 schema 對 nil 值處理不同、用 <code>lifecycle.ignore_changes</code> 暫忽略、後續逐項 fix</li>
</ol>
<h3 id="case-2state-file-lock-機制微差">Case 2：State file lock 機制微差</h3>
<p><strong>徵兆</strong>：兩個 CI pipeline 同時跑 <code>tofu apply</code>、其中一個應該 lock 拒絕、實際兩個都跑、production 端 race condition。</p>
<p><strong>根因</strong>：Terraform DynamoDB lock 跟 OpenTofu lock 用相同 schema 但 lock_id 規則略不同；舊 lock entry 殘留時 OpenTofu 端解析失敗、視為「無 lock」繼續跑。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>DynamoDB lock table 手動清舊 entry</strong>：cutover 期間先 <code>aws dynamodb delete-item</code> 清舊 lock</li>
<li><strong>單向流量切換</strong>：cutover 期間 freeze 所有 CI、只一個 pipeline 跑、避免 race</li>
<li><strong>架構</strong>：用 <em>fully replicated lock backend</em>（如 Consul）avoid backend-specific lock 怪異</li>
</ol>
<h3 id="case-3terraform-cloud-workspace-不能直接搬">Case 3：Terraform Cloud workspace 不能直接搬</h3>
<p><strong>徵兆</strong>：team 已用 Terraform Cloud workspace 跑 100+ pipeline、想切 OpenTofu、發現 <code>terraform login</code> / workspace API / VCS integration 全 HashiCorp-specific。</p>
<p><strong>根因</strong>：OpenTofu 沒對等 Terraform Cloud 服務；自家 backend 用 S3 + Atlantis / Spacelift / env0 等第三方 platform 對接、不是 1:1 替代。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>保留 Terraform Cloud 跑 production</strong>（OpenTofu 不替代）、用 OpenTofu 跑 dev / sandbox</li>
<li><strong>遷出 Terraform Cloud</strong>：state 遷 S3 + 用 Atlantis 跑 PR-based plan/apply（mature open source）</li>
<li><strong>評估 Spacelift / env0</strong> 商業替代、支援 OpenTofu + 對等 workspace feature</li>
</ol>
<h3 id="case-4ci-pipeline-寫死-terraform-binary-name">Case 4：CI pipeline 寫死 <code>terraform</code> binary name</h3>
<p><strong>徵兆</strong>：cutover 後 CI 跑 <code>terraform plan</code> 報「command not found」；team 100+ pipeline / GitHub Action / GitLab CI / shell script 都寫死 <code>terraform</code>。</p>
<p><strong>根因</strong>：rollout 計畫沒 grep 全 organization 找 binary name 引用。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Alias 策略</strong>：CI image 內 <code>ln -s /usr/local/bin/tofu /usr/local/bin/terraform</code>、保留兼容 1-3 個月</li>
<li><strong>逐步改 <code>tofu</code></strong>：跟著 IaC team 修 pipeline file、target 100% 改完才 remove alias</li>
<li><strong>架構</strong>：避免在 pipeline / script 寫死 binary、用 env variable <code>IAC_BINARY=${IAC_BINARY:-tofu}</code></li>
</ol>
<h3 id="case-5registry-routing自家-module-拉不到">Case 5：Registry routing、自家 module 拉不到</h3>
<p><strong>徵兆</strong>：cutover 後 <code>tofu init</code> 對自家 private module 報「not found」；同 module 在 Terraform 端跑得好好的。</p>
<p><strong>根因</strong>：private module 註冊在 <em>Terraform Cloud private registry</em>、OpenTofu 預設不知道這個 endpoint；需要顯式設 registry source URL。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>顯式 source URL</strong>：<code>source = &quot;app.terraform.io/myorg/myapp/aws&quot;</code> 改 git source 或自架 module registry</li>
<li><strong>架構</strong>：用 git-based module source（<code>source = &quot;git::ssh://git@github.com/myorg/myapp.git&quot;</code>）、避開 registry lock-in</li>
<li><strong>長期</strong>：自家 module 同時 publish 到 OpenTofu registry / Terraform Cloud / git、跨 tool 兼容</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Terraform</th>
          <th>OpenTofu</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Binary cost</td>
          <td>免費 (community edition)</td>
          <td>免費（永遠）</td>
      </tr>
      <tr>
          <td>Terraform Cloud cost</td>
          <td>$20 / user / month、enterprise 高</td>
          <td>無對等服務（用 Atlantis / Spacelift / env0）</td>
      </tr>
      <tr>
          <td>State storage</td>
          <td>S3 / 自家 backend、低</td>
          <td>S3 / 自家 backend、低</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-5 person-day（含 audit + cutover + CI 改）</td>
      </tr>
      <tr>
          <td>License risk</td>
          <td>BSL 限制商業使用</td>
          <td>MPL 2.0 開源、無 license risk</td>
      </tr>
      <tr>
          <td>Long-term governance</td>
          <td>HashiCorp 單一供應商</td>
          <td>Linux Foundation + 多廠商貢獻</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：純 IaC 用戶切 OpenTofu 風險低 + 省 license 風險；重度依賴 Terraform Cloud feature 的 organization 保留或評估 commercial alternatives（Spacelift / env0）。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-atlantis--spacelift--env0-整合">跟 <a href="https://www.runatlantis.io/">Atlantis / Spacelift / env0</a> 整合</h3>
<p>OpenTofu 沒對等 Terraform Cloud、需要 third-party orchestrator：</p>
<ul>
<li><strong>Atlantis</strong>：自架、開源、輕量、適合中小型 team</li>
<li><strong>Spacelift</strong>：SaaS、policy as code、支援 OpenTofu first-class</li>
<li><strong>env0</strong>：SaaS、cost estimation、workflow 完整</li>
</ul>
<h3 id="跟-terragrunt-整合">跟 <a href="https://terragrunt.gruntwork.io/">Terragrunt</a> 整合</h3>
<p>Terragrunt（OpenTofu / Terraform 共用 wrapper）已支援 OpenTofu 1.6+；多環境配置抽象保留、底層 binary 切換無感。</p>
<h3 id="反向-migrationopentofu--terraform">反向 migration（OpenTofu → Terraform）</h3>
<p>罕見、通常是 organization 走商業合約綁 HashiCorp Enterprise 才會做；流程鏡像對稱、注意 OpenTofu 1.6+ 自家 feature（state encryption / provider for_each）在 Terraform 端可能缺。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>State encryption（OpenTofu 1.7+）</strong>：sensitive state 加密、Terraform 商業版才有對等 feature</li>
<li><strong>跨 IaC tool（Pulumi / CDK）</strong>：Pulumi / AWS CDK 是不同 paradigm（imperative）、不在本 migration scope</li>
<li><strong>Provider ecosystem 長期分裂</strong>：兩家 registry 自我演化、需要 quarterly review provider compat</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform</a></li>
<li>平行 migration playbook（Type B）：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> / <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 Process content 結構由最大差異維度決定</a></li>
</ul>
]]></content:encoded></item></channel></rss>