<?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>State on Tarragon</title><link>https://tarrragon.github.io/blog/tags/state/</link><description>Recent content in State on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/state/index.xml" rel="self" type="application/rss+xml"/><item><title>IaC 工具選型與 state 地基</title><link>https://tarrragon.github.io/blog/infra/01-minimal-iac/iac-tool-state-backend/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/01-minimal-iac/iac-tool-state-backend/</guid><description>&lt;h2 id="動手前的前提">動手前的前提&lt;/h2>
&lt;p>以下步驟是寫第一行 IaC 之前需要就位的前置條件。如果已經備妥可以跳過。如果是第一次接觸雲端帳號，先讀&lt;a href="https://tarrragon.github.io/blog/infra/00-infra-mindset/first-day-with-cloud-account/" data-link-title="拿到雲端帳號的第一天" data-link-desc="被指派 infra 工作、拿到 AWS 或 GCP 帳號、不確定該先做什麼時讀 — 第一小時安全底線、帳號現況判讀、後續學習路線分流">拿到雲端帳號的第一天&lt;/a>做安全底線和帳號現況判讀。&lt;/p>
&lt;p>&lt;strong>雲端帳號&lt;/strong>。需要一個 AWS 帳號（或 GCP / Azure，本模組以 AWS 為主要範例）。註冊完成後立刻對 root 帳號啟用 MFA（Multi-Factor Authentication）——root 帳號是整個雲端環境的最高權限，沒有 MFA 等於大門沒鎖。啟用路徑：AWS Console → 右上角帳號名稱 → Security credentials → Multi-factor authentication (MFA) → Assign MFA device。日常操作用 IAM user 或 IAM Identity Center 登入，root 帳號只在需要 root-only 操作時使用。&lt;/p>
&lt;p>&lt;strong>本機工具&lt;/strong>。安裝 IaC CLI（Terraform 或 OpenTofu）和雲端 CLI（AWS CLI）：&lt;/p>





&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"># macOS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">brew install opentofu awscli
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># Arch Linux（opentofu 和 aws-cli-v2 在 AUR，需要 AUR helper）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">yay -S opentofu-bin aws-cli-v2
&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 class="c1"># 驗證安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">tofu --version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">aws --version&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>雲端認證&lt;/strong>。本機需要能對雲端 API 認證。最直接的方式是用 AWS CLI 設定 credentials：&lt;/p>





&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">aws configure
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 輸入 Access Key ID、Secret Access Key、預設 region（如 ap-northeast-1）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這組 access key 來自 IAM user。如果帳號裡還沒有 IAM user，到 AWS Console → IAM → Users 建立一個、附加 &lt;code>AdministratorAccess&lt;/code> policy、在 Security credentials 分頁建立 access key。正式環境應該用 SSO 或 short-lived credentials 取代長期 key（&lt;a href="https://tarrragon.github.io/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二&lt;/a>會展開），但起步階段一組 IAM user key 足以讓 &lt;code>tofu apply&lt;/code> 跑起來。&lt;/p>
&lt;p>&lt;strong>Git repo&lt;/strong>。IaC 程式碼從 day 1 就應該在版本控制裡——這是&lt;a href="https://tarrragon.github.io/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零&lt;/a>「可重建路徑」的落地前提。建一個 Git repo，後續所有 &lt;code>.tf&lt;/code> 檔都放在這裡：&lt;/p>





