<?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>Drift on Tarragon</title><link>https://tarrragon.github.io/blog/tags/drift/</link><description>Recent content in Drift 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/drift/index.xml" rel="self" type="application/rss+xml"/><item><title>IaC plan、apply、drift 與 recovery 流程</title><link>https://tarrragon.github.io/blog/ci/iac-platform-deploy/plan-apply-drift-recovery-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/iac-platform-deploy/plan-apply-drift-recovery-flow/</guid><description>&lt;p>IaC 發布流程的核心責任是把基礎設施變更變成可審查、可套用、可追溯的狀態轉移。Terraform、Pulumi、Helm 或平台自動化會改變網路、權限、資料庫、節點、DNS 與部署平台，因此 CI/CD 要把 plan、review、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> 與 recovery 分成明確 gate。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>IaC 的風險集中在共享狀態與不可逆資源。應用部署失敗常可回退 artifact；基礎設施變更可能刪除資料、替換節點、改掉 IAM 權限或讓 state 與真實環境分叉。發布流程應讓 reviewer 在 apply 前看到「將要改什麼」，並讓 apply 後能確認「環境是否真的符合宣告」。&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>Plan&lt;/td>
 &lt;td>預覽資源差異與風險&lt;/td>
 &lt;td>create / update / replace / destroy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review&lt;/td>
 &lt;td>審核變更意圖、權限與影響面&lt;/td>
 &lt;td>高風險資源、跨環境、資料資源&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Apply&lt;/td>
 &lt;td>在鎖定狀態下套用變更&lt;/td>
 &lt;td>state lock、timeout、partial apply&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Verify&lt;/td>
 &lt;td>確認環境符合預期&lt;/td>
 &lt;td>health、policy、smoke、connectivity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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>&lt;/td>
 &lt;td>偵測真實環境與宣告分叉&lt;/td>
 &lt;td>手動 hotfix、console edit、外部系統&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>回退、補正或 state repair&lt;/td>
 &lt;td>是否能安全恢復服務與 state&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Plan 階段負責產生可審查差異。Plan 是 reviewer 判斷資源替換、權限擴大、資料刪除與網路暴露的主要材料。CI 應保留 plan artifact，讓 apply 使用同一份輸入與版本。&lt;/p>
