<?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>模組八：治理好習慣 — 規模長大後不失控的最小節奏 on Tarragon</title><link>https://tarrragon.github.io/blog/infra/08-governance-habits/</link><description>Recent content in 模組八：治理好習慣 — 規模長大後不失控的最小節奏 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/infra/08-governance-habits/index.xml" rel="self" type="application/rss+xml"/><item><title>Tagging 規範與 Secrets 不進 code</title><link>https://tarrragon.github.io/blog/infra/08-governance-habits/tagging-secrets/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/08-governance-habits/tagging-secrets/</guid><description>&lt;p>每一個治理習慣單獨看都很小：在資源上多打三個 tag、把一段連線字串挪去別的地方。但少了這些習慣，半年後的代價是另一個量級 — 翻著一頁兩百筆沒有歸屬的資源猜哪個能砍、為了輪替一把外洩的密鑰回頭 grep 整個 repo。Tagging 與 secret 管理是治理習慣裡補救成本最陡的兩項：tag 一旦缺席就得回頭考古幾百個資源，密鑰一旦進了 git 歷史就無法清除。它們共同的特性是 day-1 建立的成本接近零，事後補的代價隨資源數量與時間複利。&lt;/p>
&lt;h2 id="tagging-規範查帳與清資源的依據">Tagging 規範：查帳與清資源的依據&lt;/h2>
&lt;p>Tag 是貼在每個資源上的結構化標籤，承擔「讓資源能被機器查詢與分群」的責任。沒有 tag 的資源在 console 裡只剩一個隨機後綴的名字，人能勉強認得幾個，但一旦數量過百，任何「列出所有 staging 的資源」「算出 team-a 這個月花多少」的問題都無法用查詢回答，只能逐筆翻。Tag 把這些問題從人工考古變成一行 filter。&lt;/p>
&lt;h3 id="最小-tag-集合">最小 tag 集合&lt;/h3>
&lt;p>值得從第一天就強制的最小 tag 集合是三個維度，各自回答一個治理問題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Tag&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>env&lt;/code>&lt;/td>
 &lt;td>這是哪個環境&lt;/td>
 &lt;td>&lt;code>prod&lt;/code> / &lt;code>staging&lt;/code> / &lt;code>dev&lt;/code>&lt;/td>
 &lt;td>清資源時不敢動、怕誤刪生產&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>owner&lt;/code>&lt;/td>
 &lt;td>出事找誰&lt;/td>
 &lt;td>&lt;code>team-payments&lt;/code> / &lt;code>platform&lt;/code>&lt;/td>
 &lt;td>資源孤兒化、沒人認領也沒人敢回收&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>cost-center&lt;/code>&lt;/td>
 &lt;td>這筆錢算誰的&lt;/td>
 &lt;td>&lt;code>cc-1024&lt;/code> / &lt;code>growth&lt;/code>&lt;/td>
 &lt;td>帳單無法拆分、成本變成一筆沒人負責的公共支出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>env&lt;/code> 是清資源時的安全護欄。回收動作最大的恐懼是誤刪生產資源。當每個資源都標了 &lt;code>env&lt;/code>，「列出所有 &lt;code>env=dev&lt;/code> 且 30 天無流量的資源」就是一條可以放心執行的清理查詢，而 &lt;code>env=prod&lt;/code> 的資源自動被排除在批次刪除之外。沒有這個 tag，任何自動化清理都因為怕誤傷而不敢落地，最後退回人工逐筆確認，於是根本沒人去清。&lt;/p>
&lt;p>&lt;code>owner&lt;/code> 解決資源孤兒化。服務出狀況、或是看到一個用途不明的資源時，第一個問題是「這誰的」。標了 owner，告警可以自動路由、清理前可以自動通知認領；沒標，這個資源就停在「沒人敢動、因為不知道砍了會不會弄壞什麼」的狀態，永久占用配額與費用。團隊命名比個人名好 — 人會離職，團隊邊界相對穩定。&lt;/p>
&lt;p>&lt;code>cost-center&lt;/code> 是成本歸屬的地基，把帳單從「一筆公共支出」拆成「每個團隊各自負責的花費」。這個維度的後續應用在&lt;a href="https://tarrragon.github.io/blog/infra/08-governance-habits/cost-visibility-rhythm/" data-link-title="成本可見性與最小可行治理節奏" data-link-desc="用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加">成本可見性與最小可行治理節奏&lt;/a>展開。&lt;/p>
&lt;h3 id="附加-tag-的合理時機">附加 tag 的合理時機&lt;/h3>
&lt;p>三個必填之外，隨著團隊規模增長，幾個常見的附加維度會自然浮現：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Tag&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>加入時機&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>managed-by&lt;/code>&lt;/td>
 &lt;td>區分 IaC 管理 vs 手動建立&lt;/td>
 &lt;td>導入 IaC 第一天就加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>project&lt;/code>&lt;/td>
 &lt;td>區分同一團隊下不同產品線&lt;/td>
 &lt;td>團隊負責超過一個產品時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>ttl&lt;/code>&lt;/td>
 &lt;td>資源預定存活時間（如 &lt;code>7d&lt;/code>）&lt;/td>
 &lt;td>開始有大量開發 / 測試用臨時資源時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>compliance&lt;/code>&lt;/td>
 &lt;td>標記受法規約束的資源（如 &lt;code>pci&lt;/code> / &lt;code>hipaa&lt;/code>）&lt;/td>
 &lt;td>開始有合規稽核需求時&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>managed-by = terraform&lt;/code> 搭配 &lt;code>env&lt;/code>，可以快速找出「不在 IaC 管理下的生產資源」 — 這些就是 Console 唯讀紀律（&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>）鬆動的痕跡。附加 tag 不需要一次規劃完，但一旦加入就要跟必填 tag 一起走自動護欄。&lt;/p>
