<?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>Transition on Tarragon</title><link>https://tarrragon.github.io/blog/tags/transition/</link><description>Recent content in Transition 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/transition/index.xml" rel="self" type="application/rss+xml"/><item><title>兩套真相並存的過渡期操作</title><link>https://tarrragon.github.io/blog/infra/takeover/partial-iac-dual-truth-operation/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/takeover/partial-iac-dual-truth-operation/</guid><description>&lt;p>部分資源由 Terraform 管理、部分仍在手動操作的環境，比全手動更危險。全手動時每個人都知道要去 Console 操作，行為模式一致；半套 IaC 時同一個環境有兩套操作路徑，每一次操作都要先判斷「這個資源歸哪套管」，判斷錯了的後果是 apply 覆蓋手動設定、或手動改動讓 state 與現實分歧。這篇處理的是怎麼在這個過渡期安全操作，以及怎麼盡快離開這個狀態。&lt;/p>
&lt;h2 id="為什麼半套比全手動更危險">為什麼半套比全手動更危險&lt;/h2>
&lt;p>兩個方向的風險同時存在，而且互相放大。&lt;/p>
&lt;h3 id="apply-可能摧毀未納管的資源">apply 可能摧毀未納管的資源&lt;/h3>
&lt;p>Terraform apply 只知道 state 裡有什麼。一個存在於雲端但不在 state 裡的資源，對 Terraform 來說「不存在」。如果某個 managed resource 引用了一個 unmanaged resource 的 ID（例如一個 security group 引用了一個手動建的 security group 作為 source），apply 不會主動碰那個 unmanaged resource——但如果有人重構了 HCL 並把那個引用移除或改掉，apply 會改動 managed 的那一端，可能讓依賴它的 unmanaged 資源失去連線。&lt;/p>
&lt;p>更直接的風險是 &lt;code>terraform destroy&lt;/code> 或 &lt;code>terraform apply&lt;/code> 配合 &lt;code>count = 0&lt;/code> 這類邏輯刪除：如果有人誤判某個資源已經不用了、但它其實只是不在 state 裡（被前人 &lt;code>state rm&lt;/code> 過），destroy 不會碰它——但如果有人重新 import 它再 destroy，資源就真的被刪了。&lt;/p>
&lt;h3 id="手動改動讓-managed-資源-drift">手動改動讓 managed 資源 drift&lt;/h3>
&lt;p>有人在 Console 手動改了一個已經由 Terraform 管理的資源（例如加了一條 security group 規則），state 不知道這個改動。下一次任何人跑 apply，Terraform 會把手動加的規則判定為「不該存在」並刪除。手動改動的人以為規則已經加好了，直到某次不相關的 apply 把它默默清掉。&lt;/p>
&lt;p>這兩個風險的交叉效應是：團隊對「能不能跑 apply」和「能不能手動改」都缺乏信心，結果是兩邊都不敢動，變更停滯，技術債累積速度比全手動還快。&lt;/p>
&lt;h2 id="過渡期操作規則">過渡期操作規則&lt;/h2>
&lt;p>過渡期的操作紀律核心是一句話：&lt;strong>每個資源在任何時刻都只有一個合法的變更路徑&lt;/strong>。managed 資源走 IaC，unmanaged 資源走 Console + 變更日誌。混用就是 drift 的來源。&lt;/p>
&lt;h3 id="規則一apply-前必讀-plan">規則一：apply 前必讀 plan&lt;/h3>
&lt;p>過渡期的每一次 &lt;code>terraform apply&lt;/code> 之前，都要完整讀 &lt;code>terraform plan&lt;/code> 的輸出，逐行確認每一項變更是預期內的。特別警惕以下訊號：&lt;/p>
&lt;ul>
&lt;li>&lt;code>will be destroyed&lt;/code>：確認這個資源是否有其他依賴（即使它在 state 裡）&lt;/li>
&lt;li>&lt;code>will be updated in-place&lt;/code> 且變更的屬性不是這次修改的：代表有人手動改了這個屬性，apply 會覆蓋回去&lt;/li>
&lt;li>&lt;code>must be replaced&lt;/code>：資源會被先刪後建，stateful 資源（RDS、EBS）在這裡要暫停確認&lt;/li>
&lt;/ul>
&lt;p>過渡期禁止 &lt;code>terraform apply -auto-approve&lt;/code>。即使 CI pipeline 也要把 apply 設為手動觸發（GitHub Actions 的 environment protection rule），確保有人看過 plan。&lt;/p>
&lt;h3 id="規則二不手動改-managed-資源">規則二：不手動改 managed 資源&lt;/h3>
&lt;p>一個資源一旦進了 Terraform state，所有對它的變更都走 HCL → plan → apply。在 Console 改它會製造 drift，而 drift 在過渡期特別危險——因為下一次 apply 可能已經隔了好幾天，中間的手動改動已經忘了。&lt;/p>
&lt;p>如果遇到緊急情況必須手動改 managed 資源（例如安全事件需要立即封鎖某個 port），操作流程是：&lt;/p>
&lt;ol>
&lt;li>在 Console 做緊急變更&lt;/li>
&lt;li>立刻在變更日誌記錄：時間、資源、改了什麼、為什麼&lt;/li>
&lt;li>在 HCL 裡同步這個變更，提 PR&lt;/li>
&lt;li>PR 裡的 plan 應該顯示零變更（因為 HCL 已經對齊了手動改動）&lt;/li>
&lt;li>合併 PR，state 透過下一次 apply 或 refresh 更新&lt;/li>
&lt;/ol>
&lt;h3 id="規則三記錄哪些資源歸誰管">規則三：記錄哪些資源歸誰管&lt;/h3>
&lt;p>維護一份「管理歸屬清單」——哪些資源在 Terraform state 裡、哪些還在手動管理。格式可以是 repo 裡的一個 markdown 表格：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="gu">## 資源管理歸屬
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">| 資源類型 | 資源名稱/ID | 管理方式 | 備註 |
&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">| VPC | vpc-0abc123 | Terraform | |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">| Subnet (×4) | subnet-0def... | Terraform | |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">| RDS | app-prod-primary | Terraform | stateful、謹慎操作 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">| SG web | sg-0web456 | Terraform | |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">| SG legacy-api | sg-0legacy789 | 手動 | 待 import |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">| EC2 worker | i-0worker123 | 手動 | 待 import |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">| Lambda cron | cleanup-job | 手動 | 待評估是否納管 |&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這份清單的維護者是跑 apply 的人——每次 import 一個新資源後更新清單。清單同時是 team communication 的基礎：team member 要改某個資源前，先查清單確認管理方式。&lt;/p></description><content:encoded><![CDATA[<p>部分資源由 Terraform 管理、部分仍在手動操作的環境，比全手動更危險。全手動時每個人都知道要去 Console 操作，行為模式一致；半套 IaC 時同一個環境有兩套操作路徑，每一次操作都要先判斷「這個資源歸哪套管」，判斷錯了的後果是 apply 覆蓋手動設定、或手動改動讓 state 與現實分歧。這篇處理的是怎麼在這個過渡期安全操作，以及怎麼盡快離開這個狀態。</p>
<h2 id="為什麼半套比全手動更危險">為什麼半套比全手動更危險</h2>
<p>兩個方向的風險同時存在，而且互相放大。</p>
<h3 id="apply-可能摧毀未納管的資源">apply 可能摧毀未納管的資源</h3>
<p>Terraform apply 只知道 state 裡有什麼。一個存在於雲端但不在 state 裡的資源，對 Terraform 來說「不存在」。如果某個 managed resource 引用了一個 unmanaged resource 的 ID（例如一個 security group 引用了一個手動建的 security group 作為 source），apply 不會主動碰那個 unmanaged resource——但如果有人重構了 HCL 並把那個引用移除或改掉，apply 會改動 managed 的那一端，可能讓依賴它的 unmanaged 資源失去連線。</p>
<p>更直接的風險是 <code>terraform destroy</code> 或 <code>terraform apply</code> 配合 <code>count = 0</code> 這類邏輯刪除：如果有人誤判某個資源已經不用了、但它其實只是不在 state 裡（被前人 <code>state rm</code> 過），destroy 不會碰它——但如果有人重新 import 它再 destroy，資源就真的被刪了。</p>
<h3 id="手動改動讓-managed-資源-drift">手動改動讓 managed 資源 drift</h3>
<p>有人在 Console 手動改了一個已經由 Terraform 管理的資源（例如加了一條 security group 規則），state 不知道這個改動。下一次任何人跑 apply，Terraform 會把手動加的規則判定為「不該存在」並刪除。手動改動的人以為規則已經加好了，直到某次不相關的 apply 把它默默清掉。</p>
<p>這兩個風險的交叉效應是：團隊對「能不能跑 apply」和「能不能手動改」都缺乏信心，結果是兩邊都不敢動，變更停滯，技術債累積速度比全手動還快。</p>
<h2 id="過渡期操作規則">過渡期操作規則</h2>
<p>過渡期的操作紀律核心是一句話：<strong>每個資源在任何時刻都只有一個合法的變更路徑</strong>。managed 資源走 IaC，unmanaged 資源走 Console + 變更日誌。混用就是 drift 的來源。</p>
<h3 id="規則一apply-前必讀-plan">規則一：apply 前必讀 plan</h3>
<p>過渡期的每一次 <code>terraform apply</code> 之前，都要完整讀 <code>terraform plan</code> 的輸出，逐行確認每一項變更是預期內的。特別警惕以下訊號：</p>
<ul>
<li><code>will be destroyed</code>：確認這個資源是否有其他依賴（即使它在 state 裡）</li>
<li><code>will be updated in-place</code> 且變更的屬性不是這次修改的：代表有人手動改了這個屬性，apply 會覆蓋回去</li>
<li><code>must be replaced</code>：資源會被先刪後建，stateful 資源（RDS、EBS）在這裡要暫停確認</li>
</ul>
<p>過渡期禁止 <code>terraform apply -auto-approve</code>。即使 CI pipeline 也要把 apply 設為手動觸發（GitHub Actions 的 environment protection rule），確保有人看過 plan。</p>
<h3 id="規則二不手動改-managed-資源">規則二：不手動改 managed 資源</h3>
<p>一個資源一旦進了 Terraform state，所有對它的變更都走 HCL → plan → apply。在 Console 改它會製造 drift，而 drift 在過渡期特別危險——因為下一次 apply 可能已經隔了好幾天，中間的手動改動已經忘了。</p>
<p>如果遇到緊急情況必須手動改 managed 資源（例如安全事件需要立即封鎖某個 port），操作流程是：</p>
<ol>
<li>在 Console 做緊急變更</li>
<li>立刻在變更日誌記錄：時間、資源、改了什麼、為什麼</li>
<li>在 HCL 裡同步這個變更，提 PR</li>
<li>PR 裡的 plan 應該顯示零變更（因為 HCL 已經對齊了手動改動）</li>
<li>合併 PR，state 透過下一次 apply 或 refresh 更新</li>
</ol>
<h3 id="規則三記錄哪些資源歸誰管">規則三：記錄哪些資源歸誰管</h3>
<p>維護一份「管理歸屬清單」——哪些資源在 Terraform state 裡、哪些還在手動管理。格式可以是 repo 裡的一個 markdown 表格：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="gu">## 資源管理歸屬
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">| 資源類型       | 資源名稱/ID         | 管理方式   | 備註             |
</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">| VPC            | vpc-0abc123          | Terraform  |                  |
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">| Subnet (×4)   | subnet-0def...       | Terraform  |                  |
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">| RDS            | app-prod-primary     | Terraform  | stateful、謹慎操作 |
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">| SG web         | sg-0web456           | Terraform  |                  |
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">| SG legacy-api  | sg-0legacy789        | 手動       | 待 import        |
</span></span><span class="line"><span class="ln">10</span><span class="cl">| EC2 worker     | i-0worker123         | 手動       | 待 import        |
</span></span><span class="line"><span class="ln">11</span><span class="cl">| Lambda cron    | cleanup-job          | 手動       | 待評估是否納管   |</span></span></code></pre></div><p>這份清單的維護者是跑 apply 的人——每次 import 一個新資源後更新清單。清單同時是 team communication 的基礎：team member 要改某個資源前，先查清單確認管理方式。</p>
<h2 id="團隊溝通">團隊溝通</h2>
<p>過渡期最重要的溝通是讓所有會碰 Console 的人知道哪些資源「不能手動改」。溝通的形式是直接的操作指令：</p>
<p>在 team channel 發一則釘選訊息：</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">[Infra 過渡期操作規則]
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">以下資源已由 Terraform 管理，變更請走 PR：
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">- VPC 和所有 subnet
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">- Security group: sg-0web456, sg-0app789
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">- RDS: app-prod-primary
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">- ALB: app-prod-alb
</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">以下資源仍為手動管理，變更請在 Console 操作後寫 changelog：
</span></span><span class="line"><span class="ln">10</span><span class="cl">- EC2: i-0worker123
</span></span><span class="line"><span class="ln">11</span><span class="cl">- Lambda: cleanup-job
</span></span><span class="line"><span class="ln">12</span><span class="cl">- SG: sg-0legacy789
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">不確定的資源：先問再動。</span></span></code></pre></div><p>隨著 import 進展更新這則訊息。如果團隊用的是 Slack，可以把這則訊息設成 channel bookmark。</p>
<h2 id="縮短過渡期">縮短過渡期</h2>
<p>過渡期越長、兩套真相並存越久、操作事故的機率越高。縮短的方式是用 import sprint 集中處理。</p>
<h3 id="import-sprint-的排程">Import sprint 的排程</h3>
<p>一個 import sprint 是 1-2 天的集中工作，目標是把一批相關的 unmanaged 資源納入 Terraform。按風險從低到高排序：</p>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>資源類型</th>
          <th>理由</th>
          <th>預估時間</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>SG、IAM role/policy</td>
          <td>高頻變更、drift 風險最高</td>
          <td>半天到一天</td>
      </tr>
      <tr>
          <td>2</td>
          <td>S3 bucket、CloudWatch</td>
          <td>stateless、import 風險低</td>
          <td>半天</td>
      </tr>
      <tr>
          <td>3</td>
          <td>EC2 instance、ECS</td>
          <td>中風險、需確認 user data 和 AMI</td>
          <td>一天</td>
      </tr>
      <tr>
          <td>4</td>
          <td>RDS、EBS</td>
          <td>stateful、import 失敗代價最高、最後做</td>
          <td>一天（含驗證）</td>
      </tr>
  </tbody>