&lt;p>Review 階段負責把風險放到正確 owner。平台、資安、資料庫或服務 owner 應依資源類型參與審核；高風險變更需要額外 gate，例如 maintenance window、人工 approval 或雙人審核。&lt;/p>
&lt;p>Apply 階段負責把宣告狀態寫入環境。&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock&lt;/a>、credential、workspace 與環境變數都要固定；partial apply 或 timeout 後，要先判斷 state 與真實資源是否一致，再決定下一步。&lt;/p>
&lt;p>Verify 階段負責確認平台可用。Apply 成功只代表 provider API 接受變更；仍需要 connectivity test、policy check、service smoke test、DNS / certificate check 或 cluster health，確認服務真的能跑。&lt;/p>
&lt;p>&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> 階段負責發現宣告與現況分叉。手動 hotfix、雲端 console 調整、外部 controller 或 provider 預設值都可能造成 drift；drift detection 要定期執行，並把修復責任導回宣告檔。&lt;/p>
&lt;p>Recovery 階段負責處理失敗套用。IaC 回復不一定是 &lt;code>git revert&lt;/code> 後 apply；可能需要 import、state mv、taint / untaint、手動修復資料資源或 forward fix。流程要先保護資料與服務，再修正宣告與 state。&lt;/p>
&lt;h2 id="plan-review-判讀">Plan review 判讀&lt;/h2>
&lt;p>Plan review 的責任是讓變更影響在 apply 前被看見。Reviewer 應依資源語意判斷，讓 diff 行數退居輔助訊號。&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>destroy&lt;/code>&lt;/td>
 &lt;td>資源將被刪除&lt;/td>
 &lt;td>確認資料、依賴與備份&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>replace&lt;/code>&lt;/td>
 &lt;td>先刪後建或重建資源&lt;/td>
 &lt;td>檢查 downtime、IP、DNS、資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IAM 權限擴大&lt;/td>
 &lt;td>blast radius 增加&lt;/td>
 &lt;td>資安或平台 owner 審核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network 開放&lt;/td>
 &lt;td>暴露面增加&lt;/td>
 &lt;td>檢查 security group / firewall&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>State 大量漂移&lt;/td>
 &lt;td>宣告與現況長期分叉&lt;/td>
 &lt;td>先處理 drift，再進 feature change&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表讓 review 從「有人按 approve」變成風險判讀。IaC review 的價值在於提前看見不可逆或高代價變更。&lt;/p></description><content:encoded><![CDATA[<p>IaC 發布流程的核心責任是把基礎設施變更變成可審查、可套用、可追溯的狀態轉移。Terraform、Pulumi、Helm 或平台自動化會改變網路、權限、資料庫、節點、DNS 與部署平台，因此 CI/CD 要把 plan、review、apply、<a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 與 recovery 分成明確 gate。</p>
<h2 id="流程定位">流程定位</h2>
<p>IaC 的風險集中在共享狀態與不可逆資源。應用部署失敗常可回退 artifact；基礎設施變更可能刪除資料、替換節點、改掉 IAM 權限或讓 state 與真實環境分叉。發布流程應讓 reviewer 在 apply 前看到「將要改什麼」，並讓 apply 後能確認「環境是否真的符合宣告」。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plan</td>
          <td>預覽資源差異與風險</td>
          <td>create / update / replace / destroy</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>審核變更意圖、權限與影響面</td>
          <td>高風險資源、跨環境、資料資源</td>
      </tr>
      <tr>
          <td>Apply</td>
          <td>在鎖定狀態下套用變更</td>
          <td>state lock、timeout、partial apply</td>
      </tr>
      <tr>
          <td>Verify</td>
          <td>確認環境符合預期</td>
          <td>health、policy、smoke、connectivity</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a></td>
          <td>偵測真實環境與宣告分叉</td>
          <td>手動 hotfix、console edit、外部系統</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>回退、補正或 state repair</td>
          <td>是否能安全恢復服務與 state</td>
      </tr>
  </tbody>
</table>
<p>Plan 階段負責產生可審查差異。Plan 是 reviewer 判斷資源替換、權限擴大、資料刪除與網路暴露的主要材料。CI 應保留 plan artifact，讓 apply 使用同一份輸入與版本。</p>
<p>Review 階段負責把風險放到正確 owner。平台、資安、資料庫或服務 owner 應依資源類型參與審核；高風險變更需要額外 gate，例如 maintenance window、人工 approval 或雙人審核。</p>
<p>Apply 階段負責把宣告狀態寫入環境。<a href="/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock</a>、credential、workspace 與環境變數都要固定；partial apply 或 timeout 後，要先判斷 state 與真實資源是否一致，再決定下一步。</p>
<p>Verify 階段負責確認平台可用。Apply 成功只代表 provider API 接受變更；仍需要 connectivity test、policy check、service smoke test、DNS / certificate check 或 cluster health，確認服務真的能跑。</p>
<p><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 階段負責發現宣告與現況分叉。手動 hotfix、雲端 console 調整、外部 controller 或 provider 預設值都可能造成 drift；drift detection 要定期執行，並把修復責任導回宣告檔。</p>
<p>Recovery 階段負責處理失敗套用。IaC 回復不一定是 <code>git revert</code> 後 apply；可能需要 import、state mv、taint / untaint、手動修復資料資源或 forward fix。流程要先保護資料與服務，再修正宣告與 state。</p>
<h2 id="plan-review-判讀">Plan review 判讀</h2>
<p>Plan review 的責任是讓變更影響在 apply 前被看見。Reviewer 應依資源語意判斷，讓 diff 行數退居輔助訊號。</p>
<table>
  <thead>
      <tr>
          <th>Plan 訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>destroy</code></td>
          <td>資源將被刪除</td>
          <td>確認資料、依賴與備份</td>
      </tr>
      <tr>
          <td><code>replace</code></td>
          <td>先刪後建或重建資源</td>
          <td>檢查 downtime、IP、DNS、資料</td>
      </tr>
      <tr>
          <td>IAM 權限擴大</td>
          <td>blast radius 增加</td>
          <td>資安或平台 owner 審核</td>
      </tr>
      <tr>
          <td>Network 開放</td>
          <td>暴露面增加</td>
          <td>檢查 security group / firewall</td>
      </tr>
      <tr>
          <td>State 大量漂移</td>
          <td>宣告與現況長期分叉</td>
          <td>先處理 drift，再進 feature change</td>
      </tr>
  </tbody>
</table>
<p>這張表讓 review 從「有人按 approve」變成風險判讀。IaC review 的價值在於提前看見不可逆或高代價變更。</p>
<h2 id="drift-處理路由">Drift 處理路由</h2>
<p>Drift 處理的責任是把現況重新帶回可管理狀態。Drift 發現後不應直接 apply 覆蓋，因為 drift 可能是事故 hotfix、外部系統自動調整或宣告檔過期。</p>
<ol>
<li>確認 drift 來源：人工 hotfix、provider 預設、外部 controller 或宣告過期。</li>
<li>判斷 drift 是否仍需要保留：若是真實修復，應回寫到 IaC。</li>
<li>判斷 apply 是否會破壞服務：特別看 replacement、destroy、權限與 network。</li>
<li>修正宣告或 state：必要時使用 import、state mv 或 provider-specific repair。</li>
<li>重新 plan，確認差異收斂到預期。</li>
</ol>
<p>這個路由讓 drift 修復具備審查性。直接在 console 裡補到看起來正常，會讓下一次 CI apply 把修復覆蓋掉。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>反模式的共同問題是把 IaC 降成指令自動化，忽略它承擔的狀態治理責任。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>plan 與 apply 使用不同輸入</td>
          <td>review 內容與實際套用內容分叉</td>
          <td>保存 plan artifact 或鎖定版本</td>
      </tr>
      <tr>
          <td>沒有 <a href="/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock</a></td>
          <td>併發 apply 覆寫狀態</td>
          <td>使用 remote backend 與 locking</td>
      </tr>
      <tr>
          <td>drift 長期忽略</td>
          <td>宣告失去可信度</td>
          <td>定期 drift detection 與 owner 路由</td>
      </tr>
      <tr>
          <td>高風險資源無額外 gate</td>
          <td>資料或網路變更直接進環境</td>
          <td>environment protection / approval</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>IaC 部署總覽：回 <a href="../">IaC / Platform 部署 CI/CD</a>。</li>
<li>環境保護：讀 <a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a>。</li>
<li>Gate 原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>Console 唯讀鐵律與最小可行資源集合</title><link>https://tarrragon.github.io/blog/infra/01-minimal-iac/console-readonly-minimal-viable/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/01-minimal-iac/console-readonly-minimal-viable/</guid><description>&lt;p>state 管好之後，下一件要釘死的事是保證 state 與現實不會分歧。&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 工具選型與 state 地基&lt;/a>建立了 state 作為工具記憶的角色，這篇處理的是怎麼讓這份記憶不被背後偷改 — Console 唯讀鐵律，以及怎麼用最小資源集合驗證整條 IaC 鏈路端到端可運作。&lt;/p>
&lt;h2 id="console-唯讀鐵律把-console-當儀表板不當方向盤">Console 唯讀鐵律：把 Console 當儀表板，不當方向盤&lt;/h2>
&lt;p>Console 唯讀鐵律是一條操作紀律：雲端 Console 只用來觀察與排查，所有會改變資源的動作都回到程式碼走 apply。這條紀律維護的是 state 與現實的一致 — IaC 工具能正確運作的前提，是它的 state 反映得了真實世界，而每一次在 Console 點按鈕改設定，都是在 state 不知情的情況下動了現實。&lt;/p>
&lt;h3 id="drift-的延遲浮現">drift 的延遲浮現&lt;/h3>
&lt;p>state 與現實的分歧叫 drift。drift 的後果在後續某次 apply 時才浮現——工具用過時的 state 比對雲端現況、把手動設定判定為「不該存在」並覆蓋掉，手動改的當下一切正常。手動改的當下一切正常，後果要等到下一次不相關的 apply 才出現。&lt;/p>
&lt;p>常見的 drift 路徑：在 Console 手動加了一條 security group 規則（例如讓外部監控系統連進來），state 不知道這條規則存在。後續某次 apply 時，工具比對 state 和雲端現況、把這條規則判定為「不在記憶裡」而刪除。同樣的機制也發生在手動調整的 RDS parameter group（例如增加 &lt;code>max_connections&lt;/code>）— 後續 apply 會把參數重設回程式碼裡的值。&lt;/p>
&lt;p>Console 改得越多、與程式碼分歧越久，某次例行 apply 就越可能掃掉一批沒人記得的手動設定。drift 的累積是單調遞增的 — 每一次手動改動都加一筆，沒有任何自然機制會讓它減少。&lt;/p>
&lt;h3 id="drift-偵測">drift 偵測&lt;/h3>
&lt;p>主動偵測 drift 的方式是定期跑 &lt;code>terraform plan&lt;/code> 而不做 apply — plan 的輸出會列出「code 描述的狀態」與「雲端現況」之間的差異。如果 plan 在沒有 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">&lt;span class="c1"># 定期 drift 偵測：plan 結果非零就告警&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">terraform plan -detailed-exitcode
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&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 接進 CI，讓 drift 在累積之前就被發現。判讀 plan 輸出時，重點看那些「會被 Terraform 改回去」的差異 — 它們就是手動變更的痕跡。&lt;/p>
&lt;h3 id="import-的痛苦">import 的痛苦&lt;/h3>
&lt;p>鐵律越早立越好，因為回頭納管的代價隨時間累積。手動建的資源要納入 IaC，得先用 &lt;code>terraform import&lt;/code> 把現實資源綁進 state，再補一段與現實完全吻合的 HCL：&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 import aws_security_group.web sg-0abc123def456&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>import 只把資源 ID 寫進 state，不會幫忙生程式碼。那個資源在 Console 上被點出來的每一個屬性 — 每條 ingress 規則、每個 tag、每項關聯設定 — 都得一字不差地補成 HCL。任何一項對不上，下次 apply 就會試圖把現實改回程式碼寫的版本 — 對 security group 來說可能是把一條正在用的規則刪掉，對 RDS 來說可能是觸發一次重啟。&lt;/p>
&lt;p>Terraform 1.5 之後提供了 &lt;code>import&lt;/code> 區塊，可以在 HCL 裡宣告式地寫 import，配合 &lt;code>terraform plan -generate-config-out=generated.tf&lt;/code> 自動生成對應的資源描述。這比手寫減少了大量逆向工程，但生成的 code 仍然需要人工確認每一個屬性是否正確 — 自動生成是起點，不是終點。&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">import&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n"> to&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_security_group&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">web&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"> id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;sg-0abc123def456&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>import 成本隨資源數量非線性增長。一個資源的逆向工程可控，幾十個各自手動微調過的資源累積起來，團隊會停止嘗試納管，環境分裂成 IaC 管理的部分和手動管理的部分。第一天就立鐵律，要納管的存量永遠是零。&lt;/p></description><content:encoded><![CDATA[<p>state 管好之後，下一件要釘死的事是保證 state 與現實不會分歧。<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 作為工具記憶的角色，這篇處理的是怎麼讓這份記憶不被背後偷改 — Console 唯讀鐵律，以及怎麼用最小資源集合驗證整條 IaC 鏈路端到端可運作。</p>
<h2 id="console-唯讀鐵律把-console-當儀表板不當方向盤">Console 唯讀鐵律：把 Console 當儀表板，不當方向盤</h2>
<p>Console 唯讀鐵律是一條操作紀律：雲端 Console 只用來觀察與排查，所有會改變資源的動作都回到程式碼走 apply。這條紀律維護的是 state 與現實的一致 — IaC 工具能正確運作的前提，是它的 state 反映得了真實世界，而每一次在 Console 點按鈕改設定，都是在 state 不知情的情況下動了現實。</p>
<h3 id="drift-的延遲浮現">drift 的延遲浮現</h3>
<p>state 與現實的分歧叫 drift。drift 的後果在後續某次 apply 時才浮現——工具用過時的 state 比對雲端現況、把手動設定判定為「不該存在」並覆蓋掉，手動改的當下一切正常。手動改的當下一切正常，後果要等到下一次不相關的 apply 才出現。</p>
<p>常見的 drift 路徑：在 Console 手動加了一條 security group 規則（例如讓外部監控系統連進來），state 不知道這條規則存在。後續某次 apply 時，工具比對 state 和雲端現況、把這條規則判定為「不在記憶裡」而刪除。同樣的機制也發生在手動調整的 RDS parameter group（例如增加 <code>max_connections</code>）— 後續 apply 會把參數重設回程式碼裡的值。</p>
<p>Console 改得越多、與程式碼分歧越久，某次例行 apply 就越可能掃掉一批沒人記得的手動設定。drift 的累積是單調遞增的 — 每一次手動改動都加一筆，沒有任何自然機制會讓它減少。</p>
<h3 id="drift-偵測">drift 偵測</h3>
<p>主動偵測 drift 的方式是定期跑 <code>terraform plan</code> 而不做 apply — plan 的輸出會列出「code 描述的狀態」與「雲端現況」之間的差異。如果 plan 在沒有 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"><span class="c1"># 定期 drift 偵測：plan 結果非零就告警</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform plan -detailed-exitcode
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># exit code 0 = 無差異, 1 = 錯誤, 2 = 有差異</span></span></span></code></pre></div><p>把這個 plan 接進 CI，讓 drift 在累積之前就被發現。判讀 plan 輸出時，重點看那些「會被 Terraform 改回去」的差異 — 它們就是手動變更的痕跡。</p>
<h3 id="import-的痛苦">import 的痛苦</h3>
<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 就會試圖把現實改回程式碼寫的版本 — 對 security group 來說可能是把一條正在用的規則刪掉，對 RDS 來說可能是觸發一次重啟。</p>
<p>Terraform 1.5 之後提供了 <code>import</code> 區塊，可以在 HCL 裡宣告式地寫 import，配合 <code>terraform plan -generate-config-out=generated.tf</code> 自動生成對應的資源描述。這比手寫減少了大量逆向工程，但生成的 code 仍然需要人工確認每一個屬性是否正確 — 自動生成是起點，不是終點。</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">web</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><p>import 成本隨資源數量非線性增長。一個資源的逆向工程可控，幾十個各自手動微調過的資源累積起來，團隊會停止嘗試納管，環境分裂成 IaC 管理的部分和手動管理的部分。第一天就立鐵律，要納管的存量永遠是零。</p>
<h3 id="鐵律靠權限落地不靠自律">鐵律靠權限落地，不靠自律</h3>
<p>光靠約定「別在 Console 改」撐不久，救火當下手最快的永遠是 Console。真正讓鐵律站得住的，是把人的日常身分收斂成唯讀、把寫入權限留給跑 apply 的自動化身分，讓「在 Console 改不動」變成預設狀態。</p>
<p>這道權限地基屬於<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>的範圍，本階先確立紀律方向：人類日常用的 IAM 身分只有 <code>ReadOnlyAccess</code>，寫入權限只存在於 CI pipeline 使用的 role，這個 role 靠 OIDC 取得短期憑證（不存長期 key）。具體的 IAM 設計和 OIDC 信任關係在模組二展開。</p>
<h2 id="最小可行能-apply-出一個完整環境的最小資源集合">最小可行：能 apply 出一個完整環境的最小資源集合</h2>
<p>最小可行 IaC 的目標是用最少的資源，跑出一條「改程式碼 → review → apply → 環境真的變了」的完整迴路。它承擔的責任是驗證地基本身能動，把所有服務都搬上來是後面的事。判準是這套程式碼能獨立 apply 出一個雖小但自洽、別人能重現的環境。</p>
<h3 id="最小集合的組成">最小集合的組成</h3>
<table>
  <thead>
      <tr>
          <th>資源</th>
          <th>職責</th>
          <th>驗證標準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>S3 bucket + DynamoDB（鎖表）</td>
          <td>remote state backend</td>
          <td>state 能寫入、鎖能取得和釋放</td>
      </tr>
      <tr>
          <td>IAM role（唯讀 + apply）</td>
          <td>人類唯讀、自動化寫入的身分基線</td>
          <td>人登入後 Console 改不動東西</td>
      </tr>
      <tr>
          <td>VPC + 最少的 subnet</td>
          <td>網路骨架</td>
          <td>資源能被放進正確的 subnet</td>
      </tr>
      <tr>
          <td>一個微小的真實資源</td>
          <td>端到端驗證</td>
          <td>apply 出現、destroy 消失</td>
      </tr>
  </tbody>
</table>
<p>把一個微小資源（例如一個 S3 bucket 或一台最小的測試 EC2）刻意留在最小集合裡，是因為它是最便宜的端到端驗證。apply 跑完後它確實出現、<code>terraform destroy</code> 後它確實消失，就證明從程式碼到雲端的整條鏈路是通的。</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;smoke_test&#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-smoke-test-${var.env}&#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 class="n">  tags</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">    purpose</span> <span class="o">=</span> <span class="s2">&#34;validate-iac-pipeline&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">    env</span>     <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">env</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">    owner</span>   <span class="o">=</span> <span class="s2">&#34;platform&#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><h3 id="刻意不放進來的東西">刻意不放進來的東西</h3>
<p>正式的應用服務、資料庫、跨環境的複製、複雜的模組抽象，全部留到地基驗證通過之後。在 state 與 Console 唯讀都還沒站穩前就堆服務，等於把房子蓋在還沒灌漿的地基上。</p>
<p>常見的過早引入包括：在最小集合裡就加 RDS（state 操作出問題時資料庫可能被影響）、在還沒有環境分離前就建多層 module 嵌套（驗證地基的複雜度不應該來自抽象層）、在一個人開發時就配好 Atlantis 或 Terraform Cloud 的完整 PR 流程（固定成本太高、且需要<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 自動化，讓基礎設施可審查、可回溯、可交接">模組七</a>的完整護欄才能發揮價值）。</p>
<p>網路骨架怎麼長、身分怎麼切，分別由<a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>與<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>接手深入；這一階只需要它們各自最薄的一層，湊出一個能 apply、能 destroy、能交接的閉環。</p>
<h3 id="驗證閉環">驗證閉環</h3>
<p>最小集合就位後的驗證步驟：</p>
<ol>
<li><code>terraform init</code> — 確認 backend 設定正確、provider 能下載</li>
<li><code>terraform plan</code> — 確認 plan 輸出符合預期、沒有意外的 destroy 或 replace</li>
<li><code>terraform apply</code> — 確認資源在雲端確實出現</li>
<li><code>terraform plan</code>（再跑一次）— 確認輸出是零差異，代表 state 與現實一致</li>
<li><code>terraform destroy</code> — 確認資源能被乾淨拆除（smoke test 資源）</li>
</ol>
<p>第四步「再跑一次 plan」是容易被跳過卻最關鍵的一步。如果第一次 apply 之後立刻 plan 就出現差異，代表 provider 的行為和 HCL 描述之間有落差（例如某些屬性是雲端自動設的、HCL 沒寫），這類落差要在最小集合階段就修掉，等到正式服務上線後再修，成本會高很多。</p>
<p>最小可行 IaC 跑通後，下一步是收斂身分與憑證——把 Console 唯讀鐵律從紀律升級成權限限制，見<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>。</p>
<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 怎麼管、backend 怎麼選</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/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/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>
</ul>
]]></content:encoded></item><item><title>Drift（設定漂移）</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/drift/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/drift/</guid><description>&lt;p>Drift 指的是 IaC 的 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/state/" data-link-title="State（IaC 狀態檔）" data-link-desc="IaC 工具用來記錄每個納管資源在雲端真實樣貌的快照，是比對差異與排定操作順序的依據">state&lt;/a> 記錄與雲端上的實際資源狀態之間的不一致。最常見的來源是有人繞過 IaC、直接在 Console 手動修改資源設定——state 不知道這次改動發生了，下一次 &lt;code>plan&lt;/code> 時工具會把手動改的設定判定為「不在我的記憶裡、要修正回程式碼的版本」。&lt;/p>
&lt;p>Drift 的代價會延遲浮現。手動改的當下看起來沒問題——設定改了、服務正常。問題出在後續某次不相關的 &lt;code>apply&lt;/code>：工具用過時的 state 去比對，把手動改的設定覆蓋掉，服務因此斷線，而且在 PR 裡看不到這件事發生過。Drift 累積越多，每次 &lt;code>apply&lt;/code> 的不確定性越高，最終團隊會開始害怕跑 &lt;code>apply&lt;/code>，IaC 名存實亡。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Drift 是 Console 唯讀鐵律存在的根本理由。&lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/console-readonly-minimal-viable/" data-link-title="Console 唯讀鐵律與最小可行資源集合" data-link-desc="Console 只用來看不用來改的操作紀律、drift 的延遲浮現與偵測，以及能跑出第一個完整 apply 迴路的最小資源集合">模組一：Console 唯讀鐵律&lt;/a>用權限機制（人類身分唯讀、寫入權限留給自動化身分）讓「在 Console 改不動」成為預設狀態，從源頭消除 drift 的產生。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>Drift 存在的訊號：&lt;code>terraform plan&lt;/code> 在沒人改過程式碼的情況下顯示變更（代表有人在 Console 動了東西）、團隊開始說「跑 plan 前先看看有沒有奇怪的差異」、某次例行 apply 意外改掉了不該改的設定。&lt;/p>
&lt;p>偵測 drift 的主動方式是定期跑 &lt;code>terraform plan&lt;/code> 但不 apply，把 diff 輸出當成 drift 偵測的報告。Terraform Cloud 有內建的 drift detection 功能，定期比對 state 與雲端現實。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>處理 drift 時要決定：&lt;/p>
&lt;ul>
&lt;li>偵測頻率：每次 PR 觸發 plan（被動偵測）vs 定期排程 plan（主動偵測）&lt;/li>
&lt;li>修正方向：把雲端改回程式碼的版本（&lt;code>apply&lt;/code>），還是把程式碼改成雲端的版本（更新 HCL）——取捨在「程式碼是 source of truth」vs「手動改的設定有它的理由」&lt;/li>
&lt;li>預防機制：Console 唯讀權限、CI gate 攔截未經 review 的 apply&lt;/li>
&lt;/ul>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/state/" data-link-title="State（IaC 狀態檔）" data-link-desc="IaC 工具用來記錄每個納管資源在雲端真實樣貌的快照，是比對差異與排定操作順序的依據">State&lt;/a> — drift 是 state 與現實的落差&lt;/li>
&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> — drift 破壞 IaC 的 source of truth 地位&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Drift 指的是 IaC 的 <a href="/blog/infra/knowledge-cards/state/" data-link-title="State（IaC 狀態檔）" data-link-desc="IaC 工具用來記錄每個納管資源在雲端真實樣貌的快照，是比對差異與排定操作順序的依據">state</a> 記錄與雲端上的實際資源狀態之間的不一致。最常見的來源是有人繞過 IaC、直接在 Console 手動修改資源設定——state 不知道這次改動發生了，下一次 <code>plan</code> 時工具會把手動改的設定判定為「不在我的記憶裡、要修正回程式碼的版本」。</p>
<p>Drift 的代價會延遲浮現。手動改的當下看起來沒問題——設定改了、服務正常。問題出在後續某次不相關的 <code>apply</code>：工具用過時的 state 去比對，把手動改的設定覆蓋掉，服務因此斷線，而且在 PR 裡看不到這件事發生過。Drift 累積越多，每次 <code>apply</code> 的不確定性越高，最終團隊會開始害怕跑 <code>apply</code>，IaC 名存實亡。</p>
<h2 id="概念位置">概念位置</h2>
<p>Drift 是 Console 唯讀鐵律存在的根本理由。<a href="/blog/infra/01-minimal-iac/console-readonly-minimal-viable/" data-link-title="Console 唯讀鐵律與最小可行資源集合" data-link-desc="Console 只用來看不用來改的操作紀律、drift 的延遲浮現與偵測，以及能跑出第一個完整 apply 迴路的最小資源集合">模組一：Console 唯讀鐵律</a>用權限機制（人類身分唯讀、寫入權限留給自動化身分）讓「在 Console 改不動」成為預設狀態，從源頭消除 drift 的產生。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>Drift 存在的訊號：<code>terraform plan</code> 在沒人改過程式碼的情況下顯示變更（代表有人在 Console 動了東西）、團隊開始說「跑 plan 前先看看有沒有奇怪的差異」、某次例行 apply 意外改掉了不該改的設定。</p>
<p>偵測 drift 的主動方式是定期跑 <code>terraform plan</code> 但不 apply，把 diff 輸出當成 drift 偵測的報告。Terraform Cloud 有內建的 drift detection 功能，定期比對 state 與雲端現實。</p>
<h2 id="設計責任">設計責任</h2>
<p>處理 drift 時要決定：</p>
<ul>
<li>偵測頻率：每次 PR 觸發 plan（被動偵測）vs 定期排程 plan（主動偵測）</li>
<li>修正方向：把雲端改回程式碼的版本（<code>apply</code>），還是把程式碼改成雲端的版本（更新 HCL）——取捨在「程式碼是 source of truth」vs「手動改的設定有它的理由」</li>
<li>預防機制：Console 唯讀權限、CI gate 攔截未經 review 的 apply</li>
</ul>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/state/" data-link-title="State（IaC 狀態檔）" data-link-desc="IaC 工具用來記錄每個納管資源在雲端真實樣貌的快照，是比對差異與排定操作順序的依據">State</a> — drift 是 state 與現實的落差</li>
<li><a href="/blog/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">IaC</a> — drift 破壞 IaC 的 source of truth 地位</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>Drift 分類處理指南</title><link>https://tarrragon.github.io/blog/infra/takeover/partial-iac-drift-triage/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/takeover/partial-iac-drift-triage/</guid><description>&lt;p>&lt;code>terraform plan&lt;/code> 跑完後如果出現非零差異，每一行差異都需要判斷：這是該保留的手動改動，還是該回退的意外漂移。這些差異就是 drift — state 記錄的狀態跟雲端實際狀態之間的落差。判斷錯誤的代價從「設定被覆蓋」到「stateful 資源被重建導致資料遺失」不等，所以分類要在 apply 之前完成。半套 IaC 環境的 drift 通常比全 IaC 環境更多，因為有人在 Console 改了 state 不知道的資源。&lt;/p>
&lt;h2 id="讀-plan-輸出三種變更類型">讀 plan 輸出：三種變更類型&lt;/h2>
&lt;p>&lt;code>terraform plan&lt;/code> 的輸出用符號標示每個資源的預期變更。三種類型的風險等級不同，處理方式也不同：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl"># in-place update（~）：修改屬性，資源本身不動
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">~ resource &amp;#34;aws_security_group_rule&amp;#34; &amp;#34;api_ingress&amp;#34; {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> ~ cidr_blocks = [&amp;#34;10.0.0.0/16&amp;#34;] -&amp;gt; [&amp;#34;10.0.1.0/24&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"># forces replacement（-/+）：刪除後重建，新資源取得新 ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">-/+ resource &amp;#34;aws_db_instance&amp;#34; &amp;#34;primary&amp;#34; {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> ~ identifier = &amp;#34;app-prod&amp;#34; -&amp;gt; &amp;#34;app-prod-v2&amp;#34; # forces replacement
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"># destroy（-）：刪除資源
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">- resource &amp;#34;aws_security_group&amp;#34; &amp;#34;legacy_api&amp;#34; {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> }&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>符號&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;code>~&lt;/code>&lt;/td>
 &lt;td>in-place update&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>逐項判斷，多數可安全 apply&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>-/+&lt;/code>&lt;/td>
 &lt;td>forces replacement&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>stateful 資源絕對不能直接 apply&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>-&lt;/code>&lt;/td>
 &lt;td>destroy&lt;/td>
 &lt;td>極高&lt;/td>
 &lt;td>代表雲端有但 code 沒有，apply 會刪除&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>-&lt;/code>（destroy）是最危險的類型。它代表某個資源存在於雲端但不在 Terraform code 裡——可能是手動建的、可能是從 state 被 &lt;code>state rm&lt;/code> 移除過、也可能是前任維護者刪了 code 但沒跑 apply。不論原因，直接 apply 會把這個資源從雲端刪除。&lt;/p>
&lt;p>&lt;code>-/+&lt;/code>（forces replacement）的危險在於它看起來像修改但實際是先刪後建。對 stateless 資源（security group rule、IAM policy）影響有限，對 stateful 資源（RDS、EBS volume）意味著資料遺失。&lt;/p>
&lt;h2 id="故意的-drift-vs-意外的-drift">故意的 drift vs 意外的 drift&lt;/h2>
&lt;p>不是所有 drift 都是問題。接手的環境裡，手動改動可能有兩種來源：&lt;/p>
&lt;p>&lt;strong>故意的改動&lt;/strong>是前任維護者為了解決特定問題而做的。常見形態：臨時開了一條 security group 規則讓外部監控系統連進來、調高了 RDS 的 &lt;code>max_connections&lt;/code> 參數來應對流量成長、手動把 instance type 從 &lt;code>t3.small&lt;/code> 升到 &lt;code>t3.medium&lt;/code> 因為記憶體不夠。這類改動通常是正確的操作決策，只是沒有同步回 code。&lt;/p>
&lt;p>&lt;strong>意外的漂移&lt;/strong>是無意中造成的。常見形態：在 Console 測試時改了某個設定但忘了改回來、另一個 Terraform workspace 的 apply 動到了共用的資源、AWS 自動更新了某些屬性（如 default security group 的描述）。&lt;/p>
&lt;p>區分兩者的方法是查 CloudTrail——看這個改動是誰做的、什麼時候、有沒有對應的 ticket 或 changelog 記錄。如果 CloudTrail 顯示改動發生在一次事故期間、由當時的值班工程師執行，大概率是故意的。如果改動來自一個不認識的 IAM user、或時間點跟任何已知事件對不上，可能是意外。&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 cloudtrail lookup-events &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --lookup-attributes &lt;span class="nv">AttributeKey&lt;/span>&lt;span class="o">=&lt;/span>ResourceName,AttributeValue&lt;span class="o">=&lt;/span>sg-0abc123 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --start-time 2026-01-01 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;Events[].[EventTime,Username,EventName]&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --output table&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="每條-drift-的處理決策">每條 drift 的處理決策&lt;/h2>
&lt;p>每條 plan 差異都需要一個明確的決定：保留手動改動（更新 HCL）、回退到 code 的版本（apply）、還是暫時擱置（不動）。&lt;/p></description><content:encoded><![CDATA[<p><code>terraform plan</code> 跑完後如果出現非零差異，每一行差異都需要判斷：這是該保留的手動改動，還是該回退的意外漂移。這些差異就是 drift — state 記錄的狀態跟雲端實際狀態之間的落差。判斷錯誤的代價從「設定被覆蓋」到「stateful 資源被重建導致資料遺失」不等，所以分類要在 apply 之前完成。半套 IaC 環境的 drift 通常比全 IaC 環境更多，因為有人在 Console 改了 state 不知道的資源。</p>
<h2 id="讀-plan-輸出三種變更類型">讀 plan 輸出：三種變更類型</h2>
<p><code>terraform plan</code> 的輸出用符號標示每個資源的預期變更。三種類型的風險等級不同，處理方式也不同：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl"># in-place update（~）：修改屬性，資源本身不動
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">~ resource &#34;aws_security_group_rule&#34; &#34;api_ingress&#34; {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    ~ cidr_blocks = [&#34;10.0.0.0/16&#34;] -&gt; [&#34;10.0.1.0/24&#34;]
</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></span><span class="line"><span class="ln"> 6</span><span class="cl"># forces replacement（-/+）：刪除後重建，新資源取得新 ID
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">-/+ resource &#34;aws_db_instance&#34; &#34;primary&#34; {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    ~ identifier = &#34;app-prod&#34; -&gt; &#34;app-prod-v2&#34; # forces replacement
</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><span class="line"><span class="ln">11</span><span class="cl"># destroy（-）：刪除資源
</span></span><span class="line"><span class="ln">12</span><span class="cl">- resource &#34;aws_security_group&#34; &#34;legacy_api&#34; {
</span></span><span class="line"><span class="ln">13</span><span class="cl">  }</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>符號</th>
          <th>意義</th>
          <th>風險等級</th>
          <th>處理原則</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>~</code></td>
          <td>in-place update</td>
          <td>中</td>
          <td>逐項判斷，多數可安全 apply</td>
      </tr>
      <tr>
          <td><code>-/+</code></td>
          <td>forces replacement</td>
          <td>高</td>
          <td>stateful 資源絕對不能直接 apply</td>
      </tr>
      <tr>
          <td><code>-</code></td>
          <td>destroy</td>
          <td>極高</td>
          <td>代表雲端有但 code 沒有，apply 會刪除</td>
      </tr>
  </tbody>
</table>
<p><code>-</code>（destroy）是最危險的類型。它代表某個資源存在於雲端但不在 Terraform code 裡——可能是手動建的、可能是從 state 被 <code>state rm</code> 移除過、也可能是前任維護者刪了 code 但沒跑 apply。不論原因，直接 apply 會把這個資源從雲端刪除。</p>
<p><code>-/+</code>（forces replacement）的危險在於它看起來像修改但實際是先刪後建。對 stateless 資源（security group rule、IAM policy）影響有限，對 stateful 資源（RDS、EBS volume）意味著資料遺失。</p>
<h2 id="故意的-drift-vs-意外的-drift">故意的 drift vs 意外的 drift</h2>
<p>不是所有 drift 都是問題。接手的環境裡，手動改動可能有兩種來源：</p>
<p><strong>故意的改動</strong>是前任維護者為了解決特定問題而做的。常見形態：臨時開了一條 security group 規則讓外部監控系統連進來、調高了 RDS 的 <code>max_connections</code> 參數來應對流量成長、手動把 instance type 從 <code>t3.small</code> 升到 <code>t3.medium</code> 因為記憶體不夠。這類改動通常是正確的操作決策，只是沒有同步回 code。</p>
<p><strong>意外的漂移</strong>是無意中造成的。常見形態：在 Console 測試時改了某個設定但忘了改回來、另一個 Terraform workspace 的 apply 動到了共用的資源、AWS 自動更新了某些屬性（如 default security group 的描述）。</p>
<p>區分兩者的方法是查 CloudTrail——看這個改動是誰做的、什麼時候、有沒有對應的 ticket 或 changelog 記錄。如果 CloudTrail 顯示改動發生在一次事故期間、由當時的值班工程師執行，大概率是故意的。如果改動來自一個不認識的 IAM user、或時間點跟任何已知事件對不上，可能是意外。</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 cloudtrail lookup-events <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --lookup-attributes <span class="nv">AttributeKey</span><span class="o">=</span>ResourceName,AttributeValue<span class="o">=</span>sg-0abc123 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --start-time 2026-01-01 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;Events[].[EventTime,Username,EventName]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --output table</span></span></code></pre></div><h2 id="每條-drift-的處理決策">每條 drift 的處理決策</h2>
<p>每條 plan 差異都需要一個明確的決定：保留手動改動（更新 HCL）、回退到 code 的版本（apply）、還是暫時擱置（不動）。</p>
<h3 id="保留adopt-into-hcl">保留（adopt into HCL）</h3>
<p>適用條件：手動改動是正確的操作決策，雲端的現況是期望狀態。處理方式是把 HCL 改成跟雲端一致，讓下次 plan 對這項顯示零差異。</p>
<p>多數 drift 應該走這條路。前任維護者調大了 instance type、加了一條 security group 規則、改了 RDS parameter——這些改動通常有操作上的理由。把 code 對齊現實，比把現實改回 code 安全。</p>
<h3 id="回退apply-to-revert">回退（apply to revert）</h3>
<p>適用條件：手動改動是錯誤的、或已經不再需要（如臨時開的除錯 port）。確認回退不會影響運行中的服務後，讓 Terraform apply 把設定改回 code 描述的版本。</p>
<p>回退前要確認的事：這條規則還有沒有服務在用？這個參數改回去會不會讓連線斷開？如果不確定，先 adopt 再說——adopt 的成本是改一行 HCL，回退錯誤的成本可能是服務中斷。</p>
<h3 id="擱置defer">擱置（defer）</h3>
<p>適用條件：目前無法判斷該保留還是回退（缺乏 context），或改動涉及 stateful 資源的 forces replacement 需要更多準備。擱置的做法是在 code 裡加 <code>lifecycle { ignore_changes = [...] }</code> 暫時跳過這項差異，並留下註解說明為什麼擱置、預計什麼時候處理。</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_db_instance&#34; &#34;primary&#34;</span> {<span class="c1">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">  # drift: identifier 被手動改過，forces replacement
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">  # 擱置原因：直接 apply 會觸發 RDS 重建、資料遺失
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">  # 預計處理：確認新 identifier 後更新 HCL + 用 moved block
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="k">lifecycle</span> {
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">    ignore_changes</span> <span class="o">=</span> <span class="p">[</span><span class="k">identifier</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  }
</span></span><span class="line"><span class="ln">8</span><span class="cl">}</span></span></code></pre></div><p>擱置不是永久解法。<code>ignore_changes</code> 會讓這個屬性脫離 IaC 管理，累積越多就越接近「回到手動」。定期回顧擱置清單，逐項決定保留或回退。</p>
<h2 id="stateful-資源的高風險-drift">Stateful 資源的高風險 drift</h2>
<p>stateful 資源（RDS、EBS volume、DynamoDB table）的 drift 需要特別處理，因為 forces replacement 意味著資料遺失。以下屬性的改動在 plan 裡會顯示 <code>-/+</code>（forces replacement），直接 apply 會先刪除再重建：</p>
<table>
  <thead>
      <tr>
          <th>資源類型</th>
          <th>觸發 replacement 的屬性</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RDS</td>
          <td><code>identifier</code>、<code>engine</code>、某些 <code>storage_type</code> 變更</td>
          <td>資料庫被刪除重建，資料遺失</td>
      </tr>
      <tr>
          <td>EBS volume</td>
          <td><code>availability_zone</code>、<code>size</code>（縮小）</td>
          <td>volume 被刪除重建，資料遺失</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td><code>hash_key</code>、<code>range_key</code></td>
          <td>table 被刪除重建，資料遺失</td>
      </tr>
  </tbody>
</table>
<p>發現 stateful 資源的 forces replacement 時，處理步驟：</p>
<ol>
<li>在 <code>lifecycle</code> 加 <code>ignore_changes</code> 暫時跳過</li>
<li>備份資源（RDS snapshot、EBS snapshot）</li>
<li>確認正確的目標狀態後，用 <code>moved</code> block 或 <code>terraform state mv</code> 處理 identity 變更</li>
<li>用 <code>terraform plan</code> 驗證變更類型從 <code>-/+</code> 變成 <code>~</code>（in-place）或零差異</li>
<li>移除 <code>ignore_changes</code></li>
</ol>
<h2 id="refresh-only安全的-state-同步">refresh-only：安全的 state 同步</h2>
<p><code>terraform apply -refresh-only</code> 只更新 state 來反映雲端現況，不改變任何雲端資源。它適用於「雲端被手動改了、想讓 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 apply -refresh-only</span></span></code></pre></div><p>refresh-only 之後，state 跟雲端一致了，但 state 跟 HCL 之間的差異仍然存在——下次跑 plan 仍會看到 drift。它解的是「state 過時」的問題，不是「code 跟現實不一致」的問題。兩者要分開處理：先 refresh-only 讓 state 乾淨，再逐項決定 HCL 要不要對齊。</p>
<p>使用 refresh-only 的前提是確認 state backend 有 versioning——如果 refresh-only 把 state 改壞了（例如併發操作導致 state 衝突），需要能回捲到上一個版本。</p>
<h2 id="批次-drift-收斂工作流">批次 drift 收斂工作流</h2>
<p>接手環境的 drift 通常不是一兩條，可能有幾十條。逐條處理可以但效率低，按類型批次處理比較實際：</p>
<p><strong>第一批：安全類</strong>。security group 規則、IAM policy 的 drift 優先處理，因為它們直接影響存取邊界。全開的規則該關就關（回退），故意開的規則 adopt 進 code。</p>
<p><strong>第二批：stateless 資源的 in-place drift</strong>。tag 不一致、description 不一致、非關鍵屬性的變更。這類 drift 風險低，可以批次 adopt（把 HCL 改成跟雲端一致）然後一次 apply 驗證。</p>
<p><strong>第三批：stateful 資源</strong>。RDS parameter、backup retention、instance class 的變更。逐個處理，每個都要確認是 in-place update 而非 forces replacement。</p>
<p><strong>第四批：擱置項</strong>。forces replacement、無法判斷的改動。加 <code>ignore_changes</code> 暫緩，排進 backlog 定期回顧。</p>
<p>每一批處理完後跑一次 plan，確認該批的 drift 消失、其他批次的 drift 沒被影響。不要一次 apply 所有批次——分批的目的是控制每次 apply 的影響範圍。</p>
<p>整個 drift 收斂流程的時程取決於 drift 數量和 stateful 資源的比例。20 條以內的 drift、多數是 stateless 的 in-place 變更，2-3 天可以收完。50 條以上、含多個 stateful 資源的 forces replacement，需要 1-2 週分階段處理。</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-state-repair/" data-link-title="State 修復與清理" data-link-desc="接手的 Terraform state 損壞、有 orphaned entry、或需要搬遷時，怎麼診斷問題、安全操作、以及從錯誤中回復">State 修復與清理</a>：drift 處理前先確認 state 本身是健康的</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 要點、批次策略與常見失敗處理">Unmanaged resource 批次 import</a>：drift 收斂完成後，開始 import unmanaged resource</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>：drift 收斂後的環境拆分路徑</li>
</ul>
]]></content:encoded></item><item><title>Infrastructure Drift</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/</guid><description>&lt;p>Infrastructure Drift 的核心概念是「真實環境狀態與宣告檔分叉」。它會削弱 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection&lt;/a> 與 deployment review 的可信度，並影響下一次 plan / apply 的安全性。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Infrastructure Drift 位在 IaC state、cloud resource、手動 hotfix 與外部 controller 之間，常由 console edit、事故修復、provider 預設值或自動調整造成。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>plan 顯示大量非預期變更。&lt;/li>
&lt;li>production 資源和 repository 宣告不一致。&lt;/li>
&lt;li>下次 apply 可能覆蓋事故 hotfix。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>事故中工程師在雲端 console 手動放寬 security group。服務恢復後，IaC plan 顯示 security group 與宣告檔不同；團隊需要判斷這個變更是短期 hotfix 還是應回寫成正式規則。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Infrastructure Drift 要定義偵測頻率、owner、修復路由、state repair 與回寫規則，讓平台狀態重新回到可審查流程。&lt;/p></description><content:encoded><![CDATA[<p>Infrastructure Drift 的核心概念是「真實環境狀態與宣告檔分叉」。它會削弱 <a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a> 與 deployment review 的可信度，並影響下一次 plan / apply 的安全性。</p>
<h2 id="概念位置">概念位置</h2>
<p>Infrastructure Drift 位在 IaC state、cloud resource、手動 hotfix 與外部 controller 之間，常由 console edit、事故修復、provider 預設值或自動調整造成。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>plan 顯示大量非預期變更。</li>
<li>production 資源和 repository 宣告不一致。</li>
<li>下次 apply 可能覆蓋事故 hotfix。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>事故中工程師在雲端 console 手動放寬 security group。服務恢復後，IaC plan 顯示 security group 與宣告檔不同；團隊需要判斷這個變更是短期 hotfix 還是應回寫成正式規則。</p>
<h2 id="設計責任">設計責任</h2>
<p>Infrastructure Drift 要定義偵測頻率、owner、修復路由、state repair 與回寫規則，讓平台狀態重新回到可審查流程。</p>
]]></content:encoded></item></channel></rss>