&lt;h3 id="用-iac-自動標記">用 IaC 自動標記&lt;/h3>
&lt;p>Tag 必須在資源建立時就由 IaC 寫進去，而不是事後補。Terraform 的 &lt;code>default_tags&lt;/code> 讓一個 provider 區塊內的所有資源自動繼承一組 tag，避免逐個資源手動標、也避免漏標：&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">provider&lt;/span> &lt;span class="s2">&amp;#34;aws&amp;#34;&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"> 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"> 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="k">default_tags&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"> tags&lt;/span> &lt;span class="o">=&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"> env&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">env&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"> owner&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">team&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> cost-center&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cost_center&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> managed-by&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;terraform&amp;#34;&lt;/span>
&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"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用 &lt;code>var&lt;/code> 取代寫死的值，讓同一套 provider 設定跨環境複用 — 每個環境的 &lt;code>terraform.tfvars&lt;/code> 填入自己的值。這和&lt;a href="https://tarrragon.github.io/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化&lt;/a>的參數化設計一致。&lt;/p></description><content:encoded><![CDATA[<p>每一個治理習慣單獨看都很小：在資源上多打三個 tag、把一段連線字串挪去別的地方。但少了這些習慣，半年後的代價是另一個量級 — 翻著一頁兩百筆沒有歸屬的資源猜哪個能砍、為了輪替一把外洩的密鑰回頭 grep 整個 repo。Tagging 與 secret 管理是治理習慣裡補救成本最陡的兩項：tag 一旦缺席就得回頭考古幾百個資源，密鑰一旦進了 git 歷史就無法清除。它們共同的特性是 day-1 建立的成本接近零，事後補的代價隨資源數量與時間複利。</p>
<h2 id="tagging-規範查帳與清資源的依據">Tagging 規範：查帳與清資源的依據</h2>
<p>Tag 是貼在每個資源上的結構化標籤，承擔「讓資源能被機器查詢與分群」的責任。沒有 tag 的資源在 console 裡只剩一個隨機後綴的名字，人能勉強認得幾個，但一旦數量過百，任何「列出所有 staging 的資源」「算出 team-a 這個月花多少」的問題都無法用查詢回答，只能逐筆翻。Tag 把這些問題從人工考古變成一行 filter。</p>
<h3 id="最小-tag-集合">最小 tag 集合</h3>
<p>值得從第一天就強制的最小 tag 集合是三個維度，各自回答一個治理問題：</p>
<table>
  <thead>
      <tr>
          <th>Tag</th>
          <th>回答的問題</th>
          <th>典型值</th>
          <th>缺了會怎樣</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>env</code></td>
          <td>這是哪個環境</td>
          <td><code>prod</code> / <code>staging</code> / <code>dev</code></td>
          <td>清資源時不敢動、怕誤刪生產</td>
      </tr>
      <tr>
          <td><code>owner</code></td>
          <td>出事找誰</td>
          <td><code>team-payments</code> / <code>platform</code></td>
          <td>資源孤兒化、沒人認領也沒人敢回收</td>
      </tr>
      <tr>
          <td><code>cost-center</code></td>
          <td>這筆錢算誰的</td>
          <td><code>cc-1024</code> / <code>growth</code></td>
          <td>帳單無法拆分、成本變成一筆沒人負責的公共支出</td>
      </tr>
  </tbody>
</table>
<p><code>env</code> 是清資源時的安全護欄。回收動作最大的恐懼是誤刪生產資源。當每個資源都標了 <code>env</code>，「列出所有 <code>env=dev</code> 且 30 天無流量的資源」就是一條可以放心執行的清理查詢，而 <code>env=prod</code> 的資源自動被排除在批次刪除之外。沒有這個 tag，任何自動化清理都因為怕誤傷而不敢落地，最後退回人工逐筆確認，於是根本沒人去清。</p>
<p><code>owner</code> 解決資源孤兒化。服務出狀況、或是看到一個用途不明的資源時，第一個問題是「這誰的」。標了 owner，告警可以自動路由、清理前可以自動通知認領；沒標，這個資源就停在「沒人敢動、因為不知道砍了會不會弄壞什麼」的狀態，永久占用配額與費用。團隊命名比個人名好 — 人會離職，團隊邊界相對穩定。</p>
<p><code>cost-center</code> 是成本歸屬的地基，把帳單從「一筆公共支出」拆成「每個團隊各自負責的花費」。這個維度的後續應用在<a href="/blog/infra/08-governance-habits/cost-visibility-rhythm/" data-link-title="成本可見性與最小可行治理節奏" data-link-desc="用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加">成本可見性與最小可行治理節奏</a>展開。</p>
<h3 id="附加-tag-的合理時機">附加 tag 的合理時機</h3>
<p>三個必填之外，隨著團隊規模增長，幾個常見的附加維度會自然浮現：</p>
<table>
  <thead>
      <tr>
          <th>Tag</th>
          <th>用途</th>
          <th>加入時機</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>managed-by</code></td>
          <td>區分 IaC 管理 vs 手動建立</td>
          <td>導入 IaC 第一天就加</td>
      </tr>
      <tr>
          <td><code>project</code></td>
          <td>區分同一團隊下不同產品線</td>
          <td>團隊負責超過一個產品時</td>
      </tr>
      <tr>
          <td><code>ttl</code></td>
          <td>資源預定存活時間（如 <code>7d</code>）</td>
          <td>開始有大量開發 / 測試用臨時資源時</td>
      </tr>
      <tr>
          <td><code>compliance</code></td>
          <td>標記受法規約束的資源（如 <code>pci</code> / <code>hipaa</code>）</td>
          <td>開始有合規稽核需求時</td>
      </tr>
  </tbody>
</table>
<p><code>managed-by = terraform</code> 搭配 <code>env</code>，可以快速找出「不在 IaC 管理下的生產資源」 — 這些就是 Console 唯讀紀律（<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一</a>）鬆動的痕跡。附加 tag 不需要一次規劃完，但一旦加入就要跟必填 tag 一起走自動護欄。</p>
<h3 id="用-iac-自動標記">用 IaC 自動標記</h3>
<p>Tag 必須在資源建立時就由 IaC 寫進去，而不是事後補。Terraform 的 <code>default_tags</code> 讓一個 provider 區塊內的所有資源自動繼承一組 tag，避免逐個資源手動標、也避免漏標：</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">provider</span> <span class="s2">&#34;aws&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</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"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">default_tags</span> {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    tags</span> <span class="o">=</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="k">var</span><span class="p">.</span><span class="k">team</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">      cost-center</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">cost_center</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">      managed-by</span>  <span class="o">=</span> <span class="s2">&#34;terraform&#34;</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></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><p>用 <code>var</code> 取代寫死的值，讓同一套 provider 設定跨環境複用 — 每個環境的 <code>terraform.tfvars</code> 填入自己的值。這和<a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>的參數化設計一致。</p>
<p>個別資源若需要額外 tag（例如 <code>ttl</code>），在資源自身的 <code>tags</code> block 裡寫，它會跟 <code>default_tags</code> 合併，不需要重複寫環境層的三個必填。兩者有相同 key 時資源層優先，所以某個特殊資源要覆蓋 owner 也行。</p>
<p>事後補 tag 是個會無限拖延的工作，因為它不影響任何功能、沒有 deadline、永遠排在 backlog 最後。</p>
<h3 id="tag-合規護欄">Tag 合規護欄</h3>
<p>判讀訊號很簡單：定期跑一條「列出缺少必填 tag 的資源」的查詢，數字若持續成長，代表有人繞過 IaC 手動開資源 — 這既是 tag 問題，也是模組一「Console 唯讀」紀律鬆動的徵兆。</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"># 列出沒有 env tag 的 EC2 instance</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws resourcegroupstaggingapi get-resources <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --resource-type-filters ec2:instance <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --tag-filters <span class="nv">Key</span><span class="o">=</span>env,Values<span class="o">=</span> <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;ResourceTagMappingList[].ResourceARN&#39;</span></span></span></code></pre></div><p>手動查詢只是起點。更可靠的做法是用策略引擎在建立期或 PR 階段就擋住不合規的資源：</p>
<ul>
<li><strong>AWS Tag Policy</strong>（Organizations 層級）：定義必填 tag 與允許值的枚舉，不符合就阻止建立。適合整個組織統一推行。</li>
<li><strong>OPA / Sentinel</strong>（CI / PR 層級）：在 <code>terraform plan</code> 之後、<code>apply</code> 之前檢查 plan 輸出，缺 tag 就讓 CI fail。適合跟<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>整合。</li>
<li><strong>checkov / tfsec 自訂規則</strong>：靜態掃描 HCL，在 code push 時就擋。成本最低但只擋得住 IaC 管理的資源。</li>
</ul>
<p>三層護欄互補：靜態掃描擋寫 code 時的遺漏、plan 檢查擋執行前的偏差、tag policy 擋繞過 IaC 的手動操作。早期只做一層也有價值，三層都做時覆蓋最完整。定期跑 tag 覆蓋率報告（缺少必填 tag 的資源數 / 總資源數）可以作為治理進度的量化指標。覆蓋率從 40% 到 90% 的趨勢比單次數字更有意義，適合放進月報讓管理層追蹤。</p>
<p>Tagging 在合規驅動的基礎設施中還有另一層用途：用 tag 標記資料的地理歸屬，讓合規查詢可以機器化。Hard Rock Digital 的運動博彩平台受美國 Wire Act 約束，不同州的投注資料必須留在州內。它們用 CockroachDB 跨 AWS Outposts 部署，每個 Outpost 的資源用地理 tag 標記歸屬州，合規稽核時用 tag 過濾而非逐台盤查。這個案例的 infra 教訓是：tag 的維度設計在受地理或法規約束的服務中，要提前納入合規需求的維度，而非只做成本和歸屬。詳見 <a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital：Wire Act 合規</a>。</p>
<h2 id="secrets-不進-code機密值的儲存與引用">Secrets 不進 code：機密值的儲存與引用</h2>
<p>機密值 — 資料庫密碼、第三方 API key、簽章用的私鑰 — 要存在專用的密鑰管理服務裡，而 code 與 IaC 只持有指向它的參照，不持有值本身。這條規則承擔的責任是把「機密外洩的爆炸半徑」與「程式碼的散布範圍」脫鉤：一旦密碼寫進 repo，它就跟著每一次 clone、每一份 CI 快取、每一個 fork 擴散，輪替時無法保證所有副本都更新，git 歷史更是會把它永久留存，即使後來刪掉那一行。</p>
<h3 id="密鑰管理服務的選型">密鑰管理服務的選型</h3>
<p>密鑰管理服務提供的是一個有存取控制、有審計紀錄、可輪替的集中儲存。值放在這裡，誰讀過、什麼時候讀的都有 log，輪替時只改一個地方，所有引用方下次讀取就拿到新值。</p>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>定位</th>
          <th>適合的情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS Secrets Manager</td>
          <td>受管 secret、支援自動輪替</td>
          <td>資料庫密碼、需要自動輪替的 key</td>
      </tr>
      <tr>
          <td>AWS SSM Parameter Store</td>
          <td>輕量級 key-value、有免費額度</td>
          <td>設定值、不需要自動輪替的 secret</td>
      </tr>
      <tr>
          <td>HashiCorp Vault</td>
          <td>自管 / 託管、跨雲、動態 secret</td>
          <td>多雲或需要動態產生短期憑證的團隊</td>
      </tr>
      <tr>
          <td>GCP Secret Manager</td>
          <td>GCP 原生受管 secret</td>
          <td>GCP 生態</td>
      </tr>
  </tbody>