&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">mkdir infra &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> infra
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;.terraform/&amp;#39;&lt;/span> &amp;gt; .gitignore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;*.tfstate&amp;#39;&lt;/span> &amp;gt;&amp;gt; .gitignore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;*.tfstate.*&amp;#39;&lt;/span> &amp;gt;&amp;gt; .gitignore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">git add .gitignore &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> git commit -m &lt;span class="s2">&amp;#34;init: gitignore for terraform&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.gitignore&lt;/code> 排除 &lt;code>.terraform/&lt;/code>（provider 快取）和 &lt;code>*.tfstate&lt;/code>（state 檔含敏感值，存放策略見下方 remote state 段落）。&lt;/p>
&lt;hr>
&lt;p>踏上&lt;a href="https://tarrragon.github.io/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">成熟度階梯&lt;/a>（從全手動到全程式碼治理的五階分級）第二階（宣告式 IaC，也就是 state 檔誕生那一階）的最小路徑，從兩件事開始：選對工具、把 state 管好。工具決定用什麼語言描述基礎設施，state 則是工具對雲端現實的唯一記憶。這份記憶存在哪、怎麼保護、怎麼防止並行寫壞，是整套 IaC 能不能站穩的地基。&lt;/p></description><content:encoded><![CDATA[<h2 id="動手前的前提">動手前的前提</h2>
<p>以下步驟是寫第一行 IaC 之前需要就位的前置條件。如果已經備妥可以跳過。如果是第一次接觸雲端帳號，先讀<a href="/blog/infra/00-infra-mindset/first-day-with-cloud-account/" data-link-title="拿到雲端帳號的第一天" data-link-desc="被指派 infra 工作、拿到 AWS 或 GCP 帳號、不確定該先做什麼時讀 — 第一小時安全底線、帳號現況判讀、後續學習路線分流">拿到雲端帳號的第一天</a>做安全底線和帳號現況判讀。</p>
<p><strong>雲端帳號</strong>。需要一個 AWS 帳號（或 GCP / Azure，本模組以 AWS 為主要範例）。註冊完成後立刻對 root 帳號啟用 MFA（Multi-Factor Authentication）——root 帳號是整個雲端環境的最高權限，沒有 MFA 等於大門沒鎖。啟用路徑：AWS Console → 右上角帳號名稱 → Security credentials → Multi-factor authentication (MFA) → Assign MFA device。日常操作用 IAM user 或 IAM Identity Center 登入，root 帳號只在需要 root-only 操作時使用。</p>
<p><strong>本機工具</strong>。安裝 IaC CLI（Terraform 或 OpenTofu）和雲端 CLI（AWS CLI）：</p>





<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"># macOS</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew install opentofu awscli
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Arch Linux（opentofu 和 aws-cli-v2 在 AUR，需要 AUR helper）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">yay -S opentofu-bin aws-cli-v2
</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 class="c1"># 驗證安裝</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">tofu --version
</span></span><span class="line"><span class="ln">9</span><span class="cl">aws --version</span></span></code></pre></div><p><strong>雲端認證</strong>。本機需要能對雲端 API 認證。最直接的方式是用 AWS CLI 設定 credentials：</p>





<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">aws configure
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 輸入 Access Key ID、Secret Access Key、預設 region（如 ap-northeast-1）</span></span></span></code></pre></div><p>這組 access key 來自 IAM user。如果帳號裡還沒有 IAM user，到 AWS Console → IAM → Users 建立一個、附加 <code>AdministratorAccess</code> policy、在 Security credentials 分頁建立 access key。正式環境應該用 SSO 或 short-lived credentials 取代長期 key（<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二</a>會展開），但起步階段一組 IAM user key 足以讓 <code>tofu apply</code> 跑起來。</p>
<p><strong>Git repo</strong>。IaC 程式碼從 day 1 就應該在版本控制裡——這是<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零</a>「可重建路徑」的落地前提。建一個 Git repo，後續所有 <code>.tf</code> 檔都放在這裡：</p>





<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">mkdir infra <span class="o">&amp;&amp;</span> <span class="nb">cd</span> infra
</span></span><span class="line"><span class="ln">2</span><span class="cl">git init
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;.terraform/&#39;</span> &gt; .gitignore
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;*.tfstate&#39;</span>  &gt;&gt; .gitignore
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;*.tfstate.*&#39;</span> &gt;&gt; .gitignore
</span></span><span class="line"><span class="ln">6</span><span class="cl">git add .gitignore <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;init: gitignore for terraform&#34;</span></span></span></code></pre></div><p><code>.gitignore</code> 排除 <code>.terraform/</code>（provider 快取）和 <code>*.tfstate</code>（state 檔含敏感值，存放策略見下方 remote state 段落）。</p>
<hr>
<p>踏上<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">成熟度階梯</a>（從全手動到全程式碼治理的五階分級）第二階（宣告式 IaC，也就是 state 檔誕生那一階）的最小路徑，從兩件事開始：選對工具、把 state 管好。工具決定用什麼語言描述基礎設施，state 則是工具對雲端現實的唯一記憶。這份記憶存在哪、怎麼保護、怎麼防止並行寫壞，是整套 IaC 能不能站穩的地基。</p>
<h2 id="iac-工具選型宣告式狀態管理-vs-程式語言抽象">IaC 工具選型：宣告式狀態管理 vs 程式語言抽象</h2>
<p>IaC 工具的核心職責是把「我要的基礎設施長什麼樣」描述成可版本控制的程式碼，再由工具負責算出現況與目標的差異並收斂。市場上的工具分成兩條路線，差別落在「用什麼語言描述」與「狀態由誰持有」這兩個軸上，而非功能多寡。</p>
<h3 id="宣告式-dsl-路線">宣告式 DSL 路線</h3>
<p>第一條路線的代表是 Terraform 與其開源分支 OpenTofu。寫的是 HCL（HashiCorp Configuration Language），描述的是資源的最終樣貌，工具自己維護一份 state 來追蹤每個資源的真實 ID 與屬性。</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="k">resource</span> <span class="s2">&#34;aws_s3_bucket&#34; &#34;artifacts&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="s2">&#34;acme-deploy-artifacts&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket_versioning&#34; &#34;artifacts&#34;</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">artifacts</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">versioning_configuration</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">    status</span> <span class="o">=</span> <span class="s2">&#34;Enabled&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  }
</span></span><span class="line"><span class="ln">10</span><span class="cl">}</span></span></code></pre></div><p>這段 HCL 描述的是「一個開了 versioning 的 S3 bucket 應該存在」。第一次 apply 時工具建立它，之後每次 apply 時工具比對 state 與雲端現況，只做差異收斂。讀的人看 HCL 就知道最終結果長什麼樣，不需要在腦中追蹤執行順序。</p>
<p>這條路線適合團隊成員背景混雜、需要讓非專職後端的人也能讀懂 infra 定義的情境 — HCL 的閱讀門檻低，diff 直觀，review 時看得出「這個 PR 會新增一個 RDS、改掉一條 security group」。缺點是 HCL 的表達力有限：遇到需要大量條件分支或動態生成的場景時，語法會變得笨拙，<code>count</code>、<code>for_each</code>、<code>dynamic</code> 區塊很快就堆出難以閱讀的嵌套。</p>
<h3 id="程式語言路線">程式語言路線</h3>
<p>第二條路線的代表是 AWS CDK 與 Pulumi。寫的是 TypeScript、Python、Go 這類語言，靠迴圈、函式、類別來生成資源。這條路線適合 infra 邏輯本身複雜、需要大量條件分支與抽象複用的團隊，例如要根據環境清單動態生成數十組對稱資源。</p>
<p>代價是 review 難度上升。一段 <code>for</code> 迴圈展開後到底建了哪些東西，得在腦中執行程式才看得出來，diff 不再等於變更本身。一個抽象類別改了一行建構子參數，展開後可能影響所有繼承它的資源，而 PR diff 上只看到那一行。對跨職能 review（PM、SRE、安全團隊都要看的變更）來說，這是可感知的閱讀成本。</p>
<h3 id="cdk-vs-pulumi狀態由誰持有">CDK vs Pulumi：狀態由誰持有</h3>
<p>CDK 與 Pulumi 同屬程式語言路線，但「狀態由誰持有」這個軸把它們再分開。</p>
<p>CDK 把程式碼 synth 成 CloudFormation 模板，再交給 CloudFormation 服務端執行與追蹤。state 由 AWS 代管 — 沒有一份 tfstate 檔要自己存放、加密、回捲，也不需要額外的鎖表來防並行。這份「狀態維運外包給雲端」正是 CDK 在 AWS 生態內的賣點之一。代價是綁定 CloudFormation 與單一雲 — CloudFormation 的更新速度、resource coverage、錯誤訊息品質都由 AWS 控制，團隊的 debug 能力受限於 CloudFormation 的回報粒度。</p>
<p>Pulumi 走另一邊：它維護一份自己的 state，預設交給 Pulumi Cloud 託管，也能改用 S3 之類的後端自管。形態上更接近 Terraform 的 state 模型，state 的存放、保護與並行控制重回團隊手上。同一條程式語言路線，選 CDK 等於把 state 責任讓給雲端，選 Pulumi 則保留對 state 落點的掌控。</p>
<h3 id="選型判準">選型判準</h3>
<p>選型看的是團隊組成與變更的審查需求，可以用一張決策表歸納：</p>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>宣告式 DSL（Terraform / OpenTofu）</th>
          <th>程式語言（CDK / Pulumi）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>diff 可讀性</td>
          <td>HCL diff 即是資源變更</td>
          <td>程式碼 diff，要展開才知道結果</td>
      </tr>
      <tr>
          <td>跨職能 review</td>
          <td>適合</td>
          <td>需要讀者熟悉程式語言</td>
      </tr>
      <tr>
          <td>抽象複用</td>
          <td>有限，靠 module + for_each</td>
          <td>完整程式語言能力</td>
      </tr>
      <tr>
          <td>state 管理</td>
          <td>自管或託管皆可</td>
          <td>CDK 交 AWS；Pulumi 自管或託管</td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>provider 生態支援多雲</td>
          <td>CDK 限 AWS；Pulumi 支援多雲</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>HCL 語法簡單，概念模型需適應</td>
          <td>語言本身熟悉，IaC 概念需適應</td>
      </tr>
  </tbody>
</table>
<p>若多數變更要跨職能 review、希望 diff 一眼可讀，宣告式 DSL 較划算；若 infra 由專職平台團隊維護、抽象複用的收益大於審查透明度的損失，程式語言路線較划算。</p>
<p>Terraform 與 OpenTofu 之間，OpenTofu 是授權變更後社群分叉出的相容實作，HCL 與 provider 生態幾乎共用；選擇主要看對授權條款與治理模式的偏好，技術判準在這一階沒有實質差異。本模組後續一律以 HCL 示意，換成任一宣告式工具判準仍成立。</p>
<p>上述兩條路線之外，還有兩類工具走不同的運作模型。Kubernetes-native 路線（代表是 Crossplane）用 CRD 描述雲資源、由 controller 持續收斂，state 由 Kubernetes 的 etcd 持有，適合已經重度投入 Kubernetes 的團隊。Serverless-first 框架（代表是 SST）把部署與 IaC 合一，適合全 serverless 架構。這兩條路線的 state 模型與 CLI 驅動的 plan/apply 流程不同，本系列不展開。</p>
<h2 id="state-是工具對現實的唯一記憶">state 是工具對現實的唯一記憶</h2>
<p>state 是 IaC 工具用來記錄「上一次 apply 之後，每個資源在雲端真實長什麼樣」的快照。它的作用是讓工具能算出「現況」與「目標」之間的最小差異。沒有 state，工具每次都得把所有資源重新查一遍才知道該不該動，而且無法分辨「這個資源是我建的、該由我管」還是「別人手動建的、不歸我管」。</p>
<p>一份 state 的實際內容大致長這樣（簡化版）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;resources&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;aws_s3_bucket&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;artifacts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nt">&#34;instances&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">          <span class="nt">&#34;attributes&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;acme-deploy-artifacts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nt">&#34;arn&#34;</span><span class="p">:</span> <span class="s2">&#34;arn:aws:s3:::acme-deploy-artifacts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="nt">&#34;bucket&#34;</span><span class="p">:</span> <span class="s2">&#34;acme-deploy-artifacts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="nt">&#34;tags&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;env&#34;</span><span class="p">:</span> <span class="s2">&#34;prod&#34;</span><span class="p">,</span> <span class="nt">&#34;owner&#34;</span><span class="p">:</span> <span class="s2">&#34;platform&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>state 裡通常含有資源的真實 ID、相依關係，以及部分敏感屬性 — 例如資料庫的初始密碼、private key 的輸出值、加密金鑰的 ARN。這帶來兩條硬邊界，違反任一條都會在未來製造代價高昂的事故。</p>
<h3 id="state-絕不能進-git">state 絕不能進 git</h3>
<p>state 含明文敏感值，一旦推進版控就等於把密碼寫進每個 clone 的歷史裡。事後 rotate 密碼也清不掉 git 歷史 — 因為 git 是 append-only 的，舊版本的 state 永遠留在 commit 裡，除非用 <code>git filter-branch</code> 或 <code>git filter-repo</code> 重寫整條歷史（這本身是一個破壞性操作，會影響所有已經 clone 的副本）。</p>
<p>在 <code>.gitignore</code> 裡搜尋 <code>*.tfstate</code> 和 <code>*.tfstate.backup</code>——如果這兩行不在，state 有進版控的風險。在 repo 根目錄執行一次搜索確認：</p>





<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">git log --all --diff-filter<span class="o">=</span>A -- <span class="s1">&#39;*.tfstate&#39;</span></span></span></code></pre></div><p>如果有任何結果，代表 state 曾經被 commit 過，那些 commit 裡的敏感值已經暴露。</p>
<h3 id="state-不能只放本地">state 不能只放本地</h3>
<p>本地 state 的失敗模式是它把整份基礎設施的記憶綁在一台筆電上 — 換人接手、換台機器、或多人同時 apply 時，記憶就分裂了。</p>
<p>具體場景：工程師 A 在自己的筆電 apply 了一次，state 記住「已經建了 3 個 security group」。工程師 B 在另一台筆電上拉了同一份 code，但她的本地沒有 state（或有一份過時的 state），apply 時工具以為那 3 個 security group 不存在，又建了 3 個重複的。更糟的場景是 B 的 state 比 A 舊，工具對比後認為 A 後來新增的 security group「不在記憶裡、是多餘的」，於是 apply 時把它們刪掉 — 而 A 還以為那些規則還在保護服務。</p>
<p>這兩條邊界共同指向同一個結論：state 需要一個團隊共享、有版本、有存取控制、且能防止同時寫入的存放處。這就是 remote state backend 要解的問題。</p>
<h2 id="remote-state-backend自管-vs-託管">remote state backend：自管 vs 託管</h2>
<p>remote state backend 是把 state 從本地移到團隊共享儲存的機制，它要同時滿足三件事：持久保存、防止並行寫入衝突、以及保護敏感內容。達成方式分成自管儲存與託管服務兩種，差別在維運責任落在誰身上。</p>
<h3 id="自管-backend">自管 backend</h3>
<p>自管路線以雲端物件儲存加鎖機制為典型組合。以 AWS 為例，state 檔放 S3、用一張 DynamoDB 鎖表防止兩個人同時 apply：</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="k">terraform</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">backend</span> <span class="s2">&#34;s3&#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;acme-tf-state&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">    key</span>            <span class="o">=</span> <span class="s2">&#34;prod/network/terraform.tfstate&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">    region</span>         <span class="o">=</span> <span class="s2">&#34;ap-northeast-1&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">    encrypt</span>        <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">    dynamodb_table</span> <span class="o">=</span> <span class="s2">&#34;acme-tf-lock&#34;</span>
</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></span></code></pre></div><p>這段設定的每一項都對應前一節的一條邊界：</p>
<p><strong><code>encrypt = true</code></strong> 讓 state 在 S3 落地時加密，回應「state 含敏感值」的風險。加密用的是 S3 的 server-side encryption，搭配 KMS key 可以進一步控制誰能解密。</p>
<p><strong>bucket versioning</strong> 是這段設定裡沒有出現、但在建立 bucket 時就該開的屬性。apply 寫壞或誤刪 state 時，versioning 是把記憶回捲到上一個正確版本的唯一退路。沒開的話一次壞寫就讓工具失去對現實的記憶，而回復的唯一方式是從雲端逐個資源重新 import。建立 state bucket 的 HCL 應該同時開 versioning 與刪除保護：</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="k">resource</span> <span class="s2">&#34;aws_s3_bucket_versioning&#34; &#34;state&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">tf_state</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">versioning_configuration</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    status</span> <span class="o">=</span> <span class="s2">&#34;Enabled&#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></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="k">resource</span> <span class="s2">&#34;aws_s3_bucket_lifecycle_configuration&#34; &#34;state&#34;</span> {
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">tf_state</span><span class="p">.</span><span class="k">id</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="k">rule</span> {
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">    id</span>     <span class="o">=</span> <span class="s2">&#34;retain-old-versions&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">    status</span> <span class="o">=</span> <span class="s2">&#34;Enabled&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">noncurrent_version_expiration</span> {
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">      noncurrent_days</span> <span class="o">=</span> <span class="m">90</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    }
</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></span></code></pre></div><p>舊版本的保留天數是成本與安全的取捨。90 天足以涵蓋大多數「發現 state 壞了再回去找正確版本」的時間差 — 超過 90 天才發現的 state 問題通常已經被後續 apply 覆蓋，回捲到更早的版本反而引入更大的落差。</p>
<p><strong><code>dynamodb_table</code></strong> 指向一張鎖表。apply 開始時寫入一筆鎖、結束才釋放，第二個人同時跑就會被擋下並提示鎖被誰持有。這正是本地 state 無法提供、卻是多人協作底線的並行保護。鎖表本身的建立只需要幾行 HCL：</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="k">resource</span> <span class="s2">&#34;aws_dynamodb_table&#34; &#34;tf_lock&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name</span>         <span class="o">=</span> <span class="s2">&#34;acme-tf-lock&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  billing_mode</span> <span class="o">=</span> <span class="s2">&#34;PAY_PER_REQUEST&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  hash_key</span>     <span class="o">=</span> <span class="s2">&#34;LockID&#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="k">attribute</span> {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;LockID&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">    type</span> <span class="o">=</span> <span class="s2">&#34;S&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  }
</span></span><span class="line"><span class="ln">10</span><span class="cl">}</span></span></code></pre></div><p>鎖表用 PAY_PER_REQUEST 模式足夠，因為它的讀寫頻率很低（只在 apply 開始和結束時各一次）。鎖卡住時（apply 中途失敗、沒有正常釋放鎖），用 <code>terraform force-unlock &lt;lock-id&gt;</code> 手動釋放，但前提是確認沒有其他 apply 正在執行。</p>
<p><strong><code>key</code></strong> 是 state 在 bucket 內的路徑，這裡先用 <code>prod/network</code> 之類的分層命名。實際怎麼依環境切分 state 留待<a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>展開。</p>
<h3 id="自管-backend-的雞生蛋問題">自管 backend 的雞生蛋問題</h3>
<p>自管 backend 有一個啟動悖論：state bucket 和 lock table 本身也是雲端資源，它們該由誰來管理？用 Terraform 管理 Terraform 的 backend？</p>
<p>務實的做法是接受這個循環：用一份獨立的、最小化的 Terraform code 來建立 state bucket 和 lock table，這份 code 用 local state（因為它只在啟動那一次跑）。建立完成後，所有後續的 Terraform code 都指向這個 remote backend。這份啟動 code 的 local state 可以 commit 進 repo（它不含敏感值，只有 bucket 和 DynamoDB table 的 ID），或直接在跑完後丟棄 — 因為這些資源如果需要重建，幾行 CLI 就能做到。</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"># bootstrap/main.tf — 只用一次，建立 state 基礎設施
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">terraform</span> {<span class="c1">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">  # 刻意用 local state，因為 remote backend 還不存在
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></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="k">resource</span> <span class="s2">&#34;aws_s3_bucket&#34; &#34;tf_state&#34;</span> {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="s2">&#34;acme-tf-state&#34;</span>
</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></span><span class="line"><span class="ln">10</span><span class="cl"><span class="err">#</span> <span class="p">...</span> <span class="k">versioning</span><span class="p">,</span> <span class="k">encryption</span><span class="p">,</span> <span class="k">lock</span> <span class="k">table</span></span></span></code></pre></div><h3 id="託管-backend">託管 backend</h3>
<p>託管路線把上述維運細節包起來，由 Terraform Cloud、Spacelift、env0 這類平台代管 state、鎖與加密，附帶 web UI 與 audit log。</p>
<p>判讀訊號是團隊規模與維運餘裕。自管 backend 的成本是要自己把 bucket versioning、加密、鎖表、IAM 權限配對，配錯任何一項都可能讓 state 失去保護 — 例如忘了開 versioning，一次壞寫就回不去。託管服務用月費換掉這份配置與維運負擔，代價是 state 託付給第三方、且進階治理功能常綁在付費級距。</p>
<p>小團隊起步、不想第一週就花在配 backend 上，託管較划算。對 state 存放位置有合規或主權要求、或希望基礎設施盡量自持的團隊，自管較划算。託管服務（Terraform Cloud / Spacelift）的免費方案涵蓋基本功能，付費級距約 $20-70/user/月；自管 backend 的成本是初次配置半天到一天的工程師時間，加上持續的 IAM 權限與 versioning 維護。</p>
<p>導入時程參考：最小可行 IaC（state backend + 第一批地基資源）的導入約需 2-3 天工程師時間。第一個可見里程碑是「一條指令能在新帳號重建整個地基環境」。之後每批服務的納管約 1-2 天/批，依資源複雜度而定。</p>
<p>State 地基設好後，下一步是立 Console 唯讀鐵律、並用最小可行資源集合驗證整條鏈路，見<a href="/blog/infra/01-minimal-iac/console-readonly-minimal-viable/" data-link-title="Console 唯讀鐵律與最小可行資源集合" data-link-desc="Console 只用來看不用來改的操作紀律、drift 的延遲浮現與偵測，以及能跑出第一個完整 apply 迴路的最小資源集合">Console 唯讀鐵律與最小可行資源集合</a>。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/01-minimal-iac/console-readonly-minimal-viable/" data-link-title="Console 唯讀鐵律與最小可行資源集合" data-link-desc="Console 只用來看不用來改的操作紀律、drift 的延遲浮現與偵測，以及能跑出第一個完整 apply 迴路的最小資源集合">Console 唯讀鐵律與最小可行資源集合</a>：state 管好之後，Console 唯讀紀律與最小 apply 閉環</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：Console 唯讀鐵律靠權限落地</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>：state 的 key 怎麼依環境切分</li>
</ul>
]]></content:encoded></item><item><title>模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律</title><link>https://tarrragon.github.io/blog/infra/01-minimal-iac/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/01-minimal-iac/</guid><description>&lt;p>踏上成熟度階梯第二階（宣告式 IaC，也就是 state 檔誕生那一階，見模組零：infra 是什麼）的最小路徑，只做兩件具體的事：把 state 管好，並立下所有資源都走程式碼的鐵律。這兩件事決定了往後每一階的地基穩不穩 — state 是 IaC 工具對現實的唯一記憶，Console 唯讀鐵律則保證這份記憶不會在背後被偷偷改掉。其餘的網路、身分、服務都還沒上場，先把這兩件事釘死，後面的擴張才有可重現的起點。&lt;/p>
&lt;h2 id="iac-工具選型宣告式狀態管理-vs-程式語言抽象">IaC 工具選型：宣告式狀態管理 vs 程式語言抽象&lt;/h2>
&lt;p>IaC 工具的核心職責是把「我要的基礎設施長什麼樣」描述成可版本控制的程式碼，再由工具負責算出現況與目標的差異並收斂。市場上的工具分成兩條路線，差別落在「用什麼語言描述」與「狀態由誰持有」這兩個軸上，而非功能多寡。&lt;/p>
&lt;p>第一條路線是宣告式 DSL，代表是 Terraform 與其開源分支 OpenTofu。寫的是 HCL，描述的是資源的最終樣貌，工具自己維護一份 state 來追蹤每個資源的真實 ID 與屬性。這條路線適合團隊成員背景混雜、需要讓非專職後端的人也能讀懂 infra 定義的情境 — HCL 的閱讀門檻低，diff 直觀，review 時看得出「這個 PR 會新增一個 RDS、改掉一條 security group」。&lt;/p>
&lt;p>第二條路線是用通用程式語言寫 infra，代表是 AWS CDK 與 Pulumi。寫的是 TypeScript、Python、Go 這類語言，靠迴圈、函式、類別來生成資源。這條路線適合 infra 邏輯本身複雜、需要大量條件分支與抽象複用的團隊，例如要根據環境清單動態生成數十組對稱資源。代價是 review 難度上升：一段 &lt;code>for&lt;/code> 迴圈展開後到底建了哪些東西，得在腦中執行程式才看得出來，diff 不再等於變更本身。&lt;/p>
&lt;p>CDK 與 Pulumi 同屬程式語言路線，但「狀態由誰持有」這個軸把它們再分開。CDK 把程式碼 synth 成 CloudFormation 模板，再交給 CloudFormation 服務端執行與追蹤，state 由 AWS 代管 — 沒有一份 tfstate 檔要自己存放、加密、回捲，也不需要額外的鎖表來防並行，這份「狀態維運外包給雲端」正是 CDK 在 AWS 生態內的賣點之一，代價是綁定 CloudFormation 與單一雲。Pulumi 走的是另一邊：它維護一份自己的 state，預設交給 Pulumi Cloud 託管，也能改用 S3 之類的後端自管 — 形態上更接近 Terraform 的 state 模型，state 的存放、保護與並行控制重回團隊手上。同一條程式語言路線，選 CDK 等於把 state 責任讓給雲端，選 Pulumi 則保留對 state 落點的掌控。&lt;/p>
&lt;p>選型看的是團隊組成與變更的審查需求。若多數變更要跨職能 review、希望 diff 一眼可讀，宣告式 DSL 較划算；若 infra 由專職平台團隊維護、抽象複用的收益大於審查透明度的損失，程式語言路線較划算。Terraform 與 OpenTofu 之間，OpenTofu 是授權變更後社群分叉出的相容實作，HCL 與 provider 生態幾乎共用；選擇主要看對授權條款與治理模式的偏好，技術判準在這一階沒有實質差異。本模組後續一律以 HCL 示意，換成任一宣告式工具判準仍成立。&lt;/p>
&lt;h2 id="state-是工具對現實的唯一記憶">state 是工具對現實的唯一記憶&lt;/h2>
&lt;p>state 是 IaC 工具用來記錄「上一次 apply 之後，每個資源在雲端真實長什麼樣」的快照，它的作用是讓工具能算出「現況」與「目標」之間的最小差異。沒有 state，工具每次都得把所有資源重新查一遍才知道該不該動，而且無法分辨「這個資源是我建的、該由我管」還是「別人手動建的、不歸我管」。&lt;/p>
&lt;p>state 裡通常含有資源的真實 ID、相依關係，以及部分敏感屬性 — 例如資料庫的初始密碼、private key 的輸出值。這帶來兩條邊界。&lt;/p>
&lt;p>第一條：state 絕不能進 git。state 含明文敏感值，一旦推進版控就等於把密碼寫進每個 clone 的歷史裡，事後 rotate 也清不掉 git 歷史。&lt;/p>
&lt;p>第二條：state 不能只放本地。本地 state 的失敗模式是它把整份基礎設施的記憶綁在一台筆電上 — 換人接手、換台機器、或多人同時 apply 時，記憶就分裂了。兩個人各自拿著不同版本的本地 state 去 apply，工具會用各自過時的記憶去算差異，互相把對方建的資源判定成「不該存在、刪掉」，基礎設施被反覆來回破壞。&lt;/p>
&lt;p>這兩條邊界共同指向同一個結論：state 需要一個團隊共享、有版本、有存取控制、且能防止同時寫入的存放處。這就是 remote state backend 要解的問題。&lt;/p>
&lt;h2 id="remote-state-backend自管-vs-託管">remote state backend：自管 vs 託管&lt;/h2>
&lt;p>remote state backend 是把 state 從本地移到團隊共享儲存的機制，它要同時滿足三件事：持久保存、防止並行寫入衝突、以及保護敏感內容。達成方式分成自管儲存與託管服務兩種，差別在維運責任落在誰身上。&lt;/p>
&lt;p>自管路線以雲端物件儲存加鎖機制為典型組合。以 AWS 為例，state 檔放 S3、用一張鎖表防止兩個人同時 apply：&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="k">terraform&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">backend&lt;/span> &lt;span class="s2">&amp;#34;s3&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;acme-tf-state&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"> key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;prod/network/terraform.tfstate&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 class="n"> region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;ap-northeast-1&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 class="n"> encrypt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="n"> dynamodb_table&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;acme-tf-lock&amp;#34;&lt;/span>
&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段設定的每一項都對應前一節的一條邊界，值得逐項拆開。&lt;code>encrypt = true&lt;/code> 讓 state 在 S3 落地時加密，回應「state 含敏感值」的風險。承載 state 的 bucket 必須開 versioning：apply 寫壞或誤刪 state 時，versioning 是把記憶回捲到上一個正確版本的唯一退路，沒開的話一次壞寫就讓工具失去對現實的記憶。&lt;code>dynamodb_table&lt;/code> 指向一張鎖表，apply 開始時寫入一筆鎖、結束才釋放，第二個人同時跑就會被擋下並提示鎖被誰持有 — 這正是本地 state 無法提供、卻是多人協作底線的並行保護。&lt;code>key&lt;/code> 則是 state 在 bucket 內的路徑，這裡先用 &lt;code>prod/network&lt;/code> 之類的分層命名，實際怎麼依環境切分 state 留待模組四：環境分離與模組化展開。&lt;/p></description><content:encoded><![CDATA[<p>踏上成熟度階梯第二階（宣告式 IaC，也就是 state 檔誕生那一階，見模組零：infra 是什麼）的最小路徑，只做兩件具體的事：把 state 管好，並立下所有資源都走程式碼的鐵律。這兩件事決定了往後每一階的地基穩不穩 — state 是 IaC 工具對現實的唯一記憶，Console 唯讀鐵律則保證這份記憶不會在背後被偷偷改掉。其餘的網路、身分、服務都還沒上場，先把這兩件事釘死，後面的擴張才有可重現的起點。</p>
<h2 id="iac-工具選型宣告式狀態管理-vs-程式語言抽象">IaC 工具選型：宣告式狀態管理 vs 程式語言抽象</h2>
<p>IaC 工具的核心職責是把「我要的基礎設施長什麼樣」描述成可版本控制的程式碼，再由工具負責算出現況與目標的差異並收斂。市場上的工具分成兩條路線，差別落在「用什麼語言描述」與「狀態由誰持有」這兩個軸上，而非功能多寡。</p>
<p>第一條路線是宣告式 DSL，代表是 Terraform 與其開源分支 OpenTofu。寫的是 HCL，描述的是資源的最終樣貌，工具自己維護一份 state 來追蹤每個資源的真實 ID 與屬性。這條路線適合團隊成員背景混雜、需要讓非專職後端的人也能讀懂 infra 定義的情境 — HCL 的閱讀門檻低，diff 直觀，review 時看得出「這個 PR 會新增一個 RDS、改掉一條 security group」。</p>
<p>第二條路線是用通用程式語言寫 infra，代表是 AWS CDK 與 Pulumi。寫的是 TypeScript、Python、Go 這類語言，靠迴圈、函式、類別來生成資源。這條路線適合 infra 邏輯本身複雜、需要大量條件分支與抽象複用的團隊，例如要根據環境清單動態生成數十組對稱資源。代價是 review 難度上升：一段 <code>for</code> 迴圈展開後到底建了哪些東西，得在腦中執行程式才看得出來，diff 不再等於變更本身。</p>
<p>CDK 與 Pulumi 同屬程式語言路線，但「狀態由誰持有」這個軸把它們再分開。CDK 把程式碼 synth 成 CloudFormation 模板，再交給 CloudFormation 服務端執行與追蹤，state 由 AWS 代管 — 沒有一份 tfstate 檔要自己存放、加密、回捲，也不需要額外的鎖表來防並行，這份「狀態維運外包給雲端」正是 CDK 在 AWS 生態內的賣點之一，代價是綁定 CloudFormation 與單一雲。Pulumi 走的是另一邊：它維護一份自己的 state，預設交給 Pulumi Cloud 託管，也能改用 S3 之類的後端自管 — 形態上更接近 Terraform 的 state 模型，state 的存放、保護與並行控制重回團隊手上。同一條程式語言路線，選 CDK 等於把 state 責任讓給雲端，選 Pulumi 則保留對 state 落點的掌控。</p>
<p>選型看的是團隊組成與變更的審查需求。若多數變更要跨職能 review、希望 diff 一眼可讀，宣告式 DSL 較划算；若 infra 由專職平台團隊維護、抽象複用的收益大於審查透明度的損失，程式語言路線較划算。Terraform 與 OpenTofu 之間，OpenTofu 是授權變更後社群分叉出的相容實作，HCL 與 provider 生態幾乎共用；選擇主要看對授權條款與治理模式的偏好，技術判準在這一階沒有實質差異。本模組後續一律以 HCL 示意，換成任一宣告式工具判準仍成立。</p>
<h2 id="state-是工具對現實的唯一記憶">state 是工具對現實的唯一記憶</h2>
<p>state 是 IaC 工具用來記錄「上一次 apply 之後，每個資源在雲端真實長什麼樣」的快照，它的作用是讓工具能算出「現況」與「目標」之間的最小差異。沒有 state，工具每次都得把所有資源重新查一遍才知道該不該動，而且無法分辨「這個資源是我建的、該由我管」還是「別人手動建的、不歸我管」。</p>
<p>state 裡通常含有資源的真實 ID、相依關係，以及部分敏感屬性 — 例如資料庫的初始密碼、private key 的輸出值。這帶來兩條邊界。</p>
<p>第一條：state 絕不能進 git。state 含明文敏感值，一旦推進版控就等於把密碼寫進每個 clone 的歷史裡，事後 rotate 也清不掉 git 歷史。</p>
<p>第二條：state 不能只放本地。本地 state 的失敗模式是它把整份基礎設施的記憶綁在一台筆電上 — 換人接手、換台機器、或多人同時 apply 時，記憶就分裂了。兩個人各自拿著不同版本的本地 state 去 apply，工具會用各自過時的記憶去算差異，互相把對方建的資源判定成「不該存在、刪掉」，基礎設施被反覆來回破壞。</p>
<p>這兩條邊界共同指向同一個結論：state 需要一個團隊共享、有版本、有存取控制、且能防止同時寫入的存放處。這就是 remote state backend 要解的問題。</p>
<h2 id="remote-state-backend自管-vs-託管">remote state backend：自管 vs 託管</h2>
<p>remote state backend 是把 state 從本地移到團隊共享儲存的機制，它要同時滿足三件事：持久保存、防止並行寫入衝突、以及保護敏感內容。達成方式分成自管儲存與託管服務兩種，差別在維運責任落在誰身上。</p>
<p>自管路線以雲端物件儲存加鎖機制為典型組合。以 AWS 為例，state 檔放 S3、用一張鎖表防止兩個人同時 apply：</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="k">terraform</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">backend</span> <span class="s2">&#34;s3&#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;acme-tf-state&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">    key</span>            <span class="o">=</span> <span class="s2">&#34;prod/network/terraform.tfstate&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">    region</span>         <span class="o">=</span> <span class="s2">&#34;ap-northeast-1&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">    encrypt</span>        <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">    dynamodb_table</span> <span class="o">=</span> <span class="s2">&#34;acme-tf-lock&#34;</span>
</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></span></code></pre></div><p>這段設定的每一項都對應前一節的一條邊界，值得逐項拆開。<code>encrypt = true</code> 讓 state 在 S3 落地時加密，回應「state 含敏感值」的風險。承載 state 的 bucket 必須開 versioning：apply 寫壞或誤刪 state 時，versioning 是把記憶回捲到上一個正確版本的唯一退路，沒開的話一次壞寫就讓工具失去對現實的記憶。<code>dynamodb_table</code> 指向一張鎖表，apply 開始時寫入一筆鎖、結束才釋放，第二個人同時跑就會被擋下並提示鎖被誰持有 — 這正是本地 state 無法提供、卻是多人協作底線的並行保護。<code>key</code> 則是 state 在 bucket 內的路徑，這裡先用 <code>prod/network</code> 之類的分層命名，實際怎麼依環境切分 state 留待模組四：環境分離與模組化展開。</p>
<p>託管路線把這些維運細節包起來，由 Terraform Cloud、Spacelift 這類平台代管 state、鎖與加密，附帶 web UI 與 audit log。判讀訊號是團隊規模與維運餘裕：自管 backend 的成本是要自己把 bucket versioning、加密、鎖表、IAM 權限配對，配錯任何一項都可能讓 state 失去保護；託管服務用月費換掉這份配置與維運負擔，代價是 state 託付給第三方、且進階治理功能常綁在付費級距。小團隊起步、不想第一週就花在配 backend 上，託管較划算；對 state 存放位置有合規或主權要求、或希望基礎設施盡量自持的團隊，自管較划算。</p>
<h2 id="console-唯讀鐵律把-console-當儀表板不當方向盤">Console 唯讀鐵律：把 Console 當儀表板，不當方向盤</h2>
<p>Console 唯讀鐵律是一條操作紀律：雲端 Console 只用來觀察與排查，所有會改變資源的動作都回到程式碼走 apply。這條紀律維護的是 state 與現實的一致 — IaC 工具能正確運作的前提，是它的 state 反映得了真實世界，而每一次在 Console 點按鈕改設定，都是在 state 不知情的情況下動了現實。</p>
<p>這種 state 與現實的分歧叫 drift。drift 的代價會延遲引爆，而非當下浮現。某人在 Console 臨時把一條 security group 規則打開救火，state 並不知道；下一次別人為了不相干的變更跑 apply，工具拿過時的 state 去比對，會把那條手動規則判定成「不在我的記憶裡、刪掉」，於是悄悄關掉，救火的洞重新出現，而且沒人在 PR 裡看得到這件事發生過。Console 改得越多、與程式碼分歧越久，某次例行 apply 就越可能掃掉一批沒人記得的手動設定。</p>
<p>鐵律越早立越好，因為回頭納管的代價隨時間累積。手動建的資源要納入 IaC，得先用 <code>terraform import</code> 把現實資源綁進 state，再補一段與現實完全吻合的 HCL：</p>





<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">terraform import aws_security_group.web sg-0abc123def456</span></span></code></pre></div><p>import 只把資源 ID 寫進 state，不會幫忙生程式碼。那個資源在 Console 上被點出來的每一個屬性 — 每條 ingress 規則、每個 tag、每項關聯設定 — 都得一字不差地補成 HCL，任何一項對不上，下次 apply 就會試圖把現實改回程式碼寫的版本。一個資源還能忍，等到累積了幾十個各自手動微調過的資源才想納管，逆向工程的工作量會大到讓人乾脆放棄，基礎設施就此分裂成「程式碼管的」與「沒人敢動的」兩塊。第一天就立鐵律，要納管的存量永遠是零。</p>
<p>讓鐵律落地靠的是權限、不是自律。光靠約定「別在 Console 改」撐不久，救火當下手最快的永遠是 Console。真正讓鐵律站得住的，是把人的日常身分收斂成唯讀、把寫入權限留給跑 apply 的自動化身分，讓「在 Console 改不動」變成預設狀態 — 這道權限地基屬於模組二：身分與憑證地基的範圍，本階先確立紀律方向。</p>
<h2 id="最小可行能-apply-出一個完整環境的最小資源集合">最小可行：能 apply 出一個完整環境的最小資源集合</h2>
<p>最小可行 IaC 的目標是用最少的資源，跑出一條「改程式碼 → review → apply → 環境真的變了」的完整迴路。它承擔的責任是驗證地基本身能動，把所有服務都搬上來是後面的事。判準是這套程式碼能獨立 apply 出一個雖小但自洽、別人能重現的環境。</p>
<p>這一階的最小集合通常包含：一個設定好 versioning、加密與鎖表的 remote state backend；一個收斂後人類唯讀的身分權限基線；一個能放東西的網路骨架（一個 VPC 加最少的 subnet）；以及一個微不足道但真實存在的資源（例如一個 S3 bucket 或一台最小的測試機），用來證明 apply 確實作用到了雲端。把這個微小資源刻意留在最小集合裡，是因為它是最便宜的端到端驗證 — apply 跑完後它真的出現、<code>terraform destroy</code> 後它真的消失，就證明從程式碼到雲端的整條鏈路是通的。</p>
<p>刻意不放進來的東西同樣重要：正式的應用服務、資料庫、跨環境的複製、複雜的模組抽象，全部留到地基驗證通過之後。在 state 與 Console 唯讀都還沒站穩前就堆服務，等於把房子蓋在還沒灌漿的地基上。網路骨架怎麼長、身分怎麼切，分別由模組三：網路地基與模組二：身分與憑證地基接手深入；這一階只需要它們各自最薄的一層，湊出一個能 apply、能 destroy、能交接的閉環。</p>
<p>第一篇文章開頭有一段「動手前的前提」，列出寫第一行 IaC 之前需要就位的前置條件（雲端帳號 + MFA、CLI 工具安裝、雲端認證、Git repo 初始化）。已經備妥的讀者可以跳過。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/01-minimal-iac/iac-tool-state-backend/" data-link-title="IaC 工具選型與 state 地基" data-link-desc="Terraform / OpenTofu / CDK / Pulumi 的選型判準，state 作為 IaC 工具對現實的唯一記憶，以及 remote state backend 的自管與託管路線">IaC 工具選型與 state 地基</a></td>
          <td>Terraform / OpenTofu / CDK / Pulumi 選型判準，state 作為唯一記憶，remote state backend 的自管與託管路線</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/01-minimal-iac/console-readonly-minimal-viable/" data-link-title="Console 唯讀鐵律與最小可行資源集合" data-link-desc="Console 只用來看不用來改的操作紀律、drift 的延遲浮現與偵測，以及能跑出第一個完整 apply 迴路的最小資源集合">Console 唯讀鐵律與最小可行資源集合</a></td>
          <td>Console 唯讀的操作紀律、drift 的延遲引爆與偵測，以及第一個完整 apply 迴路的最小資源集合</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：Console 唯讀鐵律靠權限落地，人類唯讀、自動化身分持有寫入權</li>
<li>→ <a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>：最小集合裡的 VPC 與 subnet 怎麼設計</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>：state 的 key 怎麼依環境切分、state 跟環境怎麼對應</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：state 變更與 apply 怎麼納入 review</li>
<li>→ <a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運</a>：接手既有環境後的 IaC 導入路徑</li>
</ul>
]]></content:encoded></item><item><title>State（IaC 狀態檔）</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/state/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/state/</guid><description>&lt;p>State 是 IaC 工具用來記錄「上一次 apply 之後，每個資源在雲端長什麼樣」的快照。它的作用是讓工具能算出「程式碼描述的目標」與「雲端上的現況」之間的最小差異。沒有 state，工具每次都得把所有資源重新查一遍才知道該不該動，而且無法分辨「這個資源是我建的、該由我管」還是「別人手動建的、不歸我管」。&lt;/p>
&lt;p>State 裡通常含有資源的真實 ID、相依關係，以及部分敏感屬性（例如資料庫的初始密碼、private key 的輸出值）。這帶來兩條硬邊界：state 不能進 git（含敏感值，推進版控等於把密碼寫進每個 clone 的歷史）、state 不能只放本地（本地 state 的失敗模式是記憶綁在一台筆電上，多人並行 apply 會互相覆蓋）。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>State 是 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">IaC&lt;/a> 的記憶機制。&lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/iac-tool-state-backend/" data-link-title="IaC 工具選型與 state 地基" data-link-desc="Terraform / OpenTofu / CDK / Pulumi 的選型判準，state 作為 IaC 工具對現實的唯一記憶，以及 remote state backend 的自管與託管路線">模組一：最小可行 IaC&lt;/a> 的核心主題就是怎麼把 state 管好——remote backend、加密、鎖機制。State 管不好，後續所有 IaC 操作都建立在不可靠的記憶上。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>State 出問題的訊號包括：&lt;code>terraform plan&lt;/code> 顯示大量非預期的變更（state 與現實不一致）、兩個人同時 apply 後環境出現矛盾狀態、&lt;code>state list&lt;/code> 的資源數與 Console 上看到的不一致。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>管理 state 時要決定：&lt;/p>
&lt;ul>
&lt;li>存放位置：S3 + DynamoDB（自管）vs Terraform Cloud（託管），取捨在維運負擔 vs 控制權&lt;/li>
&lt;li>加密：state 含敏感值，落地加密（S3 SSE）是底線&lt;/li>
&lt;li>版本保留：bucket versioning 讓 state 損壞時能回捲到上一個正確版本&lt;/li>
&lt;li>鎖機制：防止兩個人同時 apply 互相覆蓋&lt;/li>
&lt;li>分割策略：一個大 state vs 多個小 state，取捨在引用便利性 vs 影響範圍控制&lt;/li>
&lt;/ul>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">IaC&lt;/a> — state 是 IaC 工具的核心依賴&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/drift/" data-link-title="Drift（設定漂移）" data-link-desc="IaC 的 state 與雲端實際狀態之間的不一致，通常因為有人繞過 IaC 直接在 Console 改設定">Drift&lt;/a> — state 與現實的落差&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>State 是 IaC 工具用來記錄「上一次 apply 之後，每個資源在雲端長什麼樣」的快照。它的作用是讓工具能算出「程式碼描述的目標」與「雲端上的現況」之間的最小差異。沒有 state，工具每次都得把所有資源重新查一遍才知道該不該動，而且無法分辨「這個資源是我建的、該由我管」還是「別人手動建的、不歸我管」。</p>
<p>State 裡通常含有資源的真實 ID、相依關係，以及部分敏感屬性（例如資料庫的初始密碼、private key 的輸出值）。這帶來兩條硬邊界：state 不能進 git（含敏感值，推進版控等於把密碼寫進每個 clone 的歷史）、state 不能只放本地（本地 state 的失敗模式是記憶綁在一台筆電上，多人並行 apply 會互相覆蓋）。</p>
<h2 id="概念位置">概念位置</h2>
<p>State 是 <a href="/blog/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">IaC</a> 的記憶機制。<a href="/blog/infra/01-minimal-iac/iac-tool-state-backend/" data-link-title="IaC 工具選型與 state 地基" data-link-desc="Terraform / OpenTofu / CDK / Pulumi 的選型判準，state 作為 IaC 工具對現實的唯一記憶，以及 remote state backend 的自管與託管路線">模組一：最小可行 IaC</a> 的核心主題就是怎麼把 state 管好——remote backend、加密、鎖機制。State 管不好，後續所有 IaC 操作都建立在不可靠的記憶上。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>State 出問題的訊號包括：<code>terraform plan</code> 顯示大量非預期的變更（state 與現實不一致）、兩個人同時 apply 後環境出現矛盾狀態、<code>state list</code> 的資源數與 Console 上看到的不一致。</p>
<h2 id="設計責任">設計責任</h2>
<p>管理 state 時要決定：</p>
<ul>
<li>存放位置：S3 + DynamoDB（自管）vs Terraform Cloud（託管），取捨在維運負擔 vs 控制權</li>
<li>加密：state 含敏感值，落地加密（S3 SSE）是底線</li>
<li>版本保留：bucket versioning 讓 state 損壞時能回捲到上一個正確版本</li>
<li>鎖機制：防止兩個人同時 apply 互相覆蓋</li>
<li>分割策略：一個大 state vs 多個小 state，取捨在引用便利性 vs 影響範圍控制</li>
</ul>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">IaC</a> — state 是 IaC 工具的核心依賴</li>
<li><a href="/blog/infra/knowledge-cards/drift/" data-link-title="Drift（設定漂移）" data-link-desc="IaC 的 state 與雲端實際狀態之間的不一致，通常因為有人繞過 IaC 直接在 Console 改設定">Drift</a> — state 與現實的落差</li>
</ul>
]]></content:encoded></item><item><title>有半套 IaC 但文件缺失的環境接管</title><link>https://tarrragon.github.io/blog/infra/takeover/partial-iac-no-docs/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/takeover/partial-iac-no-docs/</guid><description>&lt;p>接手一個有半套 IaC 的環境，比接手全手動的環境更難處理。全手動環境的規則簡單：所有東西都在 Console，逐一盤點就好。半套 IaC 的環境則有兩套真相並存 — 有些資源由程式碼管理、有些是手動加的、有些曾經由程式碼管理但後來被手動改過。&lt;code>terraform plan&lt;/code> 跑出來一長串 diff，哪些是該收進來的手動變更、哪些是該回退的設定漂移、哪些資源根本不在 state 裡，都要逐一判斷。在搞清楚這些之前，任何 &lt;code>apply&lt;/code> 都可能覆蓋正在服務客戶的設定。&lt;/p>
&lt;p>本篇的操作流程從盤點差距開始，經過 state 健康檢查、drift 收斂、文件重建，到最後排出收斂的優先序。每一步都在不影響線上服務的前提下進行。&lt;/p>
&lt;h2 id="state-與現實的差距盤點">state 與現實的差距盤點&lt;/h2>
&lt;p>盤點的第一步是跑 &lt;code>terraform plan&lt;/code> 但不 apply — plan 的輸出就是程式碼描述的狀態與雲端現實之間的完整差距清單。&lt;/p>