</table>
<p>每批的操作流程：</p>
<ol>
<li>用 <code>import</code> block + <code>terraform plan -generate-config-out</code> 產生 HCL</li>
<li>審查生成的 HCL，修正屬性差異</li>
<li><code>plan</code> 確認零變更</li>
<li>合併 PR</li>
<li>更新管理歸屬清單</li>
</ol>
<h3 id="縮短期間不要追求完美">縮短期間不要追求完美</h3>
<p>import sprint 的目標是「納管」，不是「重構」。一個手動建的資源 import 進來後，它的 HCL 可能很醜（自動生成的 code 有大量冗餘屬性），但只要 plan 顯示零變更，它就已經是 managed 的了。重構 HCL 是 import 完成之後的事。</p>
<p>同樣，import sprint 期間不要同時做 module 化或環境分離。先把所有資源納管到同一份 state，之後再拆——拆的前提是所有資源都在 state 裡。</p>
<h2 id="過渡期結束的判準">過渡期結束的判準</h2>
<p>過渡期結束的定義是兩個條件同時滿足：</p>
<ol>
<li><strong><code>terraform plan</code> 在無 code 變更時顯示零差異</strong>：代表 state 與雲端現實一致，沒有 drift</li>
<li><strong>管理歸屬清單上的「手動」欄位清空</strong>：所有生產資源都進了 Terraform state</li>
</ol>
<p>第一個條件用定期排程驗證（每天跑一次 plan，非零就告警）。第二個條件用資源盤點比對——雲端的 resource inventory 減去 <code>terraform state list</code> 的輸出，差集為空就完成。</p>
<p>過渡期結束後，操作規則簡化為：所有變更走 IaC + PR，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/takeover/partial-iac-no-docs/" data-link-title="有半套 IaC 但文件缺失的環境接管" data-link-desc="IaC 覆蓋不完整、部分資源在 state 外、文件缺失的環境怎麼盤點差距、修復 state 健康、收斂 drift 並重建文件">有半套 IaC 但文件缺失的環境接管</a>：本篇的前置操作（盤點、state 健康檢查、drift 收斂）</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 surgery</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>：過渡期結束後的操作紀律</li>
<li>→ <a href="/blog/infra/04-environment-separation/single-to-multi-env-retrofit/" data-link-title="單環境到多環境的 Retrofit 操作手冊" data-link-desc="把已經跑在單一環境的 Terraform 設定拆成 module &#43; per-env 目錄結構的完整操作步驟，含 moved block、zero-change plan 驗證與常見陷阱">模組四：環境分離 retrofit</a>：所有資源納管後的下一步</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/plan-review-apply-guardrails/" data-link-title="infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：過渡期結束後的完整 PR 護欄</li>
</ul>
]]></content:encoded></item></channel></rss>