</table>
<p>選型看的是團隊已有的生態與輪替需求。對已在 AWS 上的團隊，Secrets Manager 適合需要自動輪替的資料庫密碼，SSM Parameter Store 適合其餘設定值（免費額度通常夠用）。跨雲或對動態 secret 有需求的團隊會走 Vault。</p>
<h3 id="iac-怎麼引用-secret">IaC 怎麼引用 secret</h3>
<p>IaC 應該存的是密鑰的 ARN（或等價的資源識別碼）與「在執行期去讀它」的指令，而不是密鑰的明文：</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">data</span> <span class="s2">&#34;aws_secretsmanager_secret_version&#34; &#34;db&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  secret_id</span> <span class="o">=</span> <span class="s2">&#34;prod/payments/db-password&#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_db_instance&#34; &#34;payments&#34;</span> {
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">  password</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_secretsmanager_secret_version</span><span class="p">.</span><span class="k">db</span><span class="p">.</span><span class="k">secret_string</span><span class="c1">
</span></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"><span class="c1"></span>}</span></span></code></pre></div><p>另一種做法是讓 IaC 只建立 secret 的「容器」（空的 Secrets Manager entry），值由人工或自動化流程在 IaC 之外寫入。這樣 state 裡只有 secret 的 metadata（ARN、名稱、版本 ID），完全不碰明文。適合密碼由安全團隊管理、IaC 只負責「確保 secret 存在且有正確的存取策略」的分工模式。</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_secretsmanager_secret&#34; &#34;db&#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;${var.env}/payments/db-password&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">}<span class="c1">
</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 class="c1"># 值不由 Terraform 管理 — 在 Console 或 CLI 手動設定
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="err">#</span> <span class="k">secret</span> <span class="k">version</span> <span class="k">生命週期在</span> <span class="k">IaC</span> <span class="k">之外</span></span></span></code></pre></div><h3 id="state-裡的機密邊界">state 裡的機密邊界</h3>
<p>Terraform 即使從 Secrets Manager 讀值，那個值仍然會以明文落進 state 檔。這是一個常被忽略的邊界。「不進 code」只是第一道，state 後端的加密與存取控制（<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一的 state 地基</a>）是同等重要的第二道 — 否則密鑰只是從 repo 搬到了一個沒鎖好的 state bucket。</p>
<p>State 的保護措施是一道複合防線：</p>
<ul>
<li>S3 bucket 開 <code>encrypt = true</code>（AES-256 或 KMS）</li>
<li>Bucket 的 IAM policy 只允許跑 <code>apply</code> 的 role 讀寫</li>
<li>Bucket 開 versioning，誤寫或損壞時可以回捲</li>
<li>DynamoDB lock table 防止並行 apply 覆蓋</li>
</ul>
<p>這些措施在<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一</a>的 remote state backend 段已經詳述，這裡提醒的是：state 的安全程度決定了 secret 引用策略的上限。state 沒鎖好時，把 secret 值拉進 state 的做法等於把密碼從 repo 搬到了另一個不設防的地方。</p>
<h3 id="secret-掃描">Secret 掃描</h3>
<p>判讀訊號：定期用 secret 掃描工具掃 repo 與 CI log，任何命中都當成需要輪替的外洩事件處理，而不是刪掉那行就算了 — 因為 git 歷史與既有 clone 已經保不住了。</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"># gitleaks：掃描整個 git 歷史</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gitleaks detect --source . --report-format json --report-path gitleaks-report.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"># trufflehog：掃描 git、filesystem、CI</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">trufflehog git file://. --json</span></span></code></pre></div><p>兩個工具覆蓋面不同（gitleaks 用 regex pattern、trufflehog 用 detector + entropy），搭配用覆蓋更完整。放進 CI pipeline 讓每個 PR 自動掃，比人工定期跑更可靠。命中後的處理流程：先輪替被洩露的 secret，再從 repo 清除（<code>git filter-repo</code>），最後通知所有可能受影響的服務。</p>
<h3 id="secret-命名規範">Secret 命名規範</h3>
<p>機密的命名也值得約定。用 <code>{env}/{service}/{purpose}</code> 這類有結構的路徑（如 <code>prod/payments/db-password</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="c1"># 給 payments service 的 role 只能讀自己的 secret
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;payments_secrets&#34;</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">    actions</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;secretsmanager:GetSecretValue&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">    resources</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;arn:aws:secretsmanager:*:*:secret:${var.env}/payments/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  }
</span></span><span class="line"><span class="ln">7</span><span class="cl">}</span></span></code></pre></div><p>前綴授權的好處是新增 secret 時不需要改 IAM policy — 只要命名落在同一個前綴下，存取權限自動繼承。跟<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二</a>的最小權限設計一致：service A 的 role 只看得到 <code>payments/*</code>，看不到 <code>auth/*</code>，即使它們存在同一個帳號的 Secrets Manager 裡。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<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 後端的加密與存取控制是 secret 引用策略的安全地基</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：誰能讀哪些 secret 的 IAM 權限設計</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>：tag 的環境值與 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>：tag 合規與 secret 掃描整合進 CI pipeline</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：密鑰生命週期、輪替策略與資料保護的完整討論</li>
</ul>
]]></content:encoded></item><item><title>成本可見性與最小可行治理節奏</title><link>https://tarrragon.github.io/blog/infra/08-governance-habits/cost-visibility-rhythm/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/08-governance-habits/cost-visibility-rhythm/</guid><description>&lt;p>治理習慣的責任是讓基礎設施在規模長大後仍然可被盤點、可被追責、可被回收。資源歸屬靠 tagging、密鑰安全靠 secret 管理（見 &lt;a href="https://tarrragon.github.io/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">tagging 與 secrets&lt;/a>），本篇處理兩個後續問題：成本怎麼拆解到擁有者，以及治理規範的節奏怎麼拿捏 — 什麼該第一天就立、什麼等到痛點出現再加。&lt;/p>
&lt;p>先界定邊界。成本這一塊分兩層：把資源歸屬到擁有者與用途的地基（tagging、chargeback 的依據）在這裡，運行期怎麼用 reserved instance、spot、rightsizing 去壓低帳單，是 &lt;a href="https://tarrragon.github.io/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理&lt;/a> 的範圍。&lt;/p>
&lt;h2 id="成本可見性每筆花費都對得到擁有者與用途">成本可見性：每筆花費都對得到擁有者與用途&lt;/h2>
&lt;p>成本可見性的目標是讓帳單上的每一筆花費都能回答「這是誰的、為了什麼」。雲帳單預設是一筆按服務類型加總的數字 — EC2 多少、RDS 多少 — 這個視角能告訴你花在哪類資源，卻答不出花在哪個團隊、哪個產品線、哪個功能。當這個問題答不出來，成本就變成一筆沒人負責的公共支出，沒有人有動機去優化自己看不到的帳。&lt;/p>
&lt;h3 id="tag-驅動的成本分攤">Tag 驅動的成本分攤&lt;/h3>
&lt;p>把成本拆解到擁有者的地基，正是 tagging。雲廠商的成本分攤工具（AWS Cost Explorer、Cost Allocation Tags、GCP 的 billing label）能用 tag 當分群維度，前提是那些 tag 要先在 billing 後台啟用為「成本分攤標籤（Cost Allocation Tag）」。啟用是一次性設定，之後新建的資源只要帶了這個 tag，費用就會自動歸入對應維度。&lt;/p>
&lt;p>啟用後，&lt;code>cost-center&lt;/code> 和 &lt;code>owner&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">&lt;span class="c1"># 用 AWS CLI 查某個 cost-center 的月費用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws ce get-cost-and-usage &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> --time-period &lt;span class="nv">Start&lt;/span>&lt;span class="o">=&lt;/span>2026-06-01,End&lt;span class="o">=&lt;/span>2026-06-30 &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> --granularity MONTHLY &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> --filter &lt;span class="s1">&amp;#39;{&amp;#34;Tags&amp;#34;:{&amp;#34;Key&amp;#34;:&amp;#34;cost-center&amp;#34;,&amp;#34;Values&amp;#34;:[&amp;#34;cc-1024&amp;#34;]}}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --metrics BlendedCost &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --group-by &lt;span class="nv">Type&lt;/span>&lt;span class="o">=&lt;/span>TAG,Key&lt;span class="o">=&lt;/span>owner&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「team-payments 這個月花多少」「staging 環境占總成本幾成」變成一張報表而不是一場會議。&lt;/p>
&lt;h3 id="成本異常告警">成本異常告警&lt;/h3>
&lt;p>可見性先於優化，這個順序不能反。看不見的成本無法被歸屬，無法歸屬就無法問責，沒有問責就沒有人去做優化。在可見性建立之後，下一步是設一條成本異常告警：&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">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_ce_anomaly_monitor&amp;#34; &amp;#34;cost&amp;#34;&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"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;daily-cost-anomaly&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"> monitor_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;DIMENSIONAL&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"> monitor_dimension&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;SERVICE&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_ce_anomaly_subscription&amp;#34; &amp;#34;alert&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 class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;cost-anomaly-alert&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> frequency&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;DAILY&amp;#34;&lt;/span>
&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">&lt;span class="n"> monitor_arn_list&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="k">aws_ce_anomaly_monitor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cost&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">subscriber&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;SNS&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="n"> address&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_sns_topic&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cost_alerts&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="k">threshold_expression&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">dimension&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&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;ANOMALY_TOTAL_IMPACT_ABSOLUTE&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="n"> values&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;100&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="n"> match_options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GREATER_THAN_OR_EQUAL&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>當告警觸發時，因為有 tag，可以立刻定位是哪個團隊的哪類資源在漲，而不是面對一個無法拆解到具體團隊或資源類型的總數。常見的成本異常來源：開發者開了一組大型 instance 測試後忘了關、某個 auto-scaling group 的最大值設太高在流量尖峰長出了大量機器、NAT Gateway 被大量出站流量灌到帳單翻倍。這些情境只要 tag 到位，都能在異常告警觸發後幾分鐘內找到根因。&lt;/p>
&lt;p>到了「知道誰花多少、接下來怎麼省」這一步 — reserved instance 的承諾折扣、spot 的可中斷算力、閒置資源的 rightsizing 與排程關機 — 就進入 &lt;a href="https://tarrragon.github.io/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理&lt;/a> 的運行期優化範圍。這一章負責的是讓那些優化「有帳可查、有人可問」。&lt;/p>
&lt;p>成本治理在不同規模下的操作形態差異很大。Netflix 把多套關聯式資料庫統一到 Aurora 後成本下降 28%，核心操作是「把資源種類收斂、讓成本歸因的維度減少」——這在 tagging 已經到位的前提下才做得到，見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &amp;#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix：Aurora 整併&lt;/a>。另一個極端是 Arcjet 用 Redis Streams 取代 managed Kafka，年費從六位數美金降到約 $1k，代價是自行維護 retention 與 consumer group 監控——這個取捨的前提是團隊有能力承擔額外的運維面，見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/" data-link-title="3.C43 Arcjet：Redis Streams 取代 Kafka 省 6 位數 $" data-link-desc="Arcjet security 平台、Kafka managed 6 位數 $/yr、用 Redis Streams 約 $1k/yr、自寫 Janitor 監控 retention。">3.C43 Arcjet：Redis Streams 取代 Kafka&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>治理習慣的責任是讓基礎設施在規模長大後仍然可被盤點、可被追責、可被回收。資源歸屬靠 tagging、密鑰安全靠 secret 管理（見 <a href="/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">tagging 與 secrets</a>），本篇處理兩個後續問題：成本怎麼拆解到擁有者，以及治理規範的節奏怎麼拿捏 — 什麼該第一天就立、什麼等到痛點出現再加。</p>
<p>先界定邊界。成本這一塊分兩層：把資源歸屬到擁有者與用途的地基（tagging、chargeback 的依據）在這裡，運行期怎麼用 reserved instance、spot、rightsizing 去壓低帳單，是 <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a> 的範圍。</p>
<h2 id="成本可見性每筆花費都對得到擁有者與用途">成本可見性：每筆花費都對得到擁有者與用途</h2>
<p>成本可見性的目標是讓帳單上的每一筆花費都能回答「這是誰的、為了什麼」。雲帳單預設是一筆按服務類型加總的數字 — EC2 多少、RDS 多少 — 這個視角能告訴你花在哪類資源，卻答不出花在哪個團隊、哪個產品線、哪個功能。當這個問題答不出來，成本就變成一筆沒人負責的公共支出，沒有人有動機去優化自己看不到的帳。</p>
<h3 id="tag-驅動的成本分攤">Tag 驅動的成本分攤</h3>
<p>把成本拆解到擁有者的地基，正是 tagging。雲廠商的成本分攤工具（AWS Cost Explorer、Cost Allocation Tags、GCP 的 billing label）能用 tag 當分群維度，前提是那些 tag 要先在 billing 後台啟用為「成本分攤標籤（Cost Allocation Tag）」。啟用是一次性設定，之後新建的資源只要帶了這個 tag，費用就會自動歸入對應維度。</p>
<p>啟用後，<code>cost-center</code> 和 <code>owner</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"># 用 AWS CLI 查某個 cost-center 的月費用</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws ce get-cost-and-usage <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --time-period <span class="nv">Start</span><span class="o">=</span>2026-06-01,End<span class="o">=</span>2026-06-30 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --granularity MONTHLY <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --filter <span class="s1">&#39;{&#34;Tags&#34;:{&#34;Key&#34;:&#34;cost-center&#34;,&#34;Values&#34;:[&#34;cc-1024&#34;]}}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --metrics BlendedCost <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --group-by <span class="nv">Type</span><span class="o">=</span>TAG,Key<span class="o">=</span>owner</span></span></code></pre></div><p>「team-payments 這個月花多少」「staging 環境占總成本幾成」變成一張報表而不是一場會議。</p>
<h3 id="成本異常告警">成本異常告警</h3>
<p>可見性先於優化，這個順序不能反。看不見的成本無法被歸屬，無法歸屬就無法問責，沒有問責就沒有人去做優化。在可見性建立之後，下一步是設一條成本異常告警：</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_ce_anomaly_monitor&#34; &#34;cost&#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;daily-cost-anomaly&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  monitor_type</span>      <span class="o">=</span> <span class="s2">&#34;DIMENSIONAL&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  monitor_dimension</span> <span class="o">=</span> <span class="s2">&#34;SERVICE&#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 class="k">resource</span> <span class="s2">&#34;aws_ce_anomaly_subscription&#34; &#34;alert&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  name</span>      <span class="o">=</span> <span class="s2">&#34;cost-anomaly-alert&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  frequency</span> <span class="o">=</span> <span class="s2">&#34;DAILY&#34;</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="n">  monitor_arn_list</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_ce_anomaly_monitor</span><span class="p">.</span><span class="k">cost</span><span class="p">.</span><span class="k">arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="k">subscriber</span> {
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">    type</span>    <span class="o">=</span> <span class="s2">&#34;SNS&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">    address</span> <span class="o">=</span> <span class="k">aws_sns_topic</span><span class="p">.</span><span class="k">cost_alerts</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  }
</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 class="k">threshold_expression</span> {
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">dimension</span> {
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">      key</span>           <span class="o">=</span> <span class="s2">&#34;ANOMALY_TOTAL_IMPACT_ABSOLUTE&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">      values</span>        <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;100&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="n">      match_options</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;GREATER_THAN_OR_EQUAL&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    }
</span></span><span class="line"><span class="ln">24</span><span class="cl">  }
</span></span><span class="line"><span class="ln">25</span><span class="cl">}</span></span></code></pre></div><p>當告警觸發時，因為有 tag，可以立刻定位是哪個團隊的哪類資源在漲，而不是面對一個無法拆解到具體團隊或資源類型的總數。常見的成本異常來源：開發者開了一組大型 instance 測試後忘了關、某個 auto-scaling group 的最大值設太高在流量尖峰長出了大量機器、NAT Gateway 被大量出站流量灌到帳單翻倍。這些情境只要 tag 到位，都能在異常告警觸發後幾分鐘內找到根因。</p>
<p>到了「知道誰花多少、接下來怎麼省」這一步 — reserved instance 的承諾折扣、spot 的可中斷算力、閒置資源的 rightsizing 與排程關機 — 就進入 <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a> 的運行期優化範圍。這一章負責的是讓那些優化「有帳可查、有人可問」。</p>
<p>成本治理在不同規模下的操作形態差異很大。Netflix 把多套關聯式資料庫統一到 Aurora 後成本下降 28%，核心操作是「把資源種類收斂、讓成本歸因的維度減少」——這在 tagging 已經到位的前提下才做得到，見 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix：Aurora 整併</a>。另一個極端是 Arcjet 用 Redis Streams 取代 managed Kafka，年費從六位數美金降到約 $1k，代價是自行維護 retention 與 consumer group 監控——這個取捨的前提是團隊有能力承擔額外的運維面，見 <a href="/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/" data-link-title="3.C43 Arcjet：Redis Streams 取代 Kafka 省 6 位數 $" data-link-desc="Arcjet security 平台、Kafka managed 6 位數 $/yr、用 Redis Streams 約 $1k/yr、自寫 Janitor 監控 retention。">3.C43 Arcjet：Redis Streams 取代 Kafka</a>。</p>
<h2 id="最小可行節奏先把地基跑起來再逐步加">最小可行節奏：先把地基跑起來，再逐步加</h2>
<p>治理的最小可行節奏，是早期只立「拔掉就會痛、補起來很貴」的那幾條規範，其餘留到規模逼出需求時再加。治理機制本身有維護成本 — 每一條策略規則、每一個審批關卡、每一套標籤分類法都要有人維護、有人解釋、有人在它擋錯東西時來救。在團隊還小、資源還少時堆滿企業級治理框架，付出的是當下的速度，換來的是一套還用不到的複雜度。</p>
<h3 id="補救成本曲線">補救成本曲線</h3>
<p>判斷一條治理規範該不該現在就立，看它的「補救成本曲線」— 越晚導入、事後補救的代價越高的規範，越應該提前立：</p>
<table>
  <thead>
      <tr>
          <th>規範</th>
          <th>補救成本曲線</th>
          <th>day-1 該立</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tagging</td>
          <td>陡峭</td>
          <td>是</td>
          <td>幾百個沒 tag 的資源要回頭考古，建立時順手標只要幾秒</td>
      </tr>
      <tr>
          <td>Secrets 不進 code</td>
          <td>幾乎垂直</td>
          <td>是</td>
          <td>密鑰一旦進了 git 歷史就無法清除，只能輪替</td>
      </tr>
      <tr>
          <td>成本分攤維度</td>
          <td>中等</td>
          <td>是（輕量）</td>
          <td>依賴 tagging，tag 立了它就近乎免費啟用</td>
      </tr>
      <tr>
          <td>Secret 自動輪替</td>
          <td>平緩</td>
          <td>等</td>
          <td>手動輪替在早期可接受，自動化在 secret 數量增多後再投入</td>
      </tr>
      <tr>
          <td>細緻的審批流程</td>
          <td>平坦</td>
          <td>等</td>
          <td>補救成本低、可以隨時加，早期硬上反而拖慢交付</td>
      </tr>
      <tr>
          <td>多層級策略引擎（OPA / Sentinel）</td>
          <td>平坦</td>
          <td>等</td>
          <td>等到 tag policy 擋不住的邊界案例出現再引入</td>
      </tr>
  </tbody>
</table>
<p>這個曲線給出的節奏是：補救成本陡的從第一天就用 IaC 強制，補救成本平的等到痛點確實出現 — 開始有人手滑誤刪、開始有跨團隊的權限爭議 — 再有針對性地加。那時你也才知道該往哪個方向加。</p>
<h3 id="過度治理的訊號">過度治理的訊號</h3>
<p>過度治理跟過度設計是同一類問題，訊號很類似：</p>
<ul>
<li>建一個測試用的小資源需要走三層審批流程</li>
<li>團隊花在解釋為什麼某個護欄擋錯的時間，比護欄實際擋住的風險還多</li>
<li>策略規則的 exception 清單比規則本身還長</li>
<li>新人第一週的大部分時間花在理解治理框架而非理解業務</li>
</ul>
<p>這些訊號出現時，該回頭簡化 — 砍掉沒帶來價值的規則、把誤判率高的規則降級為 warning 而非 blocking。治理框架跟程式碼一樣需要重構。</p>
<h3 id="和其他模組的節奏對齊">和其他模組的節奏對齊</h3>
<p>這個節奏跟<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零</a>的成熟度階梯是同一套思路：基礎設施的治理跟基礎設施本身一樣，是逐級長出來的，不是一次到位設計完的。把規範變成自動護欄的工程（PR 階段擋缺 tag、CI 掃 secret）值得早投入，因為自動化的護欄維護成本低、且越早接管越省人力 — 這部分怎麼落地在<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> 展開。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零：infra 是什麼</a>：成熟度階梯的務實節奏思路</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>：tag 合規與 secret 掃描整合進 CI pipeline</li>
<li>→ <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a>：運行期的成本控制與優化手段</li>
</ul>
]]></content:encoded></item><item><title>職務交接與存取撤銷設計</title><link>https://tarrragon.github.io/blog/infra/08-governance-habits/handover-design/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/08-governance-habits/handover-design/</guid><description>&lt;p>人員異動（離職、轉調、承包合約結束）是常態營運事件。基礎設施的設計決定了這件事的成本：如果環境的建立方式寫在程式碼裡、存取路徑收斂在 SSO、變更歷史留在 PR，交接是一兩天的帳號操作加上 repo 權限移交。如果環境靠個人記憶維護、存取散落在多組長期 key、變更歷史只在當事人的 shell history 裡，交接是數週的考古加上「不確定有沒有漏掉什麼」的持續焦慮。這篇文章處理兩件事：人走的時候怎麼安全撤銷存取，以及怎麼設計 infra 讓未來的交接成本結構性降低。&lt;/p>
&lt;h2 id="離職或轉調的存取撤銷清單">離職或轉調的存取撤銷清單&lt;/h2>
&lt;p>存取撤銷的目標是在人員離開的同一天（最晚 24 小時內）關閉所有該身分能存取雲端資源的路徑。撤銷的順序按影響範圍從大到小排：先關能連鎖失效的上游入口，再逐一清理下游殘留。&lt;/p>
&lt;h3 id="第一步停用-sso--idp-帳號">第一步：停用 SSO / IdP 帳號&lt;/h3>
&lt;p>如果雲端存取統一走 SSO（如 AWS IAM Identity Center、Okta、Google Workspace），停用 IdP 帳號會連鎖撤銷所有透過 SSO 取得的雲端權限 — 這是單一操作影響最大的一步。停用後，該人無法再透過 SSO 登入任何已接 SSO 的 AWS 帳號、CI 平台或內部工具。&lt;/p>
&lt;p>這一步能覆蓋多少取決於 SSO 的覆蓋率。如果某些雲端帳號還沒接 SSO（用獨立 IAM user 登入），停用 IdP 帳號不會影響那些路徑，需要額外處理。&lt;/p>
&lt;h3 id="第二步處理長期-access-key">第二步：處理長期 access key&lt;/h3>
&lt;p>從 credential report 找出該人名下的所有長期 access key：&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 iam generate-credential-report
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws iam get-credential-report --output text --query Content &lt;span class="p">|&lt;/span> base64 -d &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> &lt;span class="p">|&lt;/span> grep &lt;span class="s2">&amp;#34;departed-user&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每把 key 判斷處理方式：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>key 狀態&lt;/th>
 &lt;th>處理方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>只有該人在用&lt;/td>
 &lt;td>直接 deactivate，觀察 24 小時無異常後刪除&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>被自動化腳本引用&lt;/td>
 &lt;td>先建新 key 並更新引用處，再 deactivate 舊 key&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用途不明&lt;/td>
 &lt;td>先 deactivate（不刪），監控 CloudTrail 看有沒有存取失敗&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>deactivate 而非直接刪除是因為刪除不可逆 — 如果某個沒記錄在案的自動化正在用這把 key，deactivate 會讓它報權限錯誤，CloudTrail 會記錄失敗的 API 呼叫，方便追蹤；直接刪除後這把 key 的 ID 就消失了，追蹤更困難。&lt;/p>
&lt;h3 id="第三步刪除個人-iam-user">第三步：刪除個人 IAM user&lt;/h3>
&lt;p>確認沒有自動化依賴這個 user 後刪除。刪除前先檢查該 user 是否有 inline policy 或 group membership 被其他流程引用：&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 iam list-user-policies --user-name departed-user
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws iam list-groups-for-user --user-name departed-user
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">aws iam list-attached-user-policies --user-name departed-user&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="第四步第三方服務帳號">第四步：第三方服務帳號&lt;/h3>
&lt;p>雲端以外的存取路徑同樣需要撤銷：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>版本控制&lt;/strong>（GitHub / GitLab）：移除組織 membership 或降為 read-only&lt;/li>
&lt;li>&lt;strong>CI 平台&lt;/strong>（GitHub Actions secrets、GitLab CI variables）：如果該人曾設定過 CI secret，確認那些 secret 是否需要輪替&lt;/li>
&lt;li>&lt;strong>監控與告警&lt;/strong>（Grafana、PagerDuty、Datadog）：移除帳號或降權&lt;/li>
&lt;li>&lt;strong>基礎設施管理平台&lt;/strong>（Terraform Cloud、Spacelift）：移除 team membership&lt;/li>
&lt;/ul>
&lt;h3 id="第五步mfa-裝置解除註冊">第五步：MFA 裝置解除註冊&lt;/h3>
&lt;p>如果該人的 MFA 裝置仍然綁在帳號上（例如 root account 的 MFA），需要管理員介入解除並重新綁定。root account 的 MFA 裝置異動屬於高敏感操作，需要有第二人確認。&lt;/p>
&lt;h3 id="時程與回報">時程與回報&lt;/h3>
&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>SSO 停用&lt;/td>
 &lt;td>離職當天&lt;/td>
 &lt;td>確認 IdP 帳號已停用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>長期 key 處理&lt;/td>
 &lt;td>24 小時內&lt;/td>
 &lt;td>key 數量、各 key 處理方式（deactivate / 替換 / 刪除）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IAM user 刪除&lt;/td>
 &lt;td>48 小時內&lt;/td>
 &lt;td>確認無殘留 user&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方服務&lt;/td>
 &lt;td>48 小時內&lt;/td>
 &lt;td>各平台的處理狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>管理層回報&lt;/td>
 &lt;td>48 小時內&lt;/td>
 &lt;td>一份清單確認所有存取路徑已關閉&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這份回報不是形式 — 它是對管理層證明「離職者已無法存取任何系統」的書面紀錄，合規稽核時會被要求出示。&lt;/p></description><content:encoded><![CDATA[<p>人員異動（離職、轉調、承包合約結束）是常態營運事件。基礎設施的設計決定了這件事的成本：如果環境的建立方式寫在程式碼裡、存取路徑收斂在 SSO、變更歷史留在 PR，交接是一兩天的帳號操作加上 repo 權限移交。如果環境靠個人記憶維護、存取散落在多組長期 key、變更歷史只在當事人的 shell history 裡，交接是數週的考古加上「不確定有沒有漏掉什麼」的持續焦慮。這篇文章處理兩件事：人走的時候怎麼安全撤銷存取，以及怎麼設計 infra 讓未來的交接成本結構性降低。</p>
<h2 id="離職或轉調的存取撤銷清單">離職或轉調的存取撤銷清單</h2>
<p>存取撤銷的目標是在人員離開的同一天（最晚 24 小時內）關閉所有該身分能存取雲端資源的路徑。撤銷的順序按影響範圍從大到小排：先關能連鎖失效的上游入口，再逐一清理下游殘留。</p>
<h3 id="第一步停用-sso--idp-帳號">第一步：停用 SSO / IdP 帳號</h3>
<p>如果雲端存取統一走 SSO（如 AWS IAM Identity Center、Okta、Google Workspace），停用 IdP 帳號會連鎖撤銷所有透過 SSO 取得的雲端權限 — 這是單一操作影響最大的一步。停用後，該人無法再透過 SSO 登入任何已接 SSO 的 AWS 帳號、CI 平台或內部工具。</p>
<p>這一步能覆蓋多少取決於 SSO 的覆蓋率。如果某些雲端帳號還沒接 SSO（用獨立 IAM user 登入），停用 IdP 帳號不會影響那些路徑，需要額外處理。</p>
<h3 id="第二步處理長期-access-key">第二步：處理長期 access key</h3>
<p>從 credential report 找出該人名下的所有長期 access key：</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 iam generate-credential-report
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws iam get-credential-report --output text --query Content <span class="p">|</span> base64 -d <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="p">|</span> grep <span class="s2">&#34;departed-user&#34;</span></span></span></code></pre></div><p>每把 key 判斷處理方式：</p>
<table>
  <thead>
      <tr>
          <th>key 狀態</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只有該人在用</td>
          <td>直接 deactivate，觀察 24 小時無異常後刪除</td>
      </tr>
      <tr>
          <td>被自動化腳本引用</td>
          <td>先建新 key 並更新引用處，再 deactivate 舊 key</td>
      </tr>
      <tr>
          <td>用途不明</td>
          <td>先 deactivate（不刪），監控 CloudTrail 看有沒有存取失敗</td>
      </tr>
  </tbody>
</table>
<p>deactivate 而非直接刪除是因為刪除不可逆 — 如果某個沒記錄在案的自動化正在用這把 key，deactivate 會讓它報權限錯誤，CloudTrail 會記錄失敗的 API 呼叫，方便追蹤；直接刪除後這把 key 的 ID 就消失了，追蹤更困難。</p>
<h3 id="第三步刪除個人-iam-user">第三步：刪除個人 IAM user</h3>
<p>確認沒有自動化依賴這個 user 後刪除。刪除前先檢查該 user 是否有 inline policy 或 group membership 被其他流程引用：</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 iam list-user-policies --user-name departed-user
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws iam list-groups-for-user --user-name departed-user
</span></span><span class="line"><span class="ln">3</span><span class="cl">aws iam list-attached-user-policies --user-name departed-user</span></span></code></pre></div><h3 id="第四步第三方服務帳號">第四步：第三方服務帳號</h3>
<p>雲端以外的存取路徑同樣需要撤銷：</p>
<ul>
<li><strong>版本控制</strong>（GitHub / GitLab）：移除組織 membership 或降為 read-only</li>
<li><strong>CI 平台</strong>（GitHub Actions secrets、GitLab CI variables）：如果該人曾設定過 CI secret，確認那些 secret 是否需要輪替</li>
<li><strong>監控與告警</strong>（Grafana、PagerDuty、Datadog）：移除帳號或降權</li>
<li><strong>基礎設施管理平台</strong>（Terraform Cloud、Spacelift）：移除 team membership</li>
</ul>
<h3 id="第五步mfa-裝置解除註冊">第五步：MFA 裝置解除註冊</h3>
<p>如果該人的 MFA 裝置仍然綁在帳號上（例如 root account 的 MFA），需要管理員介入解除並重新綁定。root account 的 MFA 裝置異動屬於高敏感操作，需要有第二人確認。</p>
<h3 id="時程與回報">時程與回報</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>時限</th>
          <th>回報內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SSO 停用</td>
          <td>離職當天</td>
          <td>確認 IdP 帳號已停用</td>
      </tr>
      <tr>
          <td>長期 key 處理</td>
          <td>24 小時內</td>
          <td>key 數量、各 key 處理方式（deactivate / 替換 / 刪除）</td>
      </tr>
      <tr>
          <td>IAM user 刪除</td>
          <td>48 小時內</td>
          <td>確認無殘留 user</td>
      </tr>
      <tr>
          <td>第三方服務</td>
          <td>48 小時內</td>
          <td>各平台的處理狀態</td>
      </tr>
      <tr>
          <td>管理層回報</td>
          <td>48 小時內</td>
          <td>一份清單確認所有存取路徑已關閉</td>
      </tr>
  </tbody>
</table>
<p>這份回報不是形式 — 它是對管理層證明「離職者已無法存取任何系統」的書面紀錄，合規稽核時會被要求出示。</p>
<h2 id="離職時的-credential-rotation">離職時的 credential rotation</h2>
<p>存取撤銷處理的是「這個人自己的 key 和帳號」。如果離職者曾有 admin 級別的存取權，還需要處理他可能接觸過的共用 secret。</p>
<p>rotation 的範圍取決於該人的權限等級：</p>
<table>
  <thead>
      <tr>
          <th>權限等級</th>
          <th>rotation 範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只有特定服務的讀取</td>
          <td>不需額外 rotation</td>
      </tr>
      <tr>
          <td>特定服務的讀寫</td>
          <td>該服務的 API key 和連線密碼</td>
      </tr>
      <tr>
          <td>跨服務或帳號的管理權限</td>
          <td>所有 Secrets Manager 裡該人可讀的 secret</td>
      </tr>
      <tr>
          <td>root 或 admin 等級</td>
          <td>全面 rotation + CloudTrail 審計最近 30 天活動</td>
      </tr>
  </tbody>
</table>
<p>admin 級別離職時的 CloudTrail 審計：</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>Username,AttributeValue<span class="o">=</span>departed-user <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --start-time <span class="k">$(</span>date -v-30d +%Y-%m-%dT%H:%M:%SZ<span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --max-items <span class="m">100</span> <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;Events[].[EventTime,EventName,Resources[0].ResourceName]&#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></code></pre></div><p>審計的目的是確認離職前 30 天內有沒有異常操作（大量資料下載、權限變更、新 key 建立），而非預設離職者有惡意。這是標準的安全衛生程序。</p>
<p>如果團隊已經全面採用 OIDC 短期憑證（見<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>），離職時的 credential rotation 範圍會大幅縮小 — 沒有長期 key 就沒有需要輪替的靜態憑證，SSO 停用後短期 token 自然失效。</p>
<h2 id="iac-與-pr-歷史怎麼降低交接成本">IaC 與 PR 歷史怎麼降低交接成本</h2>
<p>存取撤銷是離職當天的緊急操作。交接成本的高低則取決於新接手的人能多快理解環境的結構與歷史。</p>
<p>環境結構寫在 IaC 裡時，新人讀 repo 就能回答「我們有幾個 VPC、subnet 怎麼切、哪些服務在哪個 private subnet」。PR 歷史回答「為什麼 NAT 從共享改成 per-AZ」（因為上個月 ap-northeast-1a 故障時全部出站斷了）。這些資訊不依賴任何個人的記憶，新人第一天就能取得。</p>
<p>程式碼和 PR 歷史能涵蓋的是環境的結構與變更理由。以下資訊不在程式碼裡，需要額外文件或交接：</p>
<ul>
<li><strong>營運脈絡</strong>：哪些服務是流量敏感的、哪個時段不能做變更、哪些客戶有特殊 SLA</li>
<li><strong>事故歷史</strong>：過去發生過什麼事故、當時怎麼處理的、有沒有遺留的 workaround</li>
<li><strong>vendor 關係</strong>：support contract 的聯絡方式、升級路徑、合約到期時間</li>
<li><strong>進行中的工作</strong>：正在做的遷移、已知但未處理的技術債、已規劃但未執行的變更</li>
</ul>
<p>時程參考：環境完全在 IaC 裡的團隊，infra 角色交接通常 1-2 天能讓新人開始獨立操作（讀 code + 第一次 PR）。沒有 IaC 的環境，交接需要 1-2 週的口頭傳授加上新人自行摸索。</p>
<h2 id="最小交接清單">最小交接清單</h2>
<p>任何 infra 角色變更（不只是離職，包括長假、轉組、新人 onboarding）都應該走過一次這份清單：</p>
<h3 id="帳號與存取盤點">帳號與存取盤點</h3>
<ul>
<li>所有雲端帳號的列表（帳號 ID、用途、環境對應）</li>
<li>CI/CD 平台的組織與 repo 存取</li>
<li>監控與告警平台的帳號</li>
<li>DNS 管理（域名註冊商、Route 53 hosted zone）</li>
<li>SSL 憑證管理（ACM、Let&rsquo;s Encrypt）</li>
</ul>
<h3 id="憑證盤點">憑證盤點</h3>
<ul>
<li>長期 access key 清單（從 credential report 取得）</li>
<li>Secrets Manager / SSM Parameter Store 裡的 secret 清單</li>
<li>第三方服務的 API key（付費服務、SaaS 整合）</li>
</ul>
<h3 id="聯絡與升級路徑">聯絡與升級路徑</h3>
<ul>
<li>雲端 vendor 的 support 聯絡方式與 support plan 等級</li>
<li>資安事件的通報對象與流程</li>
<li>on-call chain 與升級規則</li>
</ul>
<h3 id="進行中的工作">進行中的工作</h3>
<ul>
<li>正在執行的遷移或重構（目前到哪一步、下一步是什麼）</li>
<li>已知的技術債與風險（哪些資源還沒納管、哪些 key 該輪替但還沒輪替）</li>
<li>已排程但未開始的變更</li>
</ul>
<p>這份清單的維護成本很低 — 多數項目在日常工作中已經存在（credential report、repo 結構、ticket board），交接時只需要把散落的資訊收斂到一份文件。如果每次交接都要花時間「找資訊在哪裡」，代表日常的資訊組織有改善空間。</p>
<h2 id="讓交接成本結構性降低的設計">讓交接成本結構性降低的設計</h2>
<p>上面的清單處理的是每次交接的操作成本。以下設計原則處理的是讓這個成本隨時間趨近固定值、而非隨環境複雜度增長：</p>
<p><strong>SSO 作為單一存取撤銷點</strong>：所有雲端存取走 SSO，離職時停用一個帳號就關閉所有路徑。沒有 SSO 時，每多一個平台就多一個需要手動撤銷的路徑，漏撤任何一個都是安全缺口。SSO 的覆蓋率越高，撤銷操作越接近 O(1)。</p>
<p><strong>消除個人長期 key</strong>：用 OIDC + role assumption 取代長期 access key（見<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>）。沒有長期 key，離職時就沒有需要逐一追蹤和輪替的靜態憑證。credential rotation 的範圍從「所有 key」縮小到「共用 secret」。</p>
<p><strong>環境描述在程式碼裡</strong>：IaC 讓環境結構對任何有 repo 存取的人可讀。交接的知識成本從「口頭傳授整個環境長什麼樣」降到「讀 code + 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 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>。</p>
<p><strong>PR 描述記錄「為什麼」</strong>：程式碼記錄「什麼」，PR 描述記錄「為什麼」。三個月後翻 git log，看到「把 NAT 從共享改成 per-AZ」知道改了什麼；看到 PR 描述裡的「因為上週 ap-northeast-1a 故障時全部出站斷了」才知道為什麼。這段脈絡在交接時的價值最高 — 新人最常問的問題就是「為什麼這樣設定」。</p>
<p><strong>on-call 輪替分散操作知識</strong>：讓不同人輪流負責 infra 的 review、apply 和事故處理，用操作經驗分散知識。判斷知識是否過度集中的方式：如果團隊裡只有一個人敢對 production 做 apply，那個人就是交接的瓶頸。見<a href="/blog/infra/09-driving-adoption/" data-link-title="模組九：怎麼把 infra 推動起來" data-link-desc="技術正確不等於推得動 — 信任赤字、期望值對齊、知識共享，infra 落地的組織課題">模組九：怎麼把 infra 推動起來</a>。</p>
<p>這些設計的共同效果是讓交接的固定成本保持在「停用帳號 + 移交 repo 權限 + 走一次交接清單」，不隨環境複雜度或人員流動頻率等比增長。</p>
<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>：IAM 設計、OIDC 短期憑證、權限邊界</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>：PR 作為知識載體、變更可追溯</li>
<li>→ <a href="/blog/infra/09-driving-adoption/" data-link-title="模組九：怎麼把 infra 推動起來" data-link-desc="技術正確不等於推得動 — 信任赤字、期望值對齊、知識共享，infra 落地的組織課題">模組九：怎麼把 infra 推動起來</a>：知識共享與 on-call 輪替</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：Secret 輪替策略</li>
</ul>
]]></content:encoded></item></channel></rss>