&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">terraform plan -no-color &amp;gt; plan-baseline-&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>.txt&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把這份輸出存進 repo，它是接手時的基線快照。之後每一次收斂動作的效果都用「跟這份基線比少了幾項 diff」來衡量。&lt;/p>
&lt;h3 id="三類-diff-的判讀">三類 diff 的判讀&lt;/h3>
&lt;p>plan 輸出的每一項 diff 歸屬三類，各自的風險等級與處理方式不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>diff 類型&lt;/th>
 &lt;th>plan 標記&lt;/th>
 &lt;th>含義&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;th>處理方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>要改&lt;/td>
 &lt;td>&lt;code>~&lt;/code> (update in-place)&lt;/td>
 &lt;td>資源存在於 state 與雲端，但屬性不一致&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>逐項判斷是採納手動變更還是回退&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>要建&lt;/td>
 &lt;td>&lt;code>+&lt;/code> (create)&lt;/td>
 &lt;td>資源在程式碼裡但雲端不存在&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>通常是前人寫了但沒 apply、或曾 destroy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>要刪&lt;/td>
 &lt;td>&lt;code>-&lt;/code> (destroy)&lt;/td>
 &lt;td>資源在 state 裡但雲端不存在、或雲端有但程式碼想移除&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>絕對不要盲目 apply — 先確認資源是否仍在使用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「要刪」是最危險的一類。常見成因是：前人在 Console 手動刪了某個資源但沒同步從程式碼移除（state 裡還有紀錄），或者前人在程式碼裡移除了某段 HCL 但沒跑 apply（雲端資源還在、state 記得它）。兩種情況都需要先確認該資源在雲端是否存在、是否仍被服務依賴，再決定是從 state 移除（&lt;code>terraform state rm&lt;/code>）還是補回 HCL。&lt;/p>
&lt;p>另一個需要留意的標記是 &lt;code>-/+&lt;/code>（forces replacement）— 它代表 Terraform 判定這個屬性的變更無法原地更新，必須先刪除再重建。對 stateful 資源（RDS、EBS volume）來說這等於資料遺失，在接手階段看到這個標記要先暫停、查清楚是哪個屬性觸發了 replacement。&lt;/p>
&lt;h2 id="哪些資源在-state-裡哪些不在">哪些資源在 state 裡、哪些不在&lt;/h2>
&lt;p>&lt;code>terraform state list&lt;/code> 列出所有被 IaC 管理的資源。配合 &lt;code>terraform show -json&lt;/code> 可以取得更結構化的 managed resource 摘要：&lt;/p>





&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"># state 裡有什麼（清單）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">terraform state list &amp;gt; managed-resources.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># state 裡有什麼（結構化摘要：type + name + provider）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">terraform show -json &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.values.root_module.resources[] | {type, name, provider}&amp;#39;&lt;/span> &amp;gt; managed-summary.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但 state 只是一份已知的清單 — 雲端上可能還有大量不在這份清單裡的資源。用 CLI 列舉雲端資源跟 state 做比對：&lt;/p>





&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>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 雲端上有什麼（以 EC2 + RDS + SG 為例）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">aws ec2 describe-instances --query &lt;span class="s1">&amp;#39;Reservations[].Instances[].InstanceId&amp;#39;&lt;/span> --output text &amp;gt; cloud-ec2.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">aws rds describe-db-instances --query &lt;span class="s1">&amp;#39;DBInstances[].DBInstanceIdentifier&amp;#39;&lt;/span> --output text &amp;gt; cloud-rds.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">aws ec2 describe-security-groups --query &lt;span class="s1">&amp;#39;SecurityGroups[].GroupId&amp;#39;&lt;/span> --output text &amp;gt; cloud-sg.txt&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用這兩份清單做比對，分成三類：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類別&lt;/th>
 &lt;th>定義&lt;/th>
 &lt;th>下一步&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>已管理&lt;/td>
 &lt;td>state 裡有、雲端也有&lt;/td>
 &lt;td>處理 drift（上一節的 diff）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>未管理&lt;/td>
 &lt;td>雲端有、state 裡沒有&lt;/td>
 &lt;td>評估是否需要 import&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>孤兒&lt;/td>
 &lt;td>state 裡有、雲端沒有&lt;/td>
 &lt;td>&lt;code>terraform state rm&lt;/code> 清除過時紀錄&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>未管理的資源需要逐一判斷：這個資源是前人刻意排除在 IaC 外的（例如一個還在實驗的測試機），還是應該納管但漏了？判斷依據是它的角色 — security group、IAM role、VPC 這類地基資源應該優先 import；一台跑完就該關的測試 EC2 可以暫時留在手動。&lt;/p></description><content:encoded><![CDATA[<p>接手一個有半套 IaC 的環境，比接手全手動的環境更難處理。全手動環境的規則簡單：所有東西都在 Console，逐一盤點就好。半套 IaC 的環境則有兩套真相並存 — 有些資源由程式碼管理、有些是手動加的、有些曾經由程式碼管理但後來被手動改過。<code>terraform plan</code> 跑出來一長串 diff，哪些是該收進來的手動變更、哪些是該回退的設定漂移、哪些資源根本不在 state 裡，都要逐一判斷。在搞清楚這些之前，任何 <code>apply</code> 都可能覆蓋正在服務客戶的設定。</p>
<p>本篇的操作流程從盤點差距開始，經過 state 健康檢查、drift 收斂、文件重建，到最後排出收斂的優先序。每一步都在不影響線上服務的前提下進行。</p>
<h2 id="state-與現實的差距盤點">state 與現實的差距盤點</h2>
<p>盤點的第一步是跑 <code>terraform plan</code> 但不 apply — plan 的輸出就是程式碼描述的狀態與雲端現實之間的完整差距清單。</p>





<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">terraform plan -no-color &gt; plan-baseline-<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.txt</span></span></code></pre></div><p>把這份輸出存進 repo，它是接手時的基線快照。之後每一次收斂動作的效果都用「跟這份基線比少了幾項 diff」來衡量。</p>
<h3 id="三類-diff-的判讀">三類 diff 的判讀</h3>
<p>plan 輸出的每一項 diff 歸屬三類，各自的風險等級與處理方式不同：</p>
<table>
  <thead>
      <tr>
          <th>diff 類型</th>
          <th>plan 標記</th>
          <th>含義</th>
          <th>風險</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>要改</td>
          <td><code>~</code> (update in-place)</td>
          <td>資源存在於 state 與雲端，但屬性不一致</td>
          <td>中</td>
          <td>逐項判斷是採納手動變更還是回退</td>
      </tr>
      <tr>
          <td>要建</td>
          <td><code>+</code> (create)</td>
          <td>資源在程式碼裡但雲端不存在</td>
          <td>低</td>
          <td>通常是前人寫了但沒 apply、或曾 destroy</td>
      </tr>
      <tr>
          <td>要刪</td>
          <td><code>-</code> (destroy)</td>
          <td>資源在 state 裡但雲端不存在、或雲端有但程式碼想移除</td>
          <td>高</td>
          <td>絕對不要盲目 apply — 先確認資源是否仍在使用</td>
      </tr>
  </tbody>
</table>
<p>「要刪」是最危險的一類。常見成因是：前人在 Console 手動刪了某個資源但沒同步從程式碼移除（state 裡還有紀錄），或者前人在程式碼裡移除了某段 HCL 但沒跑 apply（雲端資源還在、state 記得它）。兩種情況都需要先確認該資源在雲端是否存在、是否仍被服務依賴，再決定是從 state 移除（<code>terraform state rm</code>）還是補回 HCL。</p>
<p>另一個需要留意的標記是 <code>-/+</code>（forces replacement）— 它代表 Terraform 判定這個屬性的變更無法原地更新，必須先刪除再重建。對 stateful 資源（RDS、EBS volume）來說這等於資料遺失，在接手階段看到這個標記要先暫停、查清楚是哪個屬性觸發了 replacement。</p>
<h2 id="哪些資源在-state-裡哪些不在">哪些資源在 state 裡、哪些不在</h2>
<p><code>terraform state list</code> 列出所有被 IaC 管理的資源。配合 <code>terraform show -json</code> 可以取得更結構化的 managed resource 摘要：</p>





<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"># state 裡有什麼（清單）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform state list &gt; managed-resources.txt
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># state 裡有什麼（結構化摘要：type + name + provider）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">terraform show -json <span class="p">|</span> jq <span class="s1">&#39;.values.root_module.resources[] | {type, name, provider}&#39;</span> &gt; managed-summary.json</span></span></code></pre></div><p>但 state 只是一份已知的清單 — 雲端上可能還有大量不在這份清單裡的資源。用 CLI 列舉雲端資源跟 state 做比對：</p>





<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></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 雲端上有什麼（以 EC2 + RDS + SG 為例）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">aws ec2 describe-instances --query <span class="s1">&#39;Reservations[].Instances[].InstanceId&#39;</span> --output text &gt; cloud-ec2.txt
</span></span><span class="line"><span class="ln">4</span><span class="cl">aws rds describe-db-instances --query <span class="s1">&#39;DBInstances[].DBInstanceIdentifier&#39;</span> --output text &gt; cloud-rds.txt
</span></span><span class="line"><span class="ln">5</span><span class="cl">aws ec2 describe-security-groups --query <span class="s1">&#39;SecurityGroups[].GroupId&#39;</span> --output text &gt; cloud-sg.txt</span></span></code></pre></div><p>用這兩份清單做比對，分成三類：</p>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>定義</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已管理</td>
          <td>state 裡有、雲端也有</td>
          <td>處理 drift（上一節的 diff）</td>
      </tr>
      <tr>
          <td>未管理</td>
          <td>雲端有、state 裡沒有</td>
          <td>評估是否需要 import</td>
      </tr>
      <tr>
          <td>孤兒</td>
          <td>state 裡有、雲端沒有</td>
          <td><code>terraform state rm</code> 清除過時紀錄</td>
      </tr>
  </tbody>
</table>
<p>未管理的資源需要逐一判斷：這個資源是前人刻意排除在 IaC 外的（例如一個還在實驗的測試機），還是應該納管但漏了？判斷依據是它的角色 — security group、IAM role、VPC 這類地基資源應該優先 import；一台跑完就該關的測試 EC2 可以暫時留在手動。</p>
<p>手動比對 state list 與 CLI 輸出的效率有限，driftctl（現由 Snyk 維護、開源）可以自動掃描雲端資源與 Terraform state 的差異，一次列出所有 unmanaged resource。它跟 <code>terraform plan</code> 的差別在於 plan 只看已管理資源的 drift，driftctl 同時涵蓋根本不在 state 裡的資源。兩者互補：先用 driftctl 產出完整的 unmanaged 清單，再用 plan 處理已管理資源的 drift。</p>
<h2 id="state-的健康檢查">state 的健康檢查</h2>
<p>state 本身的存放方式決定了後續所有操作的安全性。接手後第一件事是確認 state 的健康狀態。</p>
<h3 id="存放位置">存放位置</h3>





<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"># 查看 backend 設定</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -A <span class="m">10</span> <span class="s1">&#39;backend&#39;</span> *.tf</span></span></code></pre></div><p>如果 backend 是 <code>local</code>（或沒有 backend 設定），state 檔只存在某台機器的磁碟上。這代表如果有第二個人從自己的機器跑 <code>apply</code>，兩人會用不同版本的 state 互相覆蓋。把 state 搬到 remote backend（S3 + DynamoDB lock）是接手後的第一優先事項，做法見<a href="/blog/infra/01-minimal-iac/iac-tool-state-backend/" data-link-title="IaC 工具選型與 state 地基" data-link-desc="Terraform / OpenTofu / CDK / Pulumi 的選型判準，state 作為 IaC 工具對現實的唯一記憶，以及 remote state backend 的自管與託管路線">IaC 工具選型與 state 地基</a>。</p>
<h3 id="加密與版本控制">加密與版本控制</h3>
<p>如果 state 已經在 S3，確認三件事：</p>





<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"># bucket 有沒有 versioning</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws s3api get-bucket-versioning --bucket &lt;state-bucket&gt;
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># bucket 有沒有加密</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">aws s3api get-bucket-encryption --bucket &lt;state-bucket&gt;
</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 class="c1"># 有沒有 lock table</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">aws dynamodb describe-table --table-name &lt;lock-table&gt; 2&gt;/dev/null</span></span></code></pre></div><p>versioning 沒開的話，一次壞掉的 apply 寫壞 state 就回不去了。加密沒開的話，state 裡的敏感值（資料庫密碼、private key 輸出）以明文存在 S3。</p>
<h3 id="state-裡的敏感值">state 裡的敏感值</h3>
<p>state 檔經常包含不該暴露的值。確認 state 有沒有在 Git 歷史裡：</p>





<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">git log --all --diff-filter<span class="o">=</span>A -- <span class="s1">&#39;*.tfstate&#39;</span> <span class="s1">&#39;*.tfstate.backup&#39;</span></span></span></code></pre></div><p>如果命中，代表 state 曾經被推進 repo。此時 Git 歷史裡的敏感值已經無法徹底清除（<code>git filter-branch</code> 或 <code>git filter-repo</code> 可以嘗試，但無法保證所有 clone 都更新）。務實的處理是：列出 state 裡的敏感值，全部輪替。</p>





<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"># 用 jq 從 state JSON 撈敏感值候選</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform show -json <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s1">  [.. | objects | to_entries[] |
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">   select(.key | test(&#34;password|secret|key|token&#34;; &#34;i&#34;))] |
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">  unique_by(.key) | .[] | &#34;\(.key): \(.value)&#34;
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s1">&#39;</span> 2&gt;/dev/null</span></span></code></pre></div><p>這個 jq 查詢會遞迴掃描 state JSON 裡所有欄位名稱含 password / secret / key / token 的值。命中的每一筆都要確認是否為真實密鑰、是否需要輪替。</p>
<h2 id="drift-收斂策略">drift 收斂策略</h2>
<p>盤點完差距、確認 state 健康之後，逐項收斂 drift。對 plan 輸出的每一項 diff 做一個二選一的決定：採納手動變更（改 HCL 去符合現實），或回退到程式碼版本（讓下一次 apply 把現實改回來）。</p>
<h3 id="採納-vs-回退的判斷">採納 vs 回退的判斷</h3>
<p>多數 drift 應該採納。前人在 Console 手動改設定通常有一個操作理由（即使沒有記錄下來）— 加了一條 security group 規則可能是為了讓某個新服務連進來，改了 RDS 的 <code>max_connections</code> 可能是為了解決連線數不足。在沒有充分理解這些改動的背景之前，回退它們等於撤銷一個可能正在支撐服務運作的設定。</p>
<p>回退適用的情境是：drift 明顯是誤操作（例如 <code>0.0.0.0/0</code> 打開了不該打開的埠）、或 drift 的屬性是有標準答案的（例如 S3 的 <code>block_public_access</code> 被關掉了）。</p>
<h3 id="操作步驟">操作步驟</h3>





<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. 刷新 state 到最新雲端狀態（不改資源、只更新 state 的快照）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform apply -refresh-only
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 2. 再跑一次 plan — 刷新後 diff 會減少（純 state 過期的 diff 消失）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">terraform plan -no-color &gt; plan-after-refresh.txt
</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 class="c1"># 3. 對剩餘的 diff 逐項處理</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">#    採納：改 HCL 讓程式碼跟現實一致 → plan 確認該項 diff 消失</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">#    回退：不改 HCL、讓 apply 把現實改回程式碼版本 → 先確認影響</span></span></span></code></pre></div><p><code>-refresh-only</code> 是安全的操作 — 它只更新 state 裡的屬性快照，不會改動任何雲端資源。但它會把手動變更「記進」state，讓後續 plan 的 diff 只剩程式碼與 state 的差異（而非程式碼與雲端的差異）。刷新後 plan 的 diff 更精確、更少、更容易逐項處理。</p>
<h3 id="import-未管理的資源">import 未管理的資源</h3>
<p>對未管理的資源，用 <code>import</code> 區塊一次處理一個，每次 import 後都跑 plan 確認零新增 diff：</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="k">import</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  to</span> <span class="o">=</span> <span class="k">aws_security_group</span><span class="p">.</span><span class="k">legacy_app</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  id</span> <span class="o">=</span> <span class="s2">&#34;sg-0abc123def456&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">}</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"># 生成對應的 HCL</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform plan -generate-config-out<span class="o">=</span>generated_legacy_app.tf
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 確認生成的 HCL 跟現實一致</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">terraform plan
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 預期：只有 import 動作、沒有 change/destroy</span></span></span></code></pre></div><p>生成的 HCL 需要人工確認 — 有些屬性是雲端自動設的預設值，Terraform 會把它們全部列出來，造成 HCL 冗長。移除純預設值的屬性、只保留有意義的設定，讓 HCL 反映設計意圖而非雲端預設。</p>
<p>對於大量未管理資源需要一次性反推 HCL 的情境，Former2 可以從現有 AWS 資源批量生成 Terraform code。它掃描帳號裡的資源、產出對應的 HCL，品質不完美（命名會用資源 ID 而非有意義的名稱、屬性可能包含大量預設值），但作為起點比從零手寫每個資源快得多。產出後仍需逐檔清理命名與移除預設值。</p>
<h2 id="文件重建">文件重建</h2>
<p>接手的環境通常沒有文件、或者文件已經過時到比沒有更糟（記載的是兩個版本前的架構）。文件重建的目標是讓下一個接手者不需要重複同樣的盤點過程，而非追求一份完美的架構文件。</p>
<h3 id="來源">來源</h3>
<p>能重建的資訊來源有限，但每個都有價值：</p>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>能找到什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Git log</td>
          <td>commit 訊息裡可能有「為什麼這樣改」的線索</td>
      </tr>
      <tr>
          <td>PR 歷史</td>
          <td>review 討論裡可能有決策脈絡</td>
      </tr>
      <tr>
          <td>HCL 程式碼</td>
          <td>變數命名、module 結構反映架構意圖</td>
      </tr>
      <tr>
          <td>CloudTrail</td>
          <td>過去 90 天的 API 呼叫紀錄</td>
      </tr>
      <tr>
          <td>帳單</td>
          <td>哪些服務在花錢、量級多大</td>
      </tr>
      <tr>
          <td>terraform-docs</td>
          <td>從 HCL 自動產出 module 文件（inputs/outputs）</td>
      </tr>
      <tr>
          <td>Inframap</td>
          <td>從 state 產出依賴關係視覺化圖</td>
      </tr>
  </tbody>
</table>
<p>terraform-docs 用一條指令就能從現有 HCL 產出每個 module 的 inputs、outputs 和 resources 清單，省去手動整理 module 介面的時間。Inframap 從 state 或 HCL 產出依賴關係圖，比 <code>terraform graph | dot</code> 好用的地方在於它自動過濾掉 provider 和 data source 的噪音，大型 state 也能產出可讀的圖。</p>
<h3 id="最小可行文件">最小可行文件</h3>
<p>寫一份 <code>INFRA-STATE.md</code> 放在 repo 根目錄，包含：</p>
<ul>
<li><strong>管理範圍</strong>：哪些資源由 IaC 管理、哪些是手動的、為什麼手動的沒有 import（例：還在實驗、不穩定、計畫廢棄）</li>
<li><strong>已知 drift</strong>：目前 plan 輸出裡還有哪些未處理的 diff、每個 diff 的處理方向（採納/回退/待調查）</li>
<li><strong>state 存放位置</strong>：backend 設定、bucket 名稱、lock table 名稱</li>
<li><strong>credential 狀態</strong>：有幾把 access key、哪些還在用、上次輪替時間</li>
<li><strong>接手日期與盤點結果</strong>：盤點時的資源數量、覆蓋率（managed / total）</li>
</ul>
<p>這份文件不需要精美，需要的是準確且持續更新。每次收斂一項 drift 或 import 一個資源，就更新對應的段落。前任團隊的知識已經不在了，這份文件取代它成為環境的記憶。</p>
<h2 id="收斂到完整-iac-的優先序">收斂到完整 IaC 的優先序</h2>
<p>把整個收斂過程排成四個階段，每個階段都能獨立交付價值：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>目標</th>
          <th>交付物</th>
          <th>預估時間</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>state 健康</td>
          <td>remote backend + 加密 + versioning + lock</td>
          <td>1-2 天</td>
      </tr>
      <tr>
          <td>2</td>
          <td>地基 import</td>
          <td>security group、IAM role、VPC 納管</td>
          <td>1-2 週</td>
      </tr>
      <tr>
          <td>3</td>
          <td>drift 收斂</td>
          <td>已管理資源的 plan 歸零</td>
          <td>1-2 週</td>
      </tr>
      <tr>
          <td>4</td>
          <td>覆蓋率提升</td>
          <td>應用層資源逐批 import</td>
          <td>持續</td>
      </tr>
  </tbody>
</table>
<p>每個階段的驗證方式相同：<code>terraform plan</code> 的輸出是否比上一階段乾淨。階段一完成後，plan 的可信度才成立；階段二和三是把 plan 的 diff 清到零；階段四是擴大 plan 的管轄範圍。</p>
<p>每一步操作之前都先備份 state：</p>





<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"># 手動備份 state（不論 bucket 有沒有 versioning 都先拉一份）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform state pull &gt; state-backup-<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.json</span></span></code></pre></div><p>state 操作失敗時的回退路徑是 <code>terraform state push state-backup.json</code> 從備份還原 — 資源本身不受影響，只是工具對現實的記憶回到上一個正確的版本。<code>state push</code> 是覆寫操作，只在確認備份版本正確時使用。</p>
<p>需要搬移資源在 state 裡的位址時（例如重構 module 結構），優先用 <code>moved {}</code> 區塊而非 <code>terraform state mv</code>。<code>moved</code> 是宣告式的、寫在 HCL 裡、可以被 PR review、plan 時會顯示搬移動作。<code>state mv</code> 是指令式的、直接改 state、沒有 review 機制、操作紀錄只在 CLI 歷史裡。</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="k">moved</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  from</span> <span class="o">=</span> <span class="k">aws_security_group</span><span class="p">.</span><span class="k">old_name</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  to</span>   <span class="o">=</span> <span class="k">module</span><span class="p">.</span><span class="k">network</span><span class="p">.</span><span class="k">aws_security_group</span><span class="p">.</span><span class="k">app</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">}</span></span></code></pre></div><h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/01-minimal-iac/iac-tool-state-backend/" data-link-title="IaC 工具選型與 state 地基" data-link-desc="Terraform / OpenTofu / CDK / Pulumi 的選型判準，state 作為 IaC 工具對現實的唯一記憶，以及 remote state backend 的自管與託管路線">IaC 工具選型與 state 地基</a>：state 怎麼從 local 搬到 remote backend</li>
<li>→ <a href="/blog/infra/01-minimal-iac/console-readonly-minimal-viable/" data-link-title="Console 唯讀鐵律與最小可行資源集合" data-link-desc="Console 只用來看不用來改的操作紀律、drift 的延遲浮現與偵測，以及能跑出第一個完整 apply 迴路的最小資源集合">Console 唯讀鐵律</a>：drift 的來源與偵測</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">環境分離與模組化</a>：收斂完成後怎麼把單環境拆成 per-env module</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">infra 走 PR 流程</a>：收斂完成後的變更怎麼走 review</li>
<li>→ <a href="/blog/infra/takeover/partial-iac-state-repair/" data-link-title="State 修復與清理" data-link-desc="接手的 Terraform state 損壞、有 orphaned entry、或需要搬遷時，怎麼診斷問題、安全操作、以及從錯誤中回復">State 修復與清理</a>：state 損壞的操作修復步驟</li>
<li>→ <a href="/blog/infra/takeover/partial-iac-drift-triage/" data-link-title="Drift 分類處理指南" data-link-desc="接手半套 IaC 環境時，怎麼讀 plan 輸出分類 drift、判斷保留還是回退、處理 stateful 資源的高風險漂移，以及批次收斂的工作流">Drift 分類處理</a>：逐項判斷 adopt vs revert</li>
<li>→ <a href="/blog/infra/takeover/partial-iac-bulk-import/" data-link-title="Unmanaged Resource 批次 Import 工作流" data-link-desc="把 Terraform state 外的雲端資源有系統地納入 IaC 管理：優先序判斷、import block 語法、generated HCL 的 review 要點、批次策略與常見失敗處理">批次 Import 工作流</a>：unmanaged resource 的 import 操作</li>
<li>→ <a href="/blog/infra/takeover/partial-iac-dual-truth-operation/" data-link-title="兩套真相並存的過渡期操作" data-link-desc="部分資源在 IaC、部分在手動時，怎麼安全操作避免比全手動更危險，以及怎麼縮短這個過渡期">過渡期操作</a>：兩套真相並存時的安全操作規則</li>
</ul>
]]></content:encoded></item><item><title>1.8 State Ownership 與 Query Boundary</title><link>https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/</guid><description>&lt;p>State ownership 與 query boundary 的核心責任是先定義資料由誰承擔正式判斷、再定義不同查詢路徑能回答什麼問題。進入 MySQL、PostgreSQL、MSSQL 或其他資料庫前、讀者需要先知道資料庫同時是儲存工具與服務狀態的責任邊界。&lt;/p>
&lt;p>本章從 source of truth 的責任分層開始、引入 CQRS / event sourcing / materialized view 等模式、最後處理四種 query 邊界的設計。讀完後讀者能回答：哪些資料是正式狀態、什麼時候該分讀寫 model、materialized view 怎麼用、replica lag 怎麼影響 query。&lt;/p>
&lt;h2 id="state-ownership">State Ownership&lt;/h2>
&lt;p>State ownership 的責任是判斷哪些資料是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、哪些資料屬於 cache、search index、event log 或報表副本。正式狀態會影響交易結果、權限判斷、對帳與客服修復、因此需要清楚的 owner、schema、驗證方式與變更流程。&lt;/p>
&lt;p>訂單狀態、付款狀態、會員方案、權限授權與發票紀錄通常屬於正式狀態。商品搜尋索引、快取值、統計摘要與推薦結果通常是派生狀態；派生狀態可以錯過短暫更新、但正式狀態需要能被追溯、修復與稽核。&lt;/p>
&lt;h2 id="canonical-state-vs-derived-state">Canonical State vs Derived State&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Canonical state&lt;/th>
 &lt;th>Derived state&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>角色&lt;/td>
 &lt;td>source of truth&lt;/td>
 &lt;td>從 canonical 計算 / 同步&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入&lt;/td>
 &lt;td>用戶 / 業務操作&lt;/td>
 &lt;td>從 canonical 推&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一致性&lt;/td>
 &lt;td>strong / serializable&lt;/td>
 &lt;td>eventual 通常夠用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復&lt;/td>
 &lt;td>必須能精確修復&lt;/td>
 &lt;td>可以「砍掉重建」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>範例&lt;/td>
 &lt;td>訂單、付款、餘額&lt;/td>
 &lt;td>搜尋 index、recommendation、daily summary&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Canonical state 的特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>業務決策依據（付款、權限）&lt;/li>
&lt;li>不能從其他地方重建（一旦丟、無法找回）&lt;/li>
&lt;li>需要 audit log、point-in-time recovery、backup&lt;/li>
&lt;li>通常在 OLTP DB（PostgreSQL / Aurora / Spanner）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Derived state 的特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>從 canonical 推算出來&lt;/li>
&lt;li>可以「rebuild」（lazy 或 eager）&lt;/li>
&lt;li>失效可接受（用戶可能看到舊的）&lt;/li>
&lt;li>通常在 cache / search / analytics store&lt;/li>
&lt;li>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache&lt;/a> 配對快取、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store&lt;/a> feature&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>設計原則&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>同一資料 &lt;em>不能&lt;/em> 同時是兩個地方的 canonical → 衝突時不知道信誰&lt;/li>
&lt;li>寫入永遠先寫 canonical、再 propagate 到 derived&lt;/li>
&lt;li>derived 出錯只能 rebuild、不能拿來「修正 canonical」&lt;/li>
&lt;/ul>
&lt;h2 id="cqrs-在資料庫情境的應用">CQRS 在資料庫情境的應用&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS&lt;/a> 的概念定義、設計判準與代價見知識卡。本段聚焦在資料庫層面：state ownership 的決策如何影響你要不要分離讀寫模型。&lt;/p>
&lt;p>State ownership 跟 CQRS 的交叉點是：當 canonical state 的 schema 為寫入正確性最佳化（normalize、強一致、transaction boundary 清楚），但讀取面的多種消費者各自需要不同的反正規化形狀（列表頁要扁平 summary、報表要聚合、搜尋要全文索引），canonical schema 無法同時服務這些讀取需求。這時候分離 write model 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 是解決形狀不對稱的方式。&lt;/p></description><content:encoded><![CDATA[<p>State ownership 與 query boundary 的核心責任是先定義資料由誰承擔正式判斷、再定義不同查詢路徑能回答什麼問題。進入 MySQL、PostgreSQL、MSSQL 或其他資料庫前、讀者需要先知道資料庫同時是儲存工具與服務狀態的責任邊界。</p>
<p>本章從 source of truth 的責任分層開始、引入 CQRS / event sourcing / materialized view 等模式、最後處理四種 query 邊界的設計。讀完後讀者能回答：哪些資料是正式狀態、什麼時候該分讀寫 model、materialized view 怎麼用、replica lag 怎麼影響 query。</p>
<h2 id="state-ownership">State Ownership</h2>
<p>State ownership 的責任是判斷哪些資料是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、哪些資料屬於 cache、search index、event log 或報表副本。正式狀態會影響交易結果、權限判斷、對帳與客服修復、因此需要清楚的 owner、schema、驗證方式與變更流程。</p>
<p>訂單狀態、付款狀態、會員方案、權限授權與發票紀錄通常屬於正式狀態。商品搜尋索引、快取值、統計摘要與推薦結果通常是派生狀態；派生狀態可以錯過短暫更新、但正式狀態需要能被追溯、修復與稽核。</p>
<h2 id="canonical-state-vs-derived-state">Canonical State vs Derived State</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Canonical state</th>
          <th>Derived state</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>角色</td>
          <td>source of truth</td>
          <td>從 canonical 計算 / 同步</td>
      </tr>
      <tr>
          <td>寫入</td>
          <td>用戶 / 業務操作</td>
          <td>從 canonical 推</td>
      </tr>
      <tr>
          <td>一致性</td>
          <td>strong / serializable</td>
          <td>eventual 通常夠用</td>
      </tr>
      <tr>
          <td>修復</td>
          <td>必須能精確修復</td>
          <td>可以「砍掉重建」</td>
      </tr>
      <tr>
          <td>範例</td>
          <td>訂單、付款、餘額</td>
          <td>搜尋 index、recommendation、daily summary</td>
      </tr>
  </tbody>
</table>
<p><strong>Canonical state 的特徵</strong>：</p>
<ul>
<li>業務決策依據（付款、權限）</li>
<li>不能從其他地方重建（一旦丟、無法找回）</li>
<li>需要 audit log、point-in-time recovery、backup</li>
<li>通常在 OLTP DB（PostgreSQL / Aurora / Spanner）</li>
</ul>
<p><strong>Derived state 的特徵</strong>：</p>
<ul>
<li>從 canonical 推算出來</li>
<li>可以「rebuild」（lazy 或 eager）</li>
<li>失效可接受（用戶可能看到舊的）</li>
<li>通常在 cache / search / analytics store</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> 配對快取、<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store</a> feature</li>
</ul>
<p><strong>設計原則</strong>：</p>
<ul>
<li>同一資料 <em>不能</em> 同時是兩個地方的 canonical → 衝突時不知道信誰</li>
<li>寫入永遠先寫 canonical、再 propagate 到 derived</li>
<li>derived 出錯只能 rebuild、不能拿來「修正 canonical」</li>
</ul>
<h2 id="cqrs-在資料庫情境的應用">CQRS 在資料庫情境的應用</h2>
<p><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> 的概念定義、設計判準與代價見知識卡。本段聚焦在資料庫層面：state ownership 的決策如何影響你要不要分離讀寫模型。</p>
<p>State ownership 跟 CQRS 的交叉點是：當 canonical state 的 schema 為寫入正確性最佳化（normalize、強一致、transaction boundary 清楚），但讀取面的多種消費者各自需要不同的反正規化形狀（列表頁要扁平 summary、報表要聚合、搜尋要全文索引），canonical schema 無法同時服務這些讀取需求。這時候分離 write model 跟 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 是解決形狀不對稱的方式。</p>
<p>資料庫情境的 CQRS 有不同的實作強度：</p>
<p><strong>最輕量 — 同 DB 不同 query path</strong>：寫入走 canonical table，讀取走 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a> 或反正規化 view。同一個 PostgreSQL 裡用 materialized view 就能實現最基本的讀寫分離，不需要兩個 DB、不需要事件同步。適合讀寫形狀不同但流量規模還不需要獨立擴展的階段。</p>
<p><strong>中度 — 同 DB 加 read replica</strong>：寫入走 primary，列表跟報表走 read replica。Replica lag 決定哪些 query 能走 replica（見下方 Replica Lag 段）。適合讀取流量開始壓迫寫入的階段。</p>
<p><strong>完整 — 獨立 read store</strong>：寫入走 OLTP DB，讀取走獨立的 analytics store（BigQuery、Athena）或搜尋引擎（Elasticsearch）。透過 CDC 或事件同步維護 read store。適合讀取形狀、流量、SLA 都跟寫入完全不同的階段。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> — 交易層（OLTP）跟資料層（BigQuery / Athena）分開。<a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair</a> — on-prem OLTP + GCP BigQuery analytics。</p>
<h2 id="event-sourcing-與-state-ownership">Event Sourcing 與 State Ownership</h2>
<p><a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">Event sourcing</a> 的概念定義、設計判準與代價見知識卡。本段聚焦在資料庫層面：event sourcing 怎麼改變 state ownership 跟 query boundary。</p>
<p>Event sourcing 把 state ownership 的正式紀錄從 mutable row 改成 append-only <a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a>。這個改變影響本章的每一個面向：</p>
<p><strong>對 canonical / derived 分類的影響</strong>：採用 event sourcing 後，event log 是 canonical state，current state 變成 derived state。這跟傳統 CRUD 架構相反 — 傳統架構中 current state（mutable row）是 canonical，歷史紀錄（audit log）是 derived。</p>
<p><strong>對 query boundary 的影響</strong>：event log 不適合直接服務交易查詢跟列表查詢（每次 replay 整條事件流太慢）。Event sourcing 幾乎必然搭配 <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 維護 read model — projection 持續消費事件流、更新反正規化的查詢 view。交易查詢讀 projection 的輸出而非直接讀 event log。</p>
<p><strong>對修復流程的影響</strong>：傳統架構的資料修復是「直接改 row」；event sourcing 的修復是「發一筆補償事件（compensating event）」。修復本身也是事件、會被記錄在 event log 裡、提供完整的修復 audit trail。</p>
<p>Event sourcing 的設計門檻在於 projection 的維護跟 event schema evolution。Projection 數量增長後，每次 event schema 改版都需要同步更新所有 projection；projection 的 replay 跟 reconciliation 是長期運維的主要成本。這些代價決定了 event sourcing 適合「需要完整變更歷史」的業務場景（金融帳務、訂單流程、法規合規），而非所有資料存取場景。</p>
<h2 id="materialized-view-在資料庫的應用">Materialized View 在資料庫的應用</h2>
<p><a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">Materialized view</a> 的概念定義見知識卡。本段聚焦在 OLTP 資料庫裡 materialized view 作為最輕量 read model 的具體實作。</p>
<p>Materialized view 是「同 DB 內最簡單的讀寫分離」。不需要事件同步、不需要獨立 read store、不需要 projection consumer — 資料庫自己定期執行查詢、存放結果。</p>
<p><strong>跟 regular view 的差別</strong>：regular view 是 SQL 別名，每次 query 重跑底層查詢；materialized view 有實體儲存，query 時直接讀預計算結果。差別在 query-time cost — 複雜 JOIN / aggregation 重複跑時，materialized view 把計算推到 refresh 時、query 時接近零成本。</p>
<p><strong>Refresh 策略</strong>：</p>
<ul>
<li><strong>全量 refresh</strong>：PostgreSQL 的 <code>REFRESH MATERIALIZED VIEW</code>，refresh 期間 view 預設 unavailable。</li>
<li><strong>Concurrent refresh</strong>：PostgreSQL 的 <code>CONCURRENTLY</code> 模式，refresh 期間 view 仍可讀但資料可能 stale。</li>
<li><strong>增量 refresh</strong>：PostgreSQL 的 <code>pg_ivm</code>、Oracle 的 fast refresh — 只更新變更的部分，成本低但配置複雜。</li>
<li><strong>Trigger-based</strong>：特定 event 觸發 refresh，適合低頻變更的資料。</li>
</ul>
<p><strong>在 state ownership 的定位</strong>：materialized view 是 derived state，修復方式是 refresh（重建）而非直接修改。大量 materialized view 會拖累寫入吞吐 — 每次 base table 變更都可能觸發 refresh 計算。設計時要平衡 refresh 頻率跟 query freshness 需求。</p>
<p><strong>跟觀測領域的對照</strong>：觀測領域的 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 在概念上等同於 TSDB 層的 materialized view — 定期執行 query expression、把結果寫成新 series。兩者面對同樣的設計問題：refresh 頻率、freshness lag、維護成本與儲存增長。觀測領域的 CQRS 特化應用見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
<h2 id="query-boundary-四種">Query Boundary 四種</h2>
<p>Query boundary 的責任是讓不同查詢路徑承擔不同服務問題。交易查詢、列表查詢、報表查詢與對帳查詢都可能讀同一張表、但它們的正確性、延遲與資料新鮮度要求不同。</p>
<table>
  <thead>
      <tr>
          <th>查詢類型</th>
          <th>服務責任</th>
          <th>典型 latency</th>
          <th>容忍 stale</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>交易查詢</td>
          <td>支援使用者當下動作、例如付款、下單、授權</td>
          <td>&lt; 100ms</td>
          <td>不容忍</td>
          <td>延遲或錯誤會直接影響交易結果</td>
      </tr>
      <tr>
          <td>列表查詢</td>
          <td>支援使用者瀏覽與管理、例如訂單列表、會員清單</td>
          <td>&lt; 500ms</td>
          <td>可容忍秒級</td>
          <td>可能放大 index、pagination 與排序成本</td>
      </tr>
      <tr>
          <td>報表查詢</td>
          <td>支援營運分析、財務統計與趨勢判讀</td>
          <td>秒到分鐘級</td>
          <td>可容忍 hour 級</td>
          <td>容易壓迫線上資料庫與混淆資料時效</td>
      </tr>
      <tr>
          <td>對帳查詢</td>
          <td>驗證正式狀態與外部事實是否一致</td>
          <td>分鐘到小時級</td>
          <td>視業務</td>
          <td>查詢定義錯誤會造成錯修或漏修</td>
      </tr>
  </tbody>
</table>
<p>這四種查詢混在一起時、資料庫會同時承擔低延遲交易與高成本分析、最後讓任何一種資料庫選型都變得模糊。</p>
<h3 id="交易路徑的邊界">交易路徑的邊界</h3>
<p>交易路徑的責任是維持使用者動作的即時正確性。它需要短查詢、明確 index、可控 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a> 與清楚 timeout。</p>
<p>交易路徑的設計要把報表聚合或長時間掃描移到其他查詢路徑。若下單 API 同時查歷史報表、計算大範圍統計或同步重建派生狀態、交易延遲會被非交易責任拖慢。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 200 個獨立 Aurora cluster 把不同業務 transaction 分開、避免互相影響。</p>
<h3 id="列表與報表的邊界">列表與報表的邊界</h3>
<p>列表查詢的責任是支援產品體驗中的瀏覽與定位。列表查詢需要穩定排序、分頁策略、篩選條件與查詢成本界線；它應建立自己的讀取模型或索引策略、避免直接借用交易查詢的資料模型造成 slow query、排序漂移與 pagination 重複。</p>
<p>報表查詢的責任是支援分析與決策。報表通常可以接受資料延遲、因此更適合使用 read replica、materialized view、ETL 或 analytics store。把報表直接壓在線上 primary 上、會讓交易服務承擔不必要的容量風險。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair hybrid burst</a>、<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> — 交易層跟資料層分開部署。</p>
<h3 id="對帳查詢的邊界">對帳查詢的邊界</h3>
<p>對帳查詢的責任是驗證正式狀態是否與外部事實一致。付款、發票、庫存與訂閱方案都需要對帳查詢、但對帳查詢要保留時間窗、資料來源、差異定義與人工修復入口。</p>
<p>對帳查詢承擔比報表更直接的修復責任。報表回答「現在看起來如何」、對帳回答「哪一筆正式狀態需要修復」。因此對帳查詢結果要能進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
<p>詳見 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation 與 Data Repair</a>。</p>
<h2 id="replica-lag-對-query-boundary-的影響">Replica Lag 對 Query Boundary 的影響</h2>
<p>當應用使用 read replica 擴 read traffic 時、replica lag 會直接影響 query boundary 設計。</p>
<p><strong>典型 lag</strong>：</p>
<ul>
<li>PostgreSQL streaming：&lt; 100ms（同 AZ）</li>
<li>Aurora：10-30ms（同 region）</li>
<li>跨 region replica：秒級到分鐘級</li>
</ul>
<p><strong>不同 query 對 lag 的容忍</strong>：</p>
<ul>
<li>交易查詢：不可容忍 lag、必須走 primary</li>
<li>read-after-write（剛寫完查自己）：必須 primary、或 session sticky</li>
<li>列表查詢：通常容忍 lag &lt; 1 秒</li>
<li>報表查詢：lag 分鐘級可接受</li>
<li>對帳查詢：通常用 batch、lag 不關鍵</li>
</ul>
<p><strong>Stale read 容忍策略</strong>：</p>
<ul>
<li>「能容忍秒級 stale」的 read → replica（用戶 profile、報表）</li>
<li>「不能 stale」的 read → primary（剛寫入後的查詢、餘額確認）</li>
<li>read-after-write：用 session token 標記「剛寫過」、N 秒內讀走 primary</li>
</ul>
<p>對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 的「Read Replica Scaling」段。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>資料庫選型前要先回答四個問題：</p>
<ol>
<li>哪些資料是正式狀態、哪些是派生狀態</li>
<li>哪些查詢屬於交易路徑、哪些可以延遲或離線化</li>
<li>哪些查詢結果會觸發修復、退款、補償或人工決策</li>
<li>哪些資料需要 audit、masking、retention 或刪除責任</li>
</ol>
<p>這些問題決定後續該比較 relational database、document database、search index、analytics store 還是 cache。工具差異要放在責任邊界之後討論。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體資料庫文章要承接本篇的 state ownership 與 query boundary。PostgreSQL、MySQL、MSSQL 或其他 relational database 的比較、應先問它們如何支援正式狀態、交易查詢、列表查詢、報表查詢與對帳查詢、再進入索引、隔離層級、replica 或工具語法。</p>
<p>若主問題是正式狀態與交易一致性、後續文章要優先比較 transaction、isolation、index 與 migration 能力。若主問題是報表與搜尋、後續文章要評估 read replica、materialized view、search index 或 analytics store。若主問題是對帳與修復、後續文章要比較 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、audit log、backup/restore 與資料修復流程。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>state / query 設計重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a></td>
          <td>200 個獨立 cluster 隔離 transaction scope</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a></td>
          <td>OLTP 交易層 + BigQuery / Athena 分析層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair</a></td>
          <td>on-prem OLTP + GCP BigQuery 分析、典型 CQRS 配置</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></td>
          <td>feature store（derived state）、跟 source 分離</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a></td>
          <td>watch list（user state）跟 content metadata 分層</td>
      </tr>
  </tbody>
</table>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位與索引語意回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design</a></li>
<li>與 1.3 的交接：transaction boundary 設計影響哪些 query 走 primary、哪些可走 replica</li>
<li>與 1.7 的交接：正式狀態變更要進入 production rollout — <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout Evidence</a></li>
<li>與 1.9 的交接：對帳查詢的下游修復 — <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">Reconciliation and Data Repair</a></li>
<li>與 2 的交接：cache layer 是 derived state 最常見的形式 — <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a></li>
<li>與 4.20 的交接：query evidence 跟 reconciliation evidence — <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a></li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要進一步處理 schema 與資料模型、接著讀 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a>。要處理 schema 演進與正式狀態變更、接著讀 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。要處理對帳跟資料修復、接著讀 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation</a>。要設計 KV / Document 的 state ownership、接著讀 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃</a>。</p>
]]></content:encoded></item><item><title>State 修復與清理</title><link>https://tarrragon.github.io/blog/infra/takeover/partial-iac-state-repair/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/takeover/partial-iac-state-repair/</guid><description>&lt;p>接手一個有半套 IaC 的環境時，state 是工具對現實的唯一記憶，但這份記憶可能已經失真——有些記錄對應的雲端資源已經不存在、有些雲端資源從來沒被記錄、有些記錄的屬性跟現實對不上。在動任何資源之前，先把 state 修到一個可信的狀態，是所有後續操作的前提。&lt;/p>
&lt;h2 id="診斷-state-的健康狀態">診斷 state 的健康狀態&lt;/h2>
&lt;p>&lt;code>terraform plan&lt;/code> 的輸出是診斷 state 健康度的主要工具。在不做任何 code 變更的前提下跑 plan，輸出的每一行差異都代表 state 與現實的落差：&lt;/p>





&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">terraform plan -detailed-exitcode -no-color &amp;gt; plan-diagnosis.txt 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&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"># exit code: 0=無差異, 1=錯誤, 2=有差異&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Plan 的差異分三類，每一類的處理方式不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Plan 顯示&lt;/th>
 &lt;th>意義&lt;/th>
 &lt;th>處理方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>~ update in-place&lt;/code>&lt;/td>
 &lt;td>state 記錄的屬性跟雲端不同（drift）&lt;/td>
 &lt;td>判斷要保留手動改的值還是回退到 code&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>+ create&lt;/code>&lt;/td>
 &lt;td>code 裡有但 state 裡沒有（漏 import）&lt;/td>
 &lt;td>確認資源是否已存在於雲端，是則 import&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>- destroy&lt;/code>&lt;/td>
 &lt;td>state 裡有但 code 裡沒有（orphan）&lt;/td>
 &lt;td>確認資源是否還在雲端、是否還在用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Plan 跑到一半報錯（exit code 1）而非產出差異，通常代表更嚴重的問題：provider 版本不相容、state 格式損壞、或 state 引用的資源 ID 在雲端已經不存在。錯誤訊息裡的 resource address 指向問題所在。&lt;/p>
&lt;h3 id="orphaned-entry-的辨認">Orphaned entry 的辨認&lt;/h3>
&lt;p>State 裡有一筆資源記錄，但雲端已經沒有對應的資源（手動刪除、帳號切換、或 region 不對），plan 會顯示 &lt;code>- destroy&lt;/code> 或直接報 &lt;code>Error: reading ... NotFound&lt;/code>。這種 orphaned entry 需要從 state 移除，否則每次 plan 都會嘗試操作一個不存在的目標。&lt;/p>





&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"># 列出 state 裡所有資源，逐一確認是否還存在&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">terraform state list &lt;span class="p">|&lt;/span> &lt;span class="k">while&lt;/span> &lt;span class="nb">read&lt;/span> addr&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Checking: &lt;/span>&lt;span class="nv">$addr&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> terraform state show &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$addr&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt; /dev/null 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34; POSSIBLY ORPHANED: &lt;/span>&lt;span class="nv">$addr&lt;/span>&lt;span class="s2">&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 class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個腳本不連雲端驗證（只檢查 state 內部一致性），真正的驗證要靠 plan 輸出。如果 plan 對某個資源報 NotFound，那就是 orphaned。&lt;/p>
&lt;h2 id="state-操作前的備份">State 操作前的備份&lt;/h2>
&lt;p>所有 state 操作（rm、mv、push、import）都是直接改寫 state 檔的破壞性操作。操作前的備份是唯一的回退路徑。&lt;/p>





&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"># 從遠端 backend 拉一份完整的 state 到本地&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">terraform state pull &amp;gt; state-backup-&lt;span class="k">$(&lt;/span>date +%Y%m%d-%H%M&lt;span class="k">)&lt;/span>.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 確認備份可用：檢查 JSON 格式和 resource 數量&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">jq &lt;span class="s1">&amp;#39;.resources | length&amp;#39;&lt;/span> state-backup-*.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果 state 存在 S3 且 bucket 有開 versioning（應該有，見&lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一&lt;/a>），S3 的版本歷史是第二道保險。但 &lt;code>state pull&lt;/code> 的本地備份更可控——S3 versioning 的回復要操作 bucket、權限要對、而且版本 ID 需要另外查。&lt;/p>
&lt;h2 id="移除-orphaned-entrystate-rm">移除 orphaned entry：state rm&lt;/h2>
&lt;p>&lt;code>terraform state rm&lt;/code> 把一筆資源從 state 裡移除，但不觸碰雲端的實際資源。用途是清理 state 裡對應不到雲端的記錄，讓 plan 不再嘗試操作不存在的目標。&lt;/p>





&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"># 移除單一 orphaned resource&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">terraform state rm &lt;span class="s1">&amp;#39;aws_instance.old_bastion&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>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 移除整個 module 的記錄（module 被拆掉但資源還在雲端、要重新 import）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">terraform state rm &lt;span class="s1">&amp;#39;module.legacy_network&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>移除後立刻跑 plan 驗證：原本針對這個資源的 destroy / error 應該消失。如果移除後 plan 反而出現 &lt;code>+ create&lt;/code>（想重建這個資源），代表 code 裡還有對應的 resource block——要麼也刪 code，要麼這個資源需要 import 而不是 rm。&lt;/p></description><content:encoded><![CDATA[<p>接手一個有半套 IaC 的環境時，state 是工具對現實的唯一記憶，但這份記憶可能已經失真——有些記錄對應的雲端資源已經不存在、有些雲端資源從來沒被記錄、有些記錄的屬性跟現實對不上。在動任何資源之前，先把 state 修到一個可信的狀態，是所有後續操作的前提。</p>
<h2 id="診斷-state-的健康狀態">診斷 state 的健康狀態</h2>
<p><code>terraform plan</code> 的輸出是診斷 state 健康度的主要工具。在不做任何 code 變更的前提下跑 plan，輸出的每一行差異都代表 state 與現實的落差：</p>





<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">terraform plan -detailed-exitcode -no-color &gt; plan-diagnosis.txt 2&gt;<span class="p">&amp;</span><span class="m">1</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># exit code: 0=無差異, 1=錯誤, 2=有差異</span></span></span></code></pre></div><p>Plan 的差異分三類，每一類的處理方式不同：</p>
<table>
  <thead>
      <tr>
          <th>Plan 顯示</th>
          <th>意義</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>~ update in-place</code></td>
          <td>state 記錄的屬性跟雲端不同（drift）</td>
          <td>判斷要保留手動改的值還是回退到 code</td>
      </tr>
      <tr>
          <td><code>+ create</code></td>
          <td>code 裡有但 state 裡沒有（漏 import）</td>
          <td>確認資源是否已存在於雲端，是則 import</td>
      </tr>
      <tr>
          <td><code>- destroy</code></td>
          <td>state 裡有但 code 裡沒有（orphan）</td>
          <td>確認資源是否還在雲端、是否還在用</td>
      </tr>
  </tbody>
</table>
<p>Plan 跑到一半報錯（exit code 1）而非產出差異，通常代表更嚴重的問題：provider 版本不相容、state 格式損壞、或 state 引用的資源 ID 在雲端已經不存在。錯誤訊息裡的 resource address 指向問題所在。</p>
<h3 id="orphaned-entry-的辨認">Orphaned entry 的辨認</h3>
<p>State 裡有一筆資源記錄，但雲端已經沒有對應的資源（手動刪除、帳號切換、或 region 不對），plan 會顯示 <code>- destroy</code> 或直接報 <code>Error: reading ... NotFound</code>。這種 orphaned entry 需要從 state 移除，否則每次 plan 都會嘗試操作一個不存在的目標。</p>





<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"># 列出 state 裡所有資源，逐一確認是否還存在</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform state list <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> addr<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Checking: </span><span class="nv">$addr</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  terraform state show <span class="s2">&#34;</span><span class="nv">$addr</span><span class="s2">&#34;</span> &gt; /dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;  POSSIBLY ORPHANED: </span><span class="nv">$addr</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>這個腳本不連雲端驗證（只檢查 state 內部一致性），真正的驗證要靠 plan 輸出。如果 plan 對某個資源報 NotFound，那就是 orphaned。</p>
<h2 id="state-操作前的備份">State 操作前的備份</h2>
<p>所有 state 操作（rm、mv、push、import）都是直接改寫 state 檔的破壞性操作。操作前的備份是唯一的回退路徑。</p>





<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"># 從遠端 backend 拉一份完整的 state 到本地</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform state pull &gt; state-backup-<span class="k">$(</span>date +%Y%m%d-%H%M<span class="k">)</span>.json
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 確認備份可用：檢查 JSON 格式和 resource 數量</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">jq <span class="s1">&#39;.resources | length&#39;</span> state-backup-*.json</span></span></code></pre></div><p>如果 state 存在 S3 且 bucket 有開 versioning（應該有，見<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一</a>），S3 的版本歷史是第二道保險。但 <code>state pull</code> 的本地備份更可控——S3 versioning 的回復要操作 bucket、權限要對、而且版本 ID 需要另外查。</p>
<h2 id="移除-orphaned-entrystate-rm">移除 orphaned entry：state rm</h2>
<p><code>terraform state rm</code> 把一筆資源從 state 裡移除，但不觸碰雲端的實際資源。用途是清理 state 裡對應不到雲端的記錄，讓 plan 不再嘗試操作不存在的目標。</p>





<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"># 移除單一 orphaned resource</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform state rm <span class="s1">&#39;aws_instance.old_bastion&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 移除整個 module 的記錄（module 被拆掉但資源還在雲端、要重新 import）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">terraform state rm <span class="s1">&#39;module.legacy_network&#39;</span></span></span></code></pre></div><p>移除後立刻跑 plan 驗證：原本針對這個資源的 destroy / error 應該消失。如果移除後 plan 反而出現 <code>+ create</code>（想重建這個資源），代表 code 裡還有對應的 resource block——要麼也刪 code，要麼這個資源需要 import 而不是 rm。</p>
<p>判斷「該 rm 還是該 import」的依據：資源在雲端還存在嗎？存在就 import（讓 state 重新追蹤它），不存在就 rm（清掉過時的記錄）。</p>
<h2 id="搬移資源state-mv-與-moved-block">搬移資源：state mv 與 moved block</h2>
<p>重構 Terraform code（把資源搬進 module、改 resource name、改 module 結構）時，state 裡的 resource address 會跟著變。如果不處理，plan 會判定「舊 address 要 destroy、新 address 要 create」——對 stateless 資源只是多等一次重建，對 RDS 這類 stateful 資源是資料遺失。</p>
<p>Terraform 1.1+ 的 <code>moved</code> block 是宣告式的搬遷，寫在 HCL 裡、可 review、可回滾：</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="k">moved</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  from</span> <span class="o">=</span> <span class="k">aws_security_group</span><span class="p">.</span><span class="k">web</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  to</span>   <span class="o">=</span> <span class="k">module</span><span class="p">.</span><span class="k">network</span><span class="p">.</span><span class="k">aws_security_group</span><span class="p">.</span><span class="k">web</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">}</span></span></code></pre></div><p>跑 plan 時 Terraform 會把 state 裡的舊 address 自動對應到新 address，plan 顯示 <code>(moved)</code> 而非 <code>destroy + create</code>。驗證 plan 為零變更後 apply，moved block 生效後可以從 code 裡刪掉。</p>
<p><code>terraform state mv</code> 是指令式的搬遷，直接操作 state 檔。它比 moved block 靈活（可以跨 state 搬）、但不可 review、不進版本控制、操作錯了只能靠備份回退。</p>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>moved block</th>
          <th>state mv</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>可 review</td>
          <td>是（寫在 HCL）</td>
          <td>否（直接改 state）</td>
      </tr>
      <tr>
          <td>可回滾</td>
          <td>是（刪 moved block）</td>
          <td>否（靠備份）</td>
      </tr>
      <tr>
          <td>跨 state 搬遷</td>
          <td>不支援</td>
          <td>支援</td>
      </tr>
      <tr>
          <td>適用情境</td>
          <td>同 state 內的重構</td>
          <td>跨 state 搬遷、moved 表達不了的複雜搬移</td>
      </tr>
  </tbody>
</table>
<p>優先用 moved block，state mv 留給 moved 做不到的場景。</p>
<h2 id="手動編輯-statepull--改--push">手動編輯 state：pull → 改 → push</h2>
<p>極少數情況需要直接編輯 state JSON——例如修正一個 resource 的 ID（某次 import 用了錯的 ID）、或手動修改一個 attribute 讓 plan 不再觸發不必要的變更。</p>





<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"># 拉到本地</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform state pull &gt; state-edit.json
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 編輯（用 jq 或文字編輯器，改目標 resource 的 attributes）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 極度小心：改錯任何欄位都可能讓 plan 產生破壞性差異</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 class="c1"># 推回遠端</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">terraform state push state-edit.json</span></span></code></pre></div><p><code>state push</code> 有 lineage 和 serial 檢查——如果本地的 state 跟遠端的 lineage 不同（來自不同的 init），push 會被拒絕。加 <code>-force</code> 可以繞過，但這意味著覆蓋遠端、丟棄遠端從你 pull 之後的所有變更。</p>
<p>手動編輯 state 的操作規則：備份 → 改一個欄位 → push → plan 驗證 → 確認只有預期的變化。批次改多個欄位時，每改一個就走一輪 push + plan，不要累積修改。</p>
<h2 id="從錯誤的-state-push-回復">從錯誤的 state push 回復</h2>
<p>如果 <code>state push</code> 推了一個錯誤的 state，回復路徑取決於 backend 有沒有版本歷史。</p>
<h3 id="s3-backend-有-versioning">S3 backend 有 versioning</h3>





<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"># 列出 state 檔的所有版本</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws s3api list-object-versions <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --bucket acme-tf-state <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --prefix prod/network/terraform.tfstate <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;Versions[].{VersionId:VersionId,LastModified:LastModified,Size:Size}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --output table
</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="c1"># 下載上一個正確的版本</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">aws s3api get-object <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --bucket acme-tf-state <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --key prod/network/terraform.tfstate <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --version-id <span class="s2">&#34;correct-version-id&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  state-recovered.json
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 用 terraform state push 推回</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">terraform state push state-recovered.json</span></span></code></pre></div><h3 id="沒有-versioning">沒有 versioning</h3>
<p>如果 bucket 沒開 versioning、又沒有本地備份，state 的上一個版本就沒了。這時候的選項：</p>
<ol>
<li>從 plan 的輸出反推哪些 resource 的 state 記錄是錯的，逐一用 <code>state rm</code> + <code>import</code> 修正</li>
<li>作為最後手段，刪掉整份 state、從零 import 所有資源——這等於重做一次完整的 IaC 導入</li>
</ol>
<p>這正是<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一</a>要求 state bucket 開 versioning 的理由——沒有版本歷史的 state backend，一次 push 錯誤就沒有回退路徑。</p>
<h2 id="state-backend-搬遷">State backend 搬遷</h2>
<p>接手的環境可能用本地 state（<code>.terraform/terraform.tfstate</code>）或者 state 放在不符合安全要求的位置（沒加密的 S3、沒有鎖表、甚至存在某個人的桌機上）。搬遷到正規的遠端 backend 是接手後的優先工作。</p>
<h3 id="本地--s3--dynamodb">本地 → S3 + DynamoDB</h3>





<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"># 在 backend.tf 加上遠端 backend 設定
</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">backend</span> <span class="s2">&#34;s3&#34;</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    bucket</span>         <span class="o">=</span> <span class="s2">&#34;acme-tf-state&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    key</span>            <span class="o">=</span> <span class="s2">&#34;prod/network/terraform.tfstate&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">    region</span>         <span class="o">=</span> <span class="s2">&#34;ap-northeast-1&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">    encrypt</span>        <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">    dynamodb_table</span> <span class="o">=</span> <span class="s2">&#34;acme-tf-lock&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  }
</span></span><span class="line"><span class="ln">10</span><span class="cl">}</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"># 重新初始化，Terraform 會偵測到 backend 變更並提示搬遷</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform init -migrate-state
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 確認搬遷成功</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">terraform plan  <span class="c1"># 應該顯示零變更</span></span></span></code></pre></div><p><code>-migrate-state</code> 會把本地 state 的內容寫入新的遠端 backend。搬遷後本地的 <code>.terraform/terraform.tfstate</code> 變成一個指向遠端 backend 的指標，不再存放實際 state 內容。</p>
<h3 id="舊-s3--新-s3">舊 S3 → 新 S3</h3>
<p>跟本地搬遷流程相同——改 backend.tf 的 bucket/key/region，跑 <code>terraform init -migrate-state</code>。Terraform 會從舊 backend 讀 state、寫入新 backend。</p>
<p>搬遷後驗證：plan 為零變更、新 bucket 裡有 state 檔、舊 bucket 的 state 檔可以保留一段時間作為備份。搬遷過程中 DynamoDB 的 lock 會確保沒有人同時 apply。</p>
<p>搬遷期間的風險：如果有人在你改 backend.tf 之後、跑 init 之前，用舊 backend 跑了 apply，新 backend 的 state 會缺少那次變更。搬遷時通知團隊暫停所有 Terraform 操作，搬遷完成後再恢復。</p>
<p>時程參考：單一 orphaned entry 的 rm 操作約 15-30 分鐘（含備份和驗證）。Backend migration 約 1-2 小時。5-10 個問題項的完整 state 整理約半天到一天。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/takeover/partial-iac-no-docs/" data-link-title="有半套 IaC 但文件缺失的環境接管" data-link-desc="IaC 覆蓋不完整、部分資源在 state 外、文件缺失的環境怎麼盤點差距、修復 state 健康、收斂 drift 並重建文件">有半套 IaC 但文件缺失的環境接管</a>：本篇的上層操作流程</li>
<li>→ <a href="/blog/infra/takeover/partial-iac-drift-triage/" data-link-title="Drift 分類處理指南" data-link-desc="接手半套 IaC 環境時，怎麼讀 plan 輸出分類 drift、判斷保留還是回退、處理 stateful 資源的高風險漂移，以及批次收斂的工作流">Drift 分類處理</a>：state 修復完成後，下一步是處理 managed resource 的 drift</li>
<li>→ <a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a>：state backend 的設計與 versioning 要求</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>：moved block 在環境拆分 retrofit 裡的角色</li>
</ul>
]]></content:encoded></item><item><title>State Lock</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/state-lock/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/state-lock/</guid><description>&lt;p>State Lock 的核心概念是「讓同一份基礎設施狀態一次只被一個 apply 修改」。它支撐 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a> 的治理，避免 CI job 或人工操作併發覆寫 state。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>State Lock 位在 IaC state backend、plan / apply workflow 與平台資源之間，常由 Terraform backend、Pulumi state 或平台鎖定機制提供。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>多個 pipeline 同時 apply 同一個 workspace。&lt;/li>
&lt;li>state file 出現併發覆寫或 partial apply 後不一致。&lt;/li>
&lt;li>apply 長時間卡住需要判斷 lock 是否仍有效。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>兩個 PR 同時修改 production network。第一個 workflow 取得 state lock 後進入 apply，第二個 workflow 等待或失敗，避免兩次變更同時寫入 state。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>State Lock 要定義 lock backend、timeout、人工解鎖條件、環境隔離與失敗處理，讓 IaC apply 保持序列化。&lt;/p></description><content:encoded><![CDATA[<p>State Lock 的核心概念是「讓同一份基礎設施狀態一次只被一個 apply 修改」。它支撐 <a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 的治理，避免 CI job 或人工操作併發覆寫 state。</p>
<h2 id="概念位置">概念位置</h2>
<p>State Lock 位在 IaC state backend、plan / apply workflow 與平台資源之間，常由 Terraform backend、Pulumi state 或平台鎖定機制提供。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>多個 pipeline 同時 apply 同一個 workspace。</li>
<li>state file 出現併發覆寫或 partial apply 後不一致。</li>
<li>apply 長時間卡住需要判斷 lock 是否仍有效。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>兩個 PR 同時修改 production network。第一個 workflow 取得 state lock 後進入 apply，第二個 workflow 等待或失敗，避免兩次變更同時寫入 state。</p>
<h2 id="設計責任">設計責任</h2>
<p>State Lock 要定義 lock backend、timeout、人工解鎖條件、環境隔離與失敗處理，讓 IaC apply 保持序列化。</p>
]]></content:encoded></item></channel></rss>