<?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>Deployment on Tarragon</title><link>https://tarrragon.github.io/blog/tags/deployment/</link><description>Recent content in Deployment 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/deployment/index.xml" rel="self" type="application/rss+xml"/><item><title>Client-Side LLM / Embedding</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/client-side-llm/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/client-side-llm/</guid><description>&lt;p>Client-side LLM / embedding 的核心概念是「&lt;strong>模型權重下載到使用者瀏覽器、用 WebGPU 或 WebAssembly 直接在 browser 內推論、不經過任何 server&lt;/strong>」。代表 runtime：WebLLM（MLC AI、用 WebGPU）、wllama（llama.cpp 的 WebAssembly port）、&lt;code>@xenova/transformers&lt;/code>（瀏覽器版 transformers）。是「靜態網站做 RAG」、「離線可用 LLM 應用」這類場景的關鍵基底。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>跟其他 LLM deployment 形態的對比：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>形態&lt;/th>
 &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>雲端 LLM API&lt;/td>
 &lt;td>雲端伺服器&lt;/td>
 &lt;td>雲端&lt;/td>
 &lt;td>視 vendor 政策&lt;/td>
 &lt;td>高品質、production&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本地 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器&lt;/a>&lt;/td>
 &lt;td>本機磁碟&lt;/td>
 &lt;td>本機 process&lt;/td>
 &lt;td>完全本地&lt;/td>
 &lt;td>寫 code、個人 dev&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Client-side LLM&lt;/strong>&lt;/td>
 &lt;td>使用者 browser cache&lt;/td>
 &lt;td>使用者 browser&lt;/td>
 &lt;td>完全本地（不經 server）&lt;/td>
 &lt;td>靜態網站、demo、離線&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>主流 client-side runtime（2026/5）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Runtime&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>@xenova/transformers&lt;/code>&lt;/td>
 &lt;td>WASM、ONNX 格式&lt;/td>
 &lt;td>sentence-transformers、小型 LLM、CLIP、embedding&lt;/td>
 &lt;td>&amp;lt; 100 MB / 模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WebLLM（MLC）&lt;/td>
 &lt;td>WebGPU、自家 MLC compiled&lt;/td>
 &lt;td>Llama / Qwen / Gemma / Phi 等 1-13B&lt;/td>
 &lt;td>1-8 GB / 模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>wllama&lt;/td>
 &lt;td>WASM、llama.cpp 編譯版&lt;/td>
 &lt;td>GGUF Q4 等量化模型、&amp;lt; 4B 為主&lt;/td>
 &lt;td>0.5-4 GB / 模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>transformers.js&lt;/code>&lt;/td>
 &lt;td>WASM、跟 &lt;code>@xenova/transformers&lt;/code> 同源&lt;/td>
 &lt;td>同上&lt;/td>
 &lt;td>同上&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>讀靜態網站 / 前端 RAG / 離線 LLM 教學看到「WebGPU LLM」「browser-side embedding」「offline LLM」就是這 paradigm。寫 code 場景的判讀：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>首訪載入慢&lt;/strong>：browser 第一次要下載模型權重（embedding 模型 ~50MB、LLM 1-5GB）、首訪體驗差；後續訪問 cache 起來、變快&lt;/li>
&lt;li>&lt;strong>WebGPU 支援度&lt;/strong>：2026/5 仍非所有 browser / 裝置都穩定支援、Safari iOS 較弱；fallback 到 WASM 但速度降一個量級&lt;/li>
&lt;li>&lt;strong>模型完整性沒簽章&lt;/strong>：使用者下載到的模型權重沒類似 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/model-card/" data-link-title="Model Card" data-link-desc="Hugging Face 等平台上模型的 metadata 文件、列出模型來源、訓練資料、能力、限制、授權">GGUF model card&lt;/a> 的官方驗證、要靠 CDN + HTTPS 信任、不像本地 Ollama 有 hash 比對&lt;/li>
&lt;li>&lt;strong>適合「embedding + 小 LLM」、不適合「30B reasoning」&lt;/strong>：browser 記憶體跟 WebGPU 算力都遠不如本地 Ollama、選 &amp;lt; 4B 模型較實際&lt;/li>
&lt;li>&lt;strong>跟資安的關係&lt;/strong>：client-side 不需要 server API key、隱私強；但模型分發鏈（CDN → browser）成為新的供應鏈面、見 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">4.16 靜態 RAG deployment&lt;/a> 的資安段&lt;/li>
&lt;/ol></description><content:encoded><![CDATA[<p>Client-side LLM / embedding 的核心概念是「<strong>模型權重下載到使用者瀏覽器、用 WebGPU 或 WebAssembly 直接在 browser 內推論、不經過任何 server</strong>」。代表 runtime：WebLLM（MLC AI、用 WebGPU）、wllama（llama.cpp 的 WebAssembly port）、<code>@xenova/transformers</code>（瀏覽器版 transformers）。是「靜態網站做 RAG」、「離線可用 LLM 應用」這類場景的關鍵基底。</p>
<h2 id="概念位置">概念位置</h2>
<p>跟其他 LLM deployment 形態的對比：</p>
<table>
  <thead>
      <tr>
          <th>形態</th>
          <th>模型權重位置</th>
          <th>推論執行位置</th>
          <th>隱私</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>雲端 LLM API</td>
          <td>雲端伺服器</td>
          <td>雲端</td>
          <td>視 vendor 政策</td>
          <td>高品質、production</td>
      </tr>
      <tr>
          <td>本地 <a href="/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器</a></td>
          <td>本機磁碟</td>
          <td>本機 process</td>
          <td>完全本地</td>
          <td>寫 code、個人 dev</td>
      </tr>
      <tr>
          <td><strong>Client-side LLM</strong></td>
          <td>使用者 browser cache</td>
          <td>使用者 browser</td>
          <td>完全本地（不經 server）</td>
          <td>靜態網站、demo、離線</td>
      </tr>
  </tbody>
</table>
<p>主流 client-side runtime（2026/5）：</p>
<table>
  <thead>
      <tr>
          <th>Runtime</th>
          <th>機制</th>
          <th>模型支援</th>
          <th>典型體積</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>@xenova/transformers</code></td>
          <td>WASM、ONNX 格式</td>
          <td>sentence-transformers、小型 LLM、CLIP、embedding</td>
          <td>&lt; 100 MB / 模型</td>
      </tr>
      <tr>
          <td>WebLLM（MLC）</td>
          <td>WebGPU、自家 MLC compiled</td>
          <td>Llama / Qwen / Gemma / Phi 等 1-13B</td>
          <td>1-8 GB / 模型</td>
      </tr>
      <tr>
          <td>wllama</td>
          <td>WASM、llama.cpp 編譯版</td>
          <td>GGUF Q4 等量化模型、&lt; 4B 為主</td>
          <td>0.5-4 GB / 模型</td>
      </tr>
      <tr>
          <td><code>transformers.js</code></td>
          <td>WASM、跟 <code>@xenova/transformers</code> 同源</td>
          <td>同上</td>
          <td>同上</td>
      </tr>
  </tbody>
</table>
<h2 id="設計責任">設計責任</h2>
<p>讀靜態網站 / 前端 RAG / 離線 LLM 教學看到「WebGPU LLM」「browser-side embedding」「offline LLM」就是這 paradigm。寫 code 場景的判讀：</p>
<ol>
<li><strong>首訪載入慢</strong>：browser 第一次要下載模型權重（embedding 模型 ~50MB、LLM 1-5GB）、首訪體驗差；後續訪問 cache 起來、變快</li>
<li><strong>WebGPU 支援度</strong>：2026/5 仍非所有 browser / 裝置都穩定支援、Safari iOS 較弱；fallback 到 WASM 但速度降一個量級</li>
<li><strong>模型完整性沒簽章</strong>：使用者下載到的模型權重沒類似 <a href="/blog/llm/knowledge-cards/model-card/" data-link-title="Model Card" data-link-desc="Hugging Face 等平台上模型的 metadata 文件、列出模型來源、訓練資料、能力、限制、授權">GGUF model card</a> 的官方驗證、要靠 CDN + HTTPS 信任、不像本地 Ollama 有 hash 比對</li>
<li><strong>適合「embedding + 小 LLM」、不適合「30B reasoning」</strong>：browser 記憶體跟 WebGPU 算力都遠不如本地 Ollama、選 &lt; 4B 模型較實際</li>
<li><strong>跟資安的關係</strong>：client-side 不需要 server API key、隱私強；但模型分發鏈（CDN → browser）成為新的供應鏈面、見 <a href="/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">4.16 靜態 RAG deployment</a> 的資安段</li>
</ol>
]]></content:encoded></item><item><title>5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/</guid><description>&lt;p>這個案例的核心責任是把平台遷移從「搬家」改寫成「流量與依賴分段切換」。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Tradeshift 從 self-hosted Kubernetes 遷移到 Amazon EKS，legacy 叢集上運行 409 個 service。遷移以零停機為硬性前提，且要求對應用程式碼零修改——遷移的複雜度由平台層吸收，服務團隊不改程式碼。&lt;/p>
&lt;p>遷移採用 parallel cluster 架構：新舊叢集同時運行，透過 Linkerd service mesh 的 multi-cluster 能力橋接。Linkerd 在新叢集中建立 mirrored service（帶叢集後綴），讓跨叢集服務呼叫對應用層透明。流量切換用 Linkerd 的 traffic splitting policy 分批控制，不需要修改個別服務的路由邏輯。&lt;/p>
&lt;p>跨叢集延遲實測：從 EKS 叢集存取 legacy 叢集的 gateway，P50=2ms、P95=8ms、P99=9ms。這個延遲水平足以支撐遷移期的跨叢集服務呼叫，但對延遲敏感的路徑仍需要在同一叢集內完成切換才能消除這層額外延遲。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這類遷移的難點在跨叢集服務依賴與流量切換，Kubernetes API 相容性反而是最容易處理的部分。Linkerd multi-cluster 在這個案例中解決了三個問題：跨叢集 service discovery（mirrored service 自動同步）、流量分批控制（traffic splitting 不改應用碼）、遷移期 rollback（切回舊叢集只需調整 traffic split 比例）。&lt;/p>
&lt;p>409 個 service 的遷移不是一次完成——service 之間有依賴關係，遷移順序要按依賴拓樸規劃。被多個服務依賴的基礎 service（auth、config）通常最後遷移或在兩邊都保留，避免跨叢集呼叫成為所有服務的共同瓶頸。&lt;/p>
&lt;p>遷移期最大的隱性風險是「跨叢集延遲累積」。單次跨叢集呼叫 P99=9ms 看似可接受，但一條請求路徑如果串接 5 個跨叢集呼叫，累積延遲可達 45ms。遷移規劃要把服務依賴鏈上的跨叢集呼叫次數納入切換順序考量。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>建立 parallel cluster + mesh bridge&lt;/strong>：新叢集用 EKS 建立，Linkerd multi-cluster 連接新舊叢集，mirrored service 讓跨叢集呼叫透明。&lt;/li>
&lt;li>&lt;strong>按依賴拓樸排序遷移批次&lt;/strong>：葉子服務（無下游依賴）先遷，基礎服務最後遷或雙邊保留。每批遷移後驗證跨叢集延遲是否在可接受範圍。&lt;/li>
&lt;li>&lt;strong>Traffic splitting 分批切流量&lt;/strong>：每個服務遷移後，用 traffic split 從 0% 開始逐步把流量導向新叢集。觀察 per-service error rate 與 latency，確認穩定後提高比例。&lt;/li>
&lt;li>&lt;strong>保留 rollback 路徑&lt;/strong>：舊叢集服務不立即下線，traffic split 隨時可切回 100% 舊叢集。rollback 操作是調整 split 比例，不需要重新部署。&lt;/li>
&lt;li>&lt;strong>遷移完成後拆除 mesh bridge&lt;/strong>：所有服務切換完成且穩定觀測後，移除跨叢集 Linkerd 連線，舊叢集下線。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫的章節段落">可回寫的章節段落&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移&lt;/a>：traffic split 的分批切換與回退策略&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 跨叢集 Discovery&lt;/a>：Linkerd mirrored service 是跨叢集 discovery 的 service mesh federation 做法&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a>：每批切換的放行條件與停損訊號&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/containers/tradeshifts-migration-to-amazon-eks-without-downtime-using-linkerd/">Tradeshift migration to EKS without downtime using Linkerd&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把平台遷移從「搬家」改寫成「流量與依賴分段切換」。</p>
<h2 id="觀察">觀察</h2>
<p>Tradeshift 從 self-hosted Kubernetes 遷移到 Amazon EKS，legacy 叢集上運行 409 個 service。遷移以零停機為硬性前提，且要求對應用程式碼零修改——遷移的複雜度由平台層吸收，服務團隊不改程式碼。</p>
<p>遷移採用 parallel cluster 架構：新舊叢集同時運行，透過 Linkerd service mesh 的 multi-cluster 能力橋接。Linkerd 在新叢集中建立 mirrored service（帶叢集後綴），讓跨叢集服務呼叫對應用層透明。流量切換用 Linkerd 的 traffic splitting policy 分批控制，不需要修改個別服務的路由邏輯。</p>
<p>跨叢集延遲實測：從 EKS 叢集存取 legacy 叢集的 gateway，P50=2ms、P95=8ms、P99=9ms。這個延遲水平足以支撐遷移期的跨叢集服務呼叫，但對延遲敏感的路徑仍需要在同一叢集內完成切換才能消除這層額外延遲。</p>
<h2 id="判讀">判讀</h2>
<p>這類遷移的難點在跨叢集服務依賴與流量切換，Kubernetes API 相容性反而是最容易處理的部分。Linkerd multi-cluster 在這個案例中解決了三個問題：跨叢集 service discovery（mirrored service 自動同步）、流量分批控制（traffic splitting 不改應用碼）、遷移期 rollback（切回舊叢集只需調整 traffic split 比例）。</p>
<p>409 個 service 的遷移不是一次完成——service 之間有依賴關係，遷移順序要按依賴拓樸規劃。被多個服務依賴的基礎 service（auth、config）通常最後遷移或在兩邊都保留，避免跨叢集呼叫成為所有服務的共同瓶頸。</p>
<p>遷移期最大的隱性風險是「跨叢集延遲累積」。單次跨叢集呼叫 P99=9ms 看似可接受，但一條請求路徑如果串接 5 個跨叢集呼叫，累積延遲可達 45ms。遷移規劃要把服務依賴鏈上的跨叢集呼叫次數納入切換順序考量。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>建立 parallel cluster + mesh bridge</strong>：新叢集用 EKS 建立，Linkerd multi-cluster 連接新舊叢集，mirrored service 讓跨叢集呼叫透明。</li>
<li><strong>按依賴拓樸排序遷移批次</strong>：葉子服務（無下游依賴）先遷，基礎服務最後遷或雙邊保留。每批遷移後驗證跨叢集延遲是否在可接受範圍。</li>
<li><strong>Traffic splitting 分批切流量</strong>：每個服務遷移後，用 traffic split 從 0% 開始逐步把流量導向新叢集。觀察 per-service error rate 與 latency，確認穩定後提高比例。</li>
<li><strong>保留 rollback 路徑</strong>：舊叢集服務不立即下線，traffic split 隨時可切回 100% 舊叢集。rollback 操作是調整 split 比例，不需要重新部署。</li>
<li><strong>遷移完成後拆除 mesh bridge</strong>：所有服務切換完成且穩定觀測後，移除跨叢集 Linkerd 連線，舊叢集下線。</li>
</ol>
<h2 id="可回寫的章節段落">可回寫的章節段落</h2>
<ul>
<li><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移</a>：traffic split 的分批切換與回退策略</li>
<li><a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 跨叢集 Discovery</a>：Linkerd mirrored service 是跨叢集 discovery 的 service mesh federation 做法</li>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>：每批切換的放行條件與停損訊號</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/containers/tradeshifts-migration-to-amazon-eks-without-downtime-using-linkerd/">Tradeshift migration to EKS without downtime using Linkerd</a></li>
</ul>
]]></content:encoded></item><item><title>Kubernetes</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/</guid><description>&lt;p>Kubernetes 是 container orchestration 事實標準、承擔三個責任：workload lifecycle（pod / deployment / probe / rolling update）、cluster networking（service / ingress / DNS）、resource scheduling（resource limit / QoS / autoscaling）。設計取捨偏向「declarative + control loop + extensible」、是 cloud-native 生態的核心抽象。可自管或用 cloud managed（GKE / EKS / AKS）。&lt;/p>
&lt;p>對「多服務多實例 container orchestration、需要 rolling update / blue-green / canary、跨雲 / 跨環境統一抽象」這條路徑、Kubernetes 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 kubectl 部署 Deployment + Service、配置 probe / resource limit&lt;/li>
&lt;li>設計 rolling update / pod disruption budget 避免服務中斷&lt;/li>
&lt;li>選 Ingress controller（nginx / traefik / GLBC / ALB Controller）&lt;/li>
&lt;li>看懂 pod stuck / probe fail / OOMKilled / drain timeout 訊號&lt;/li>
&lt;li>評估 managed（GKE / EKS / AKS）vs 自管 vs Operator 進階場景&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-kubernetes-跑起來">最短路徑：5 分鐘把 Kubernetes 跑起來&lt;/h2>





&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"># 1. 本機跑 kind（需先安裝 kind + docker）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">kind create cluster --name dev
&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"># 2. 部署 Deployment + Service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">kubectl create deployment nginx --image&lt;span class="o">=&lt;/span>nginx:stable-alpine
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">kubectl expose deployment nginx --port&lt;span class="o">=&lt;/span>&lt;span class="m">80&lt;/span> --type&lt;span class="o">=&lt;/span>ClusterIP
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">kubectl get pods,svc,deploy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">kubectl port-forward svc/nginx 8080:80&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="kubectl-核心指令">kubectl 核心指令&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>資源生命週期：apply / create / delete / get / describe / logs / exec&lt;/li>
&lt;li>Rolling update：set image / rollout status / rollout undo&lt;/li>
&lt;li>Debug：events / port-forward / cp / top&lt;/li>
&lt;li>對應指令範例：&lt;code>kubectl get pods -A&lt;/code>、&lt;code>kubectl describe pod &amp;lt;name&amp;gt;&lt;/code>、&lt;code>kubectl logs -f&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="workload-設計">Workload 設計&lt;/h3>
&lt;p>Pod lifecycle 是 K8s 的核心抽象。子議題：&lt;/p></description><content:encoded><![CDATA[<p>Kubernetes 是 container orchestration 事實標準、承擔三個責任：workload lifecycle（pod / deployment / probe / rolling update）、cluster networking（service / ingress / DNS）、resource scheduling（resource limit / QoS / autoscaling）。設計取捨偏向「declarative + control loop + extensible」、是 cloud-native 生態的核心抽象。可自管或用 cloud managed（GKE / EKS / AKS）。</p>
<p>對「多服務多實例 container orchestration、需要 rolling update / blue-green / canary、跨雲 / 跨環境統一抽象」這條路徑、Kubernetes 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 kubectl 部署 Deployment + Service、配置 probe / resource limit</li>
<li>設計 rolling update / pod disruption budget 避免服務中斷</li>
<li>選 Ingress controller（nginx / traefik / GLBC / ALB Controller）</li>
<li>看懂 pod stuck / probe fail / OOMKilled / drain timeout 訊號</li>
<li>評估 managed（GKE / EKS / AKS）vs 自管 vs Operator 進階場景</li>
</ol>
<h2 id="最短路徑5-分鐘把-kubernetes-跑起來">最短路徑：5 分鐘把 Kubernetes 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 本機跑 kind（需先安裝 kind + docker）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">kind create cluster --name dev
</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. 部署 Deployment + Service</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">kubectl create deployment nginx --image<span class="o">=</span>nginx:stable-alpine
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">kubectl expose deployment nginx --port<span class="o">=</span><span class="m">80</span> --type<span class="o">=</span>ClusterIP
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 3. 驗證</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">kubectl get pods,svc,deploy
</span></span><span class="line"><span class="ln">10</span><span class="cl">kubectl port-forward svc/nginx 8080:80</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="kubectl-核心指令">kubectl 核心指令</h3>
<p>子議題：</p>
<ul>
<li>資源生命週期：apply / create / delete / get / describe / logs / exec</li>
<li>Rolling update：set image / rollout status / rollout undo</li>
<li>Debug：events / port-forward / cp / top</li>
<li>對應指令範例：<code>kubectl get pods -A</code>、<code>kubectl describe pod &lt;name&gt;</code>、<code>kubectl logs -f</code></li>
</ul>
<h3 id="workload-設計">Workload 設計</h3>
<p>Pod lifecycle 是 K8s 的核心抽象。子議題：</p>
<ul>
<li>Deployment（stateless）/ StatefulSet（stateful）/ DaemonSet（per-node）/ Job / CronJob</li>
<li>Pod 多 container（sidecar / init container）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 K8s deployment</a></li>
</ul>
<h3 id="probe--resource-limit--qos">Probe / Resource limit / QoS</h3>
<p>子議題：</p>
<ul>
<li>Liveness（活著嗎）/ Readiness（接流量嗎）/ Startup（啟動完了嗎）— 三 probe 各自責任</li>
<li>Resource limit（requests / limits）+ QoS class（Guaranteed / Burstable / BestEffort）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform lifecycle contract</a></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="rolling-update--disruption-budget">Rolling update / disruption budget</h3>
<p>對應案例 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：cutover without drain</a>。子議題：</p>
<ul>
<li>maxSurge / maxUnavailable 配置</li>
<li>PodDisruptionBudget 限制 voluntary disruption</li>
<li>Preemption / priority class</li>
</ul>
<h3 id="ingress--service-mesh-integration">Ingress / Service mesh integration</h3>
<p>子議題：</p>
<ul>
<li>Ingress controller 選擇（<a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a> / <a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a> / ALB Controller）</li>
<li>Gateway API（next gen Ingress）</li>
<li>Service mesh integration（<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a>-based Istio / Linkerd）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio</a></li>
</ul>
<h3 id="operator-pattern--crd">Operator pattern / CRD</h3>
<p>子議題：</p>
<ul>
<li>CRD（CustomResourceDefinition）+ Controller 模式</li>
<li>Operator framework（OperatorSDK / kubebuilder）</li>
<li>常見 Operator：Prometheus / Cert-manager / Argo CD</li>
</ul>
<h3 id="managed-vs-self-managed">Managed vs self-managed</h3>
<p>對應案例 <a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a>、<a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast EKS</a>、<a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s</a>、<a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4 Mobileye EKS</a>、<a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro EKS</a>。子議題：</p>
<ul>
<li>Self-managed（kubeadm / Cluster API）的 control plane 維運成本</li>
<li>Managed（GKE / EKS / AKS）的限制（版本鎖定 / managed addon）</li>
<li>遷移路徑跟回退設計</li>
</ul>
<h3 id="multi-cluster--federation">Multi-cluster / Federation</h3>
<p>子議題：</p>
<ul>
<li>Federation v2 / Cluster API multi-cluster</li>
<li>Cross-cluster service mesh（Istio multi-cluster）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6 Airbnb cluster scaling</a></li>
</ul>
<h3 id="cluster-autoscaling">Cluster autoscaling</h3>
<p>子議題：</p>
<ul>
<li>Horizontal Pod Autoscaler / Vertical Pod Autoscaler</li>
<li>Cluster Autoscaler / Karpenter</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a> 對照</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="pod-stuckpending--crashloopbackoff">Pod stuck（Pending / CrashLoopBackOff）</h3>
<p>操作原則：先 <code>kubectl describe pod</code> 看 events、再 <code>kubectl logs</code> 看 container 訊息。</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">kubectl describe pod &lt;name&gt;           <span class="c1"># 看 Events 段的 scheduling / pull / probe 訊息</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">kubectl logs &lt;name&gt; --previous        <span class="c1"># 看 crash 前一輪的 container log</span></span></span></code></pre></div><p>判讀路徑：Pending → resource 不足 / nodeSelector 不匹配；CrashLoopBackOff → exit code + log 找原因。</p>
<h3 id="probe-failure-造成不停-restart">Probe failure 造成不停 restart</h3>
<p>操作原則：probe path / initial delay / timeout 配置錯。判讀：<code>describe pod</code> 看 probe events。</p>
<h3 id="oomkilled">OOMKilled</h3>
<p>操作原則：memory limit 太低、container 被殺。判讀：<code>describe pod</code> 看 last state reason。修法：raise limit 或優化 application memory。</p>
<h3 id="rolling-update-stuck">Rolling update stuck</h3>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>。判讀路徑：新 pod 起不來 → readiness 失敗 → 舊 pod 不下線 → 卡住。</p>
<h3 id="drain-timeout">Drain timeout</h3>
<p>操作原則：<code>kubectl drain</code> 失敗、PDB 限制太緊。判讀：<code>kubectl describe pdb</code>。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單機服務（VM / bare metal）</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd</a></td>
      </tr>
      <tr>
          <td>Local dev / CI</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a> Compose</td>
      </tr>
      <tr>
          <td>AWS managed runtime（不要 K8s）</td>
          <td>ECS / Fargate</td>
      </tr>
      <tr>
          <td>極簡 PaaS</td>
          <td>Cloud Run / Heroku / Fly.io</td>
      </tr>
      <tr>
          <td>替代 orchestrator</td>
          <td>Nomad / Rancher</td>
      </tr>
      <tr>
          <td>Edge / IoT 場景</td>
          <td>K3s / MicroK8s</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 kubectl 指令 reference</li>
<li>YAML manifest 完整 schema</li>
<li>各 Operator 細節</li>
<li>各語言 client-go API</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>自管 K8s 遷 managed、零停機切流</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast EKS</a></td>
          <td>多團隊異質集群整併到單一控制面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s</a></td>
          <td>平台重置不中斷產品的能力遷移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4 Mobileye EKS</a></td>
          <td>大規模 workload 分批遷 EKS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro EKS</a></td>
          <td>Managed K8s 跟團隊維運模型對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6 Airbnb cluster scaling</a></td>
          <td>手動擴縮 → 自動化容量治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio</a></td>
          <td>Service mesh 升級分批治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：cutover without drain</a></td>
          <td>Rolling update / drain 沒做的傷</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小型 systemd → 中型 K8s → 大型 multi-cluster</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 K8s deployment</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a>、<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></li>
<li>下游能力：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6 reliability</a>（release gate）、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a></li>
</ul>
]]></content:encoded></item><item><title>5.1 container 與 runtime</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/</guid><description>&lt;p>容器執行環境（container runtime）的核心責任是把應用執行環境做成可重現、可限制、可觀測的交付單位。它是部署可靠性的起點——後續的 probe、canary、rollback 都假設 runtime 產物行為可預測。&lt;/p>
&lt;h2 id="image-與建置責任">image 與建置責任&lt;/h2>
&lt;p>image 的責任是固定依賴、執行入口與檔案結構，讓同一版本在不同環境行為一致。建置流程要回答三件事：基底映像是否可維護、建置產物是否可追溯、敏感資訊是否被隔離。&lt;/p>
&lt;p>映像層數、套件來源、編譯參數都會影響啟動時間與安全邊界。部署策略在後面才有效，前提是 runtime 產物本身可預測。&lt;/p>
&lt;h3 id="基底映像選擇">基底映像選擇&lt;/h3>
&lt;p>基底映像（base image）決定 image 的安全維護基線與啟動時體積。選擇的核心取捨是體積 / 啟動速度與除錯便利性：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>語言官方映像&lt;/strong>（&lt;code>python:3.12&lt;/code>、&lt;code>node:20&lt;/code>）：套件齊全、除錯方便，但體積大（通常 800MB+）、攻擊面廣。適合開發環境與 CI。&lt;/li>
&lt;li>&lt;strong>slim / alpine 變體&lt;/strong>（&lt;code>python:3.12-slim&lt;/code>、&lt;code>node:20-alpine&lt;/code>）：體積壓到 100-200MB、啟動快、攻擊面小。代價是缺少除錯工具（strace、curl、dig），生產事故時 exec 進容器排查會受限。Alpine 用 musl libc 而非 glibc，某些 C extension 需要額外處理。&lt;/li>
&lt;li>&lt;strong>distroless&lt;/strong>（&lt;code>gcr.io/distroless/base&lt;/code>）：只包含 runtime 必要檔案，無 shell、無套件管理器。攻擊面最小，但除錯只能靠 ephemeral debug container 或外部觀測。適合安全要求高且觀測基礎建設完備的生產環境。&lt;/li>
&lt;li>&lt;strong>自建基底&lt;/strong>：組織統一維護的基底映像，可以固定安全基線、預裝觀測 agent、統一 timezone / locale。代價是基底維護本身是持續工作，版本更新節奏要有明確 owner。&lt;/li>
&lt;/ul>
&lt;p>選完基底後要確認兩件事：upstream 的更新節奏是否可追蹤（CVE 修補從上游到自家 image 的時間），以及團隊是否有能力在基底更新後快速重建並驗證所有服務 image。&lt;/p>
&lt;h3 id="建置可重現性">建置可重現性&lt;/h3>
&lt;p>同一份 source code 在不同時間建置出不同 image，會讓 rollback 的假設失效——「回退到上一版」回退的是哪一版，取決於當時 build 環境的狀態。&lt;/p>
&lt;p>可重現建置的關鍵實踐：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>鎖定依賴版本&lt;/strong>：&lt;code>go.sum&lt;/code>、&lt;code>package-lock.json&lt;/code>、&lt;code>poetry.lock&lt;/code> 要進 git。依賴解析在建置時不從 registry 重新 resolve。&lt;/li>
&lt;li>&lt;strong>Multi-stage build&lt;/strong>：把建置環境（compiler、dev dependencies）和執行環境分開。最終 image 只包含 runtime 必要檔案，體積小且攻擊面收窄。&lt;/li>
&lt;li>&lt;strong>避免 image 中殘留敏感資訊&lt;/strong>：build arg、環境變數、中間層都可能殘留 secret。secret 不進 Dockerfile，用 runtime mount 或 secret manager 注入。&lt;/li>
&lt;li>&lt;strong>image 標記策略&lt;/strong>：&lt;code>latest&lt;/code> tag 不可重現——同一個 tag 指向的 image 會隨時間改變。用 git commit SHA 或語意版本號標記，讓每個 tag 指向唯一 image。&lt;/li>
&lt;/ol>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration&lt;/a>：揭露「跨平台遷移本質是能力遷移」。遷移到新平台時，CI/CD pipeline 可能換了 runner 環境、換了 registry——建置可重現性的前提是依賴鎖定與 multi-stage build 本身不依賴特定 CI 環境。&lt;/p>
&lt;h2 id="entrypoint-與啟動行為">entrypoint 與啟動行為&lt;/h2>
&lt;p>entrypoint/command 的責任是定義容器如何啟動與退出。啟動流程應顯式處理初始化步驟、配置載入、依賴檢查與失敗退出。退出流程應處理信號中斷、在途請求收斂與資源釋放。&lt;/p>
&lt;p>若啟動行為隱藏在 shell script 且無可觀測訊號，部署平台很難判斷 readiness 與失敗原因。&lt;/p>
&lt;h3 id="pid-1-與信號處理">PID 1 與信號處理&lt;/h3>
&lt;p>容器內 PID 1 有特殊語意：它是 init process，負責接收平台送來的 SIGTERM / SIGINT 並轉發給子進程。PID 1 的問題出在三種情境：&lt;/p>
&lt;p>&lt;strong>Shell 作為 PID 1&lt;/strong>：&lt;code>ENTRYPOINT [&amp;quot;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;java -jar app.jar&amp;quot;]&lt;/code> 讓 sh 成為 PID 1。SIGTERM 送到 sh、sh 預設不轉發、java 進程收不到信號、等到 terminationGracePeriodSeconds 到期後被 SIGKILL 強殺。修法是用 &lt;code>exec&lt;/code> 或直接用 exec form：&lt;code>ENTRYPOINT [&amp;quot;java&amp;quot;, &amp;quot;-jar&amp;quot;, &amp;quot;app.jar&amp;quot;]&lt;/code>。&lt;/p>
&lt;p>&lt;strong>多進程容器&lt;/strong>：一個容器跑多個進程時，PID 1 要負責信號轉發與子進程回收（zombie reaping）。如果 PID 1 不做 wait()，結束的子進程會變成 zombie。解法是用 tini 或 dumb-init 作為輕量 init，或在 Kubernetes 設 &lt;code>shareProcessNamespace: true&lt;/code> 讓 kubelet 處理。&lt;/p></description><content:encoded><![CDATA[<p>容器執行環境（container runtime）的核心責任是把應用執行環境做成可重現、可限制、可觀測的交付單位。它是部署可靠性的起點——後續的 probe、canary、rollback 都假設 runtime 產物行為可預測。</p>
<h2 id="image-與建置責任">image 與建置責任</h2>
<p>image 的責任是固定依賴、執行入口與檔案結構，讓同一版本在不同環境行為一致。建置流程要回答三件事：基底映像是否可維護、建置產物是否可追溯、敏感資訊是否被隔離。</p>
<p>映像層數、套件來源、編譯參數都會影響啟動時間與安全邊界。部署策略在後面才有效，前提是 runtime 產物本身可預測。</p>
<h3 id="基底映像選擇">基底映像選擇</h3>
<p>基底映像（base image）決定 image 的安全維護基線與啟動時體積。選擇的核心取捨是體積 / 啟動速度與除錯便利性：</p>
<ul>
<li><strong>語言官方映像</strong>（<code>python:3.12</code>、<code>node:20</code>）：套件齊全、除錯方便，但體積大（通常 800MB+）、攻擊面廣。適合開發環境與 CI。</li>
<li><strong>slim / alpine 變體</strong>（<code>python:3.12-slim</code>、<code>node:20-alpine</code>）：體積壓到 100-200MB、啟動快、攻擊面小。代價是缺少除錯工具（strace、curl、dig），生產事故時 exec 進容器排查會受限。Alpine 用 musl libc 而非 glibc，某些 C extension 需要額外處理。</li>
<li><strong>distroless</strong>（<code>gcr.io/distroless/base</code>）：只包含 runtime 必要檔案，無 shell、無套件管理器。攻擊面最小，但除錯只能靠 ephemeral debug container 或外部觀測。適合安全要求高且觀測基礎建設完備的生產環境。</li>
<li><strong>自建基底</strong>：組織統一維護的基底映像，可以固定安全基線、預裝觀測 agent、統一 timezone / locale。代價是基底維護本身是持續工作，版本更新節奏要有明確 owner。</li>
</ul>
<p>選完基底後要確認兩件事：upstream 的更新節奏是否可追蹤（CVE 修補從上游到自家 image 的時間），以及團隊是否有能力在基底更新後快速重建並驗證所有服務 image。</p>
<h3 id="建置可重現性">建置可重現性</h3>
<p>同一份 source code 在不同時間建置出不同 image，會讓 rollback 的假設失效——「回退到上一版」回退的是哪一版，取決於當時 build 環境的狀態。</p>
<p>可重現建置的關鍵實踐：</p>
<ol>
<li><strong>鎖定依賴版本</strong>：<code>go.sum</code>、<code>package-lock.json</code>、<code>poetry.lock</code> 要進 git。依賴解析在建置時不從 registry 重新 resolve。</li>
<li><strong>Multi-stage build</strong>：把建置環境（compiler、dev dependencies）和執行環境分開。最終 image 只包含 runtime 必要檔案，體積小且攻擊面收窄。</li>
<li><strong>避免 image 中殘留敏感資訊</strong>：build arg、環境變數、中間層都可能殘留 secret。secret 不進 Dockerfile，用 runtime mount 或 secret manager 注入。</li>
<li><strong>image 標記策略</strong>：<code>latest</code> tag 不可重現——同一個 tag 指向的 image 會隨時間改變。用 git commit SHA 或語意版本號標記，讓每個 tag 指向唯一 image。</li>
</ol>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration</a>：揭露「跨平台遷移本質是能力遷移」。遷移到新平台時，CI/CD pipeline 可能換了 runner 環境、換了 registry——建置可重現性的前提是依賴鎖定與 multi-stage build 本身不依賴特定 CI 環境。</p>
<h2 id="entrypoint-與啟動行為">entrypoint 與啟動行為</h2>
<p>entrypoint/command 的責任是定義容器如何啟動與退出。啟動流程應顯式處理初始化步驟、配置載入、依賴檢查與失敗退出。退出流程應處理信號中斷、在途請求收斂與資源釋放。</p>
<p>若啟動行為隱藏在 shell script 且無可觀測訊號，部署平台很難判斷 readiness 與失敗原因。</p>
<h3 id="pid-1-與信號處理">PID 1 與信號處理</h3>
<p>容器內 PID 1 有特殊語意：它是 init process，負責接收平台送來的 SIGTERM / SIGINT 並轉發給子進程。PID 1 的問題出在三種情境：</p>
<p><strong>Shell 作為 PID 1</strong>：<code>ENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;java -jar app.jar&quot;]</code> 讓 sh 成為 PID 1。SIGTERM 送到 sh、sh 預設不轉發、java 進程收不到信號、等到 terminationGracePeriodSeconds 到期後被 SIGKILL 強殺。修法是用 <code>exec</code> 或直接用 exec form：<code>ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code>。</p>
<p><strong>多進程容器</strong>：一個容器跑多個進程時，PID 1 要負責信號轉發與子進程回收（zombie reaping）。如果 PID 1 不做 wait()，結束的子進程會變成 zombie。解法是用 tini 或 dumb-init 作為輕量 init，或在 Kubernetes 設 <code>shareProcessNamespace: true</code> 讓 kubelet 處理。</p>
<p><strong>啟動腳本的信號遮蔽</strong>：entrypoint script 在初始化階段（下載 config、等依賴就緒）捕捉 SIGTERM 做清理，但如果清理邏輯卡住，整個 shutdown 會被阻塞。啟動腳本的 trap handler 要有 timeout，避免把 graceful shutdown 變成 ungraceful hang。</p>
<h3 id="啟動時間對部署策略的影響">啟動時間對部署策略的影響</h3>
<p>啟動時間直接影響 rollout 的最短觀察窗。一個啟動需 60 秒的服務，rollout 每批至少要等 60 秒 + 觀察窗口才能確認新版本穩定。啟動時間的組成與壓縮策略見 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
<p>image 體積也影響啟動時間——image pull 在冷啟動（節點上沒有這個 image 的快取）時占啟動時間的顯著比例。1GB image 在 100Mbps 網路下需要 ~80 秒 pull。壓縮 image 體積同時改善啟動速度與節省 registry 頻寬。</p>
<h2 id="resource-limit">resource limit</h2>
<p>CPU/memory <a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a> 隔離資源競爭並保護叢集穩態。限制過低會導致頻繁節流與重啟，過高會壓縮同節點容量並放大鄰近工作負載風險。</p>
<p>限制設計要依服務流量型態與 GC/執行時特性調整，並與 autoscaling、rollout 批次策略一起評估。</p>
<h3 id="cpu-request-與-limit-的設定策略">CPU request 與 limit 的設定策略</h3>
<p>CPU 限制有兩個參數：request（排程保證）與 limit（硬上限）。兩者的關係決定服務在負載變動下的行為：</p>
<ul>
<li><strong>request = limit</strong>（guaranteed QoS）：CPU 用量穩定可預測，不會被 throttle 也不會超用。代價是無法在閒時借用節點剩餘 CPU。適合延遲敏感的 API 服務。</li>
<li><strong>request &lt; limit</strong>（burstable QoS）：平時用 request 保證的份額，高峰時可用到 limit。代價是當節點 CPU 競爭激烈時，所有 burstable pod 同時被 throttle，延遲會一起劣化。適合批次處理或對延遲要求不高的服務。</li>
<li><strong>不設 limit</strong>（只設 request）：服務可用到節點全部剩餘 CPU。Kubernetes 社群近年傾向這個做法——CPU throttle 常比 CPU contention 更難排查。代價是需要良好的觀測來偵測 noisy neighbor。</li>
</ul>
<h3 id="memory-limit-與-oom-的判讀">Memory limit 與 OOM 的判讀</h3>
<p>memory limit 是硬邊界——超過就 OOM kill，不走 graceful shutdown。OOM kill 的判讀分兩種情境：</p>
<p><strong>真正的 memory leak</strong>：記憶體使用量隨時間單調上升，GC 無法回收。修法在程式碼層。memory limit 只是延後問題爆發，不是解法。</p>
<p><strong>memory limit 設太低</strong>：服務在高峰流量下的正常記憶體使用超過 limit。常見於 JVM 服務——JVM heap + metaspace + native memory + thread stack 的總和超出 container memory limit。設 limit 時要用「峰值實際使用 + headroom」而非「平均使用」。</p>
<p>GC-based runtime（JVM、.NET、Go）要注意 container-aware memory 設定。早期 JVM 不認 cgroup memory limit，會按宿主機記憶體計算 heap 大小，導致 heap 配置超過 container limit。現代 JVM（Java 10+）預設啟用 container awareness（<code>-XX:+UseContainerSupport</code>），Go runtime 1.19+ 支援 <code>GOMEMLIMIT</code>。</p>
<h3 id="資源設定與-autoscaling-的協同">資源設定與 autoscaling 的協同</h3>
<p>resource request 同時決定 HPA（Horizontal Pod Autoscaler）的觸發基線。request 設太高時，CPU utilization % 會偏低，HPA 不會觸發擴容，導致服務在真正需要擴容前已經出現延遲。request 設太低時，utilization % 容易衝高，HPA 頻繁擴容，造成 pod 數量抖動。</p>
<p>穩定做法是先在 staging 環境跑負載測試確認服務的實際資源消耗曲線，再以 p90 負載的 CPU / memory 使用作為 request 基線。</p>
<h2 id="runtime-config">runtime config</h2>
<p>環境差異要顯式化才能追蹤——<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a> 承擔這個責任。配置來源、版本、更新節奏都應可追蹤。高風險設定需配合 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 策略，避免同批大規模變更。</p>
<p>runtime 配置與映像版本要保留相容窗口，讓部署與回退可分步進行。</p>
<h3 id="配置注入方式與取捨">配置注入方式與取捨</h3>
<p>配置注入容器有三條路徑，各自有不同的版本追蹤與更新語意：</p>
<table>
  <thead>
      <tr>
          <th>注入方式</th>
          <th>版本追蹤</th>
          <th>更新行為</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>環境變數</td>
          <td>跟 deployment spec 一起版控</td>
          <td>需要 pod restart 才生效</td>
          <td>啟動時固定的設定（DB URL、port）</td>
      </tr>
      <tr>
          <td>ConfigMap mount</td>
          <td>ConfigMap 版本</td>
          <td>自動更新（kubelet sync period 內）</td>
          <td>需要動態更新的非敏感設定</td>
      </tr>
      <tr>
          <td>Secret mount</td>
          <td>Secret 版本</td>
          <td>自動更新（同 ConfigMap）</td>
          <td>credential、cert、API key</td>
      </tr>
      <tr>
          <td>外部 config store</td>
          <td>config store 內版本</td>
          <td>應用主動拉取或 sidecar push</td>
          <td>feature flag、複雜設定邏輯</td>
      </tr>
  </tbody>
</table>
<p>環境變數最簡單但更新需要 restart。ConfigMap mount 可以動態更新但應用要能偵測檔案變化並 reload。外部 config store（Consul KV、AWS AppConfig、Feature Flag service）最靈活但引入了額外依賴。</p>
<p>設定變更跟 image 變更走不同路徑時，要確保兩者的版本可以交叉相容。版本 v2 的 image 搭版本 A 的 config 能跑、版本 v1 的 image 搭版本 B 的 config 也能跑——rollback image 但 config 沒回退、或 rollback config 但 image 沒回退的情境下、服務不應崩潰。這個相容窗口的設計責任見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Config Boundary</a>。</p>
<h2 id="遷移期的-runtime-穩定性">遷移期的 Runtime 穩定性</h2>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro managed EKS 遷移</a>：揭露「平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略」。遷移到 managed 平台後，runtime 層面的變化包含 container runtime 版本（containerd vs Docker shim）、node OS、storage driver、network plugin。這些變化可能改變 image pull 速度、filesystem 行為、DNS 解析路徑。</p>
<p>遷移前後的 runtime 驗證應包含：</p>
<ol>
<li><strong>image pull 時間比較</strong>：新 registry / 新 node 的 pull 速度是否在 startup timeout 內。</li>
<li><strong>filesystem 行為</strong>：log 寫入路徑、tmp 目錄、volume mount 行為在新 runtime 下是否一致。</li>
<li><strong>DNS 解析</strong>：新叢集的 CoreDNS / node-local DNS 設定是否影響服務的依賴連線建立速度。</li>
<li><strong>resource 行為</strong>：新 node type 的 CPU 架構（x86 vs ARM）、memory page size 是否影響服務性能特性。</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新版本容器啟動時間顯著增加</td>
          <td>image 體積或初始化步驟膨脹</td>
          <td>優化映像層、拆分初始化流程</td>
      </tr>
      <tr>
          <td>rollout 初期出現 OOM/CPU throttle</td>
          <td>resource limit 與實際負載不匹配</td>
          <td>重設 request/limit、調整併發與批次</td>
      </tr>
      <tr>
          <td>配置變更後特定環境異常</td>
          <td>runtime config 管理不一致</td>
          <td>統一配置來源、補版本追蹤與差異檢查</td>
      </tr>
      <tr>
          <td>容器停止時請求中斷率上升</td>
          <td>signal/drain 協調不足</td>
          <td>補 shutdown hook、對齊 termination 流程</td>
      </tr>
      <tr>
          <td>同版本在不同節點行為差異大</td>
          <td>runtime 依賴未固定或環境漂移</td>
          <td>收斂基底映像、鎖定依賴與建置流程</td>
      </tr>
      <tr>
          <td>JVM 服務 OOM 但 heap 未用滿</td>
          <td>native memory / metaspace 超出 limit</td>
          <td>調整 MaxMetaspaceSize、限制 thread 數</td>
      </tr>
      <tr>
          <td>冷啟動節點上服務啟動超慢</td>
          <td>image pull 時間在啟動時間中占比高</td>
          <td>壓縮 image 體積、啟用 image cache</td>
      </tr>
      <tr>
          <td>rollback 後行為跟上次部署不同</td>
          <td>建置不可重現、tag 覆蓋</td>
          <td>改用 commit SHA 標記、鎖定依賴版本</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>Container 常被簡化成「打包完就好」的步驟，結果是部署風險被後移到 rollout 階段。runtime 產物穩定性不足時，後續 probe、canary、rollback 都只能被動補救。</p>
<p>把資源限制設成平台預設值，也常造成高峰期不穩。限制應反映服務真實耗用模式，不應只追求表面資源利用率。</p>
<p>把 <code>latest</code> tag 當成版本標記，會讓 rollback 指向無法預測的 image。image tag 在 registry 上是 mutable——同一個 tag 可以被覆蓋指向新 image。用 immutable tag（commit SHA、content digest）才能保證 rollback 的確定性。</p>
<p>把所有配置都用環境變數注入，會讓設定變更跟 image 部署綁在一起。需要動態更新的設定（feature flag、rate limit 閾值）應該用 ConfigMap mount 或外部 config store，讓設定變更不需要 pod restart。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>runtime 穩定性可用 <a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift：self-managed K8s -&gt; EKS</a> 回寫。先看遷移期內啟動行為與資源限制如何影響切流，再對照本章檢查 image、entrypoint、limit 與 config 相容窗口。這個案例主要支撐的是「執行環境可重現性」判讀——遷移到新叢集時，image 不變但 runtime 環境變了（node OS、container runtime 版本、network plugin），runtime 穩定性的前提是 image 本身不依賴特定宿主環境的行為。</p>
<p><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro managed EKS 遷移</a> 從另一個角度支撐：managed 平台接管 runtime 基礎設施後，container runtime 版本升級由平台控制，團隊要能驗證自家 image 在新 runtime 版本下行為一致。</p>
<p>若同版容器在不同節點出現分歧行為，先追建置來源與 runtime config 版本鏈，確認是依賴漂移還是環境漂移，再把關鍵證據收斂到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。不直接支撐 service discovery TTL 或 queue replay 邏輯；若根因在定位鏈路或重播流程，應轉到 5.4 或 3.4。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 5.2 的交接：部署批次與探針策略回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes 部署策略</a>。</li>
<li>與 5.3 的交接：流量進出與連線收斂回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.6 的交接：startup / readiness / drain 的生命週期定義回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 4.20 的交接：啟動與資源證據回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.8 的交接：放行與回退條件回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 7.3 的交接：image 安全基線與攻擊面回到 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 runtime 行為接到部署收斂，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a>。要看切流與退場條件，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看 runtime 層的生命週期如何被平台表達，接著讀 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
]]></content:encoded></item><item><title>6.1 graceful shutdown 與 signal handling</title><link>https://tarrragon.github.io/blog/go-advanced/06-production-operations/graceful-shutdown/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/06-production-operations/graceful-shutdown/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Graceful shutdown&lt;/a> 的核心目標是服務收到停止訊號後，不再接受新工作，並給既有工作一段時間完成或清理。Go 服務通常用 signal、root context、&lt;code>http.Server.Shutdown&lt;/code>、worker context 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 串起停止流程。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>把 OS signal 轉成 root context 取消&lt;/li>
&lt;li>用 &lt;code>http.Server.Shutdown&lt;/code> 停止接受新 request&lt;/li>
&lt;li>讓 worker、hub、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> pump 觀察同一個停止訊號&lt;/li>
&lt;li>設計 shutdown timeout 與強制退出邊界&lt;/li>
&lt;li>測試 server 與 worker 的停止流程&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察直接結束-process-會留下不確定狀態">【觀察】直接結束 process 會留下不確定狀態&lt;/h2>
&lt;p>Shutdown 的核心風險是停止流程不明確。服務可能正在處理 request、WebSocket client 仍在線、worker 正在寫資料、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> message 尚未 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack&lt;/a>、diagnostics 還以為服務可接流量。&lt;/p>
&lt;p>不完整停止常見後果：&lt;/p>
&lt;ul>
&lt;li>新 request 在服務即將關閉時仍被接受。&lt;/li>
&lt;li>WebSocket client 沒收到 close，server 端 goroutine 殘留。&lt;/li>
&lt;li>背景 worker 寫到一半被中斷。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 還是 200，負載平衡器繼續送流量。&lt;/li>
&lt;li>測試結束後留下 goroutine 或開放 port。&lt;/li>
&lt;/ul>
&lt;p>Graceful shutdown 是讓停止策略可預期。&lt;/p>
&lt;h2 id="判讀shutdown-是多階段流程">【判讀】shutdown 是多階段流程&lt;/h2>
&lt;p>Graceful shutdown 的核心流程是先停止接新工作，再讓既有工作收尾，最後釋放資源。&lt;/p>
&lt;p>建議順序：&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">receive SIGINT/SIGTERM
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> │
&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">cancel root context
&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"> ├── readiness becomes false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> ├── HTTP server stops accepting new requests
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> ├── workers stop consuming new jobs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> ├── WebSocket hub unregisters clients
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> └── diagnostics/log records shutdown reason
&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;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">wait within timeout
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">process exits&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不同服務會有不同細節，但核心不變：停止訊號要集中，元件各自完成自己的 cleanup，整體流程要有 timeout。&lt;/p>
&lt;h2 id="執行signal-轉成-root-context">【執行】signal 轉成 root context&lt;/h2>
&lt;p>Signal handling 的核心責任是把作業系統訊號轉成應用程式可理解的取消訊號。Go 1.16 之後可以使用 &lt;code>signal.NotifyContext&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">stop&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">signal&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NotifyContext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="nx">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Interrupt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">syscall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SIGTERM&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nf">stop&lt;/span>&lt;span class="p">()&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;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ctx&lt;/code> 是 root context。HTTP server、worker、hub、diagnostics 都應從它派生出自己的 lifecycle，而不是每個元件各自監聽 signal。&lt;/p>
&lt;p>Signal handler 不應放大量清理邏輯。它只負責發出停止意圖；實際清理由各元件在自己的 ownership 邊界內完成。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Graceful shutdown</a> 的核心目標是服務收到停止訊號後，不再接受新工作，並給既有工作一段時間完成或清理。Go 服務通常用 signal、root context、<code>http.Server.Shutdown</code>、worker context 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 串起停止流程。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>把 OS signal 轉成 root context 取消</li>
<li>用 <code>http.Server.Shutdown</code> 停止接受新 request</li>
<li>讓 worker、hub、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> pump 觀察同一個停止訊號</li>
<li>設計 shutdown timeout 與強制退出邊界</li>
<li>測試 server 與 worker 的停止流程</li>
</ol>
<hr>
<h2 id="觀察直接結束-process-會留下不確定狀態">【觀察】直接結束 process 會留下不確定狀態</h2>
<p>Shutdown 的核心風險是停止流程不明確。服務可能正在處理 request、WebSocket client 仍在線、worker 正在寫資料、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> message 尚未 <a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack</a>、diagnostics 還以為服務可接流量。</p>
<p>不完整停止常見後果：</p>
<ul>
<li>新 request 在服務即將關閉時仍被接受。</li>
<li>WebSocket client 沒收到 close，server 端 goroutine 殘留。</li>
<li>背景 worker 寫到一半被中斷。</li>
<li><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 還是 200，負載平衡器繼續送流量。</li>
<li>測試結束後留下 goroutine 或開放 port。</li>
</ul>
<p>Graceful shutdown 是讓停止策略可預期。</p>
<h2 id="判讀shutdown-是多階段流程">【判讀】shutdown 是多階段流程</h2>
<p>Graceful shutdown 的核心流程是先停止接新工作，再讓既有工作收尾，最後釋放資源。</p>
<p>建議順序：</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">receive SIGINT/SIGTERM
</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">        ▼
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">cancel root context
</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">        ├── readiness becomes false
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        ├── HTTP server stops accepting new requests
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        ├── workers stop consuming new jobs
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        ├── WebSocket hub unregisters clients
</span></span><span class="line"><span class="ln">10</span><span class="cl">        └── diagnostics/log records shutdown reason
</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><span class="line"><span class="ln">13</span><span class="cl">wait within timeout
</span></span><span class="line"><span class="ln">14</span><span class="cl">        │
</span></span><span class="line"><span class="ln">15</span><span class="cl">        ▼
</span></span><span class="line"><span class="ln">16</span><span class="cl">process exits</span></span></code></pre></div><p>不同服務會有不同細節，但核心不變：停止訊號要集中，元件各自完成自己的 cleanup，整體流程要有 timeout。</p>
<h2 id="執行signal-轉成-root-context">【執行】signal 轉成 root context</h2>
<p>Signal handling 的核心責任是把作業系統訊號轉成應用程式可理解的取消訊號。Go 1.16 之後可以使用 <code>signal.NotifyContext</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">ctx</span><span class="p">,</span> <span class="nx">stop</span> <span class="o">:=</span> <span class="nx">signal</span><span class="p">.</span><span class="nf">NotifyContext</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">os</span><span class="p">.</span><span class="nx">Interrupt</span><span class="p">,</span> <span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGTERM</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">defer</span> <span class="nf">stop</span><span class="p">()</span>
</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">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">run</span><span class="p">(</span><span class="nx">ctx</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>ctx</code> 是 root context。HTTP server、worker、hub、diagnostics 都應從它派生出自己的 lifecycle，而不是每個元件各自監聽 signal。</p>
<p>Signal handler 不應放大量清理邏輯。它只負責發出停止意圖；實際清理由各元件在自己的 ownership 邊界內完成。</p>
<h2 id="執行http-server-用-shutdown-停止接新-request">【執行】HTTP server 用 Shutdown 停止接新 request</h2>
<p><code>http.Server.Shutdown</code> 的核心行為是停止接受新連線，並等待既有 request 在 timeout 內完成。它比直接 <code>Close</code> 更適合 graceful shutdown。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">RunHTTPServer</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">handler</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">server</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">http</span><span class="p">.</span><span class="nx">Server</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">Addr</span><span class="p">:</span>    <span class="s">&#34;:8080&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">Handler</span><span class="p">:</span> <span class="nx">handler</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <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 class="nx">errCh</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kt">error</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">errCh</span> <span class="o">&lt;-</span> <span class="nx">server</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}()</span>
</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 class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="nx">shutdownCtx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="mi">10</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">return</span> <span class="nx">server</span><span class="p">.</span><span class="nf">Shutdown</span><span class="p">(</span><span class="nx">shutdownCtx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">case</span> <span class="nx">err</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">errCh</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="k">if</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ErrServerClosed</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Shutdown timeout 是必要邊界。沒有 timeout 的 shutdown 可能永遠等待某個卡住 request；timeout 太短則可能讓合理 request 來不及收尾。</p>
<h2 id="策略readiness-應先變成-false">【策略】readiness 應先變成 false</h2>
<p>Readiness 的核心用途是控制服務是否應接新流量。Shutdown 開始後，readiness 應先變成 false，再停止 server 或等待既有工作。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">Lifecycle</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">shuttingDown</span> <span class="nx">atomic</span><span class="p">.</span><span class="nx">Bool</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">l</span> <span class="o">*</span><span class="nx">Lifecycle</span><span class="p">)</span> <span class="nf">BeginShutdown</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">l</span><span class="p">.</span><span class="nx">shuttingDown</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">l</span> <span class="o">*</span><span class="nx">Lifecycle</span><span class="p">)</span> <span class="nf">Ready</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="p">!</span><span class="nx">l</span><span class="p">.</span><span class="nx">shuttingDown</span><span class="p">.</span><span class="nf">Load</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Signal 收到後：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">lifecycle</span><span class="p">.</span><span class="nf">BeginShutdown</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nf">cancel</span><span class="p">()</span></span></span></code></pre></div><p>這讓負載平衡器或監控能知道服務不應再接新流量。Process 還活著，但 readiness 已經反映操作狀態。</p>
<h2 id="執行背景工作要觀察-context">【執行】背景工作要觀察 context</h2>
<p>背景 worker 的核心 shutdown 條件是每個 loop 都能觀察停止訊號。Ticker、queue <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a>、WebSocket hub 都應該有退出路徑。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">RunWorker</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ticker</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">NewTicker</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Minute</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">defer</span> <span class="nx">ticker</span><span class="p">.</span><span class="nf">Stop</span><span class="p">()</span>
</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">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="k">return</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ticker</span><span class="p">.</span><span class="nx">C</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">RunOnce</span><span class="p">(</span><span class="nx">ctx</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">                <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>若 <code>RunOnce</code> 可能執行很久，也應接收 context。否則外層 loop 看到 cancel，內層 I/O 或計算仍可能卡住。</p>
<h2 id="策略websocket-cleanup-要回到-hub-owner">【策略】WebSocket cleanup 要回到 hub owner</h2>
<p>WebSocket shutdown 的核心原則是讓 hub 或 connection manager 統一清理 client。不要讓 signal handler 直接遍歷各種 connection 並隨意 close。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hub</span><span class="p">)</span> <span class="nf">Run</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="nx">h</span><span class="p">.</span><span class="nf">closeAllClients</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">case</span> <span class="nx">client</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">h</span><span class="p">.</span><span class="nx">register</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="nx">h</span><span class="p">.</span><span class="nf">registerClient</span><span class="p">(</span><span class="nx">client</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">case</span> <span class="nx">client</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">h</span><span class="p">.</span><span class="nx">unregister</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nx">h</span><span class="p">.</span><span class="nf">unregisterClient</span><span class="p">(</span><span class="nx">client</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>closeAllClients</code> 應透過 hub 的既有 owner 邏輯關閉 <code>send</code>、移除訂閱、關閉 connection。這延續前面模組的 ownership 原則。</p>
<h2 id="測試shutdown-測試要觀察明確條件">【測試】shutdown 測試要觀察明確條件</h2>
<p>Shutdown 測試的核心是確認停止訊號能讓元件退出，而不是等待固定時間。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestWorkerStopsOnContextCancel</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithCancel</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">done</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{})</span>
</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">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">defer</span> <span class="nb">close</span><span class="p">(</span><span class="nx">done</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">_</span> <span class="p">=</span> <span class="nf">RunWorker</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nf">cancel</span><span class="p">()</span>
</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 class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">done</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">time</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;worker did not stop&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>HTTP server 測試可以啟動 server 後 cancel context，確認 <code>RunHTTPServer</code> 回傳。測試應使用隨機 port 或 <code>httptest.Server</code>，避免固定 port 造成衝突。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理服務內部的 shutdown 順序與 cleanup owner；平台 hook、timeout 與 load balancer 合約，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go-advanced/07-distributed-operations/deployment-contracts/" data-link-title="7.5 Kubernetes、systemd 與 load balancer 合約" data-link-desc="理解部署平台如何影響 Go 服務的 shutdown、health 與資源限制">Go 進階：Kubernetes、systemd 與 load balancer 合約</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 goroutine lifecycle、ticker cleanup 與 platform handoff；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">Go：goroutine：輕量並發工作</a></li>
<li><a href="/blog/go/03-stdlib/defer-cleanup/" data-link-title="3.8 defer 與資源清理" data-link-desc="用 defer 管理 close、unlock、cleanup 與 panic 邊界">Go：defer 與資源清理</a></li>
<li><a href="/blog/go/04-concurrency/select/" data-link-title="4.3 select：同時等待多種事件" data-link-desc="用 select 建立事件迴圈">Go：select：同時等待多種事件</a></li>
<li><a href="/blog/go-advanced/03-runtime-profiling/goroutine-leak/" data-link-title="3.3 goroutine leak 偵測" data-link-desc="判斷背景工作與 client pump 是否正確退出">Go：goroutine leak 偵測</a></li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>Graceful shutdown 是多階段流程：signal 轉成 root context，readiness 先關閉，HTTP server 停止接新 request，worker 和 WebSocket hub 觀察 context 收尾，整體流程受 timeout 保護。停止訊號越集中，元件 ownership 越清楚，服務在部署、測試與本機開發時越不容易留下殘存 goroutine 或未釋放連線。</p>
]]></content:encoded></item><item><title>部署光譜：從 BaaS 到自架的四條路徑</title><link>https://tarrragon.github.io/blog/monitoring/06-commercial-comparison/deployment-spectrum/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/06-commercial-comparison/deployment-spectrum/</guid><description>&lt;p>監控方案的選擇不是「完全自架 Go collector」和「買 Sentry 訂閱」的二元決策。中間存在兩條路徑 — 用 BaaS（Supabase / Firebase）搭出託管版 collector，或用 PaaS（Railway / Fly.io）跑自架 collector 原始碼但不管 server。四條路徑的本質差異在「哪些層自己管、哪些交給平台」。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/monitoring/06-commercial-comparison/self-hosted-vs-commercial/" data-link-title="自架 vs 商業的判斷決策表" data-link-desc="使用者數、網路範圍、功能需求、合規要求四個維度判斷該自架還是用商業方案">自架 vs 商業的判斷決策表&lt;/a>用四個維度（使用者數 / 網路範圍 / 功能需求 / 合規）做二元分流。本章把光譜展開成四條路徑，讓中間的 BaaS 和 PaaS 選項浮現。Backend 選型模組已建立了完整的交付形態光譜（&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">交付形態選型&lt;/a>）和逐能力判斷外包深度的框架（&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">能力級買 vs 建&lt;/a>）。本章把那個框架特化到監控場景。&lt;/p>
&lt;h2 id="四條路徑">四條路徑&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>路徑&lt;/th>
 &lt;th>代表方案&lt;/th>
 &lt;th>Collector 是什麼&lt;/th>
 &lt;th>Storage 是什麼&lt;/th>
 &lt;th>自己管什麼&lt;/th>
 &lt;th>平台管什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>A. 商業監控 SaaS&lt;/td>
 &lt;td>Sentry / Datadog / Firebase Analytics&lt;/td>
 &lt;td>vendor 提供&lt;/td>
 &lt;td>vendor 提供&lt;/td>
 &lt;td>SDK 埋點&lt;/td>
 &lt;td>全部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B. BaaS + Serverless&lt;/td>
 &lt;td>Supabase + Vercel / Cloudflare Workers&lt;/td>
 &lt;td>serverless function（自己寫）&lt;/td>
 &lt;td>managed PostgreSQL（Supabase）&lt;/td>
 &lt;td>collector 邏輯、schema&lt;/td>
 &lt;td>server 維運、DB 維運、TLS、HA&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>C. PaaS&lt;/td>
 &lt;td>Railway / Fly.io / Render&lt;/td>
 &lt;td>Go binary（自架 collector 原始碼）&lt;/td>
 &lt;td>SQLite（同 binary）或 managed DB&lt;/td>
 &lt;td>collector 邏輯、storage&lt;/td>
 &lt;td>server 維運、TLS、deploy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>D. 完全自架&lt;/td>
 &lt;td>VPS + Go binary&lt;/td>
 &lt;td>Go binary&lt;/td>
 &lt;td>SQLite 或自管 PostgreSQL&lt;/td>
 &lt;td>全部&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>路徑 A 和 D 分別是光譜的兩端 — &lt;a href="https://tarrragon.github.io/blog/monitoring/06-commercial-comparison/sentry-deep-dive/" data-link-title="Sentry 深入" data-link-desc="Error tracking &amp;#43; performance monitoring &amp;#43; session replay 的架構 — Sentry 從 error-first 出發如何擴展到全面可觀測性">Sentry 深入&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/monitoring/06-commercial-comparison/firebase-suite/" data-link-title="Firebase 套件" data-link-desc="Crashlytics &amp;#43; Analytics &amp;#43; Remote Config 的整合 — Firebase 把 error tracking 和行為分析拆成獨立產品的設計取捨">Firebase 套件&lt;/a>和&lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 設計&lt;/a>已完整討論。以下展開路徑 B 和 C。&lt;/p></description><content:encoded><![CDATA[<p>監控方案的選擇不是「完全自架 Go collector」和「買 Sentry 訂閱」的二元決策。中間存在兩條路徑 — 用 BaaS（Supabase / Firebase）搭出託管版 collector，或用 PaaS（Railway / Fly.io）跑自架 collector 原始碼但不管 server。四條路徑的本質差異在「哪些層自己管、哪些交給平台」。</p>
<p><a href="/blog/monitoring/06-commercial-comparison/self-hosted-vs-commercial/" data-link-title="自架 vs 商業的判斷決策表" data-link-desc="使用者數、網路範圍、功能需求、合規要求四個維度判斷該自架還是用商業方案">自架 vs 商業的判斷決策表</a>用四個維度（使用者數 / 網路範圍 / 功能需求 / 合規）做二元分流。本章把光譜展開成四條路徑，讓中間的 BaaS 和 PaaS 選項浮現。Backend 選型模組已建立了完整的交付形態光譜（<a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">交付形態選型</a>）和逐能力判斷外包深度的框架（<a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">能力級買 vs 建</a>）。本章把那個框架特化到監控場景。</p>
<h2 id="四條路徑">四條路徑</h2>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>代表方案</th>
          <th>Collector 是什麼</th>
          <th>Storage 是什麼</th>
          <th>自己管什麼</th>
          <th>平台管什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A. 商業監控 SaaS</td>
          <td>Sentry / Datadog / Firebase Analytics</td>
          <td>vendor 提供</td>
          <td>vendor 提供</td>
          <td>SDK 埋點</td>
          <td>全部</td>
      </tr>
      <tr>
          <td>B. BaaS + Serverless</td>
          <td>Supabase + Vercel / Cloudflare Workers</td>
          <td>serverless function（自己寫）</td>
          <td>managed PostgreSQL（Supabase）</td>
          <td>collector 邏輯、schema</td>
          <td>server 維運、DB 維運、TLS、HA</td>
      </tr>
      <tr>
          <td>C. PaaS</td>
          <td>Railway / Fly.io / Render</td>
          <td>Go binary（自架 collector 原始碼）</td>
          <td>SQLite（同 binary）或 managed DB</td>
          <td>collector 邏輯、storage</td>
          <td>server 維運、TLS、deploy</td>
      </tr>
      <tr>
          <td>D. 完全自架</td>
          <td>VPS + Go binary</td>
          <td>Go binary</td>
          <td>SQLite 或自管 PostgreSQL</td>
          <td>全部</td>
          <td>無</td>
      </tr>
  </tbody>
</table>
<p>路徑 A 和 D 分別是光譜的兩端 — <a href="/blog/monitoring/06-commercial-comparison/sentry-deep-dive/" data-link-title="Sentry 深入" data-link-desc="Error tracking &#43; performance monitoring &#43; session replay 的架構 — Sentry 從 error-first 出發如何擴展到全面可觀測性">Sentry 深入</a>、<a href="/blog/monitoring/06-commercial-comparison/firebase-suite/" data-link-title="Firebase 套件" data-link-desc="Crashlytics &#43; Analytics &#43; Remote Config 的整合 — Firebase 把 error tracking 和行為分析拆成獨立產品的設計取捨">Firebase 套件</a>和<a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 設計</a>已完整討論。以下展開路徑 B 和 C。</p>
<h2 id="路徑-bbaas--serverless">路徑 B：BaaS + Serverless</h2>
<p>APP 上線初期用 Supabase + Vercel（或 Cloudflare Workers）搭監控後端：serverless function 接收 SDK 送來的事件、驗證 schema 後寫入 Supabase 的 PostgreSQL。整條鏈路在免費方案額度內可以零成本運作。</p>
<h3 id="架構差異">架構差異</h3>
<p>Serverless function 沒有常駐 process。模組四假設的 Go single binary 架構 — channel 背壓、single-writer goroutine pattern、in-memory buffer — 在 serverless 環境都不適用。每個 HTTP request 是獨立的 function invocation，沒有跨 request 的記憶體狀態。</p>
<p>背壓機制需要重新設計：Go collector 用 channel 容量做背壓（channel 滿回 429），serverless 版改用 DB-level 的 rate limit（PostgreSQL 的 advisory lock 或外部 rate limiter 如 Upstash Redis）或 platform-level 的 quota（Vercel 的 concurrency limit）。SDK 端的 429 處理邏輯不需要改 — 不管背壓訊號來自 channel 還是 DB quota，SDK 都是收到 429 後降採樣。</p>
<p>Downsample 和 purge 在 Go collector 是 background goroutine 定期執行。Serverless 沒有 background job — 需要外部 cron trigger（Vercel Cron / Supabase pg_cron / GitHub Actions scheduled workflow）。</p>
<h3 id="免費方案限額">免費方案限額</h3>
<p>以下為 2026-06 查詢的各平台免費方案限額。平台定價會變動，決策前以官方定價頁為準。</p>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>免費方案限額</th>
          <th>對監控場景的意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Supabase Free</td>
          <td>500MB DB、50K MAU、500K Edge Function invocations/月</td>
          <td>500MB 約 50-100 萬筆事件（每筆 ~500 bytes）、自用場景可用數月</td>
      </tr>
      <tr>
          <td>Vercel Hobby</td>
          <td>100GB bandwidth、10s function timeout、無明確 invocation 上限</td>
          <td>瓶頸在 bandwidth 和 execution duration、非 invocation 數；timeout 對 ingestion 足夠</td>
      </tr>
      <tr>
          <td>Cloudflare Workers</td>
          <td>100K requests/天（免費）、D1 5GB</td>
          <td>100K requests/天 x 100 筆/batch = 10M events/天、D1 的 SQLite 可替代 Supabase</td>
      </tr>
  </tbody>
</table>
<p>Audit date: 2026-06。平台免費方案限額可能調整，決策前以官方定價頁為準。</p>
<h3 id="適合情境">適合情境</h3>
<p>路徑 B 適合以下組合：APP 上線初期（使用者數 &lt; 100）、團隊熟悉前端和 SQL 但不想管 server、想保留自訂 schema 和查詢彈性（商業 SaaS 的 schema 是 vendor 定義的）、零成本起步但未來可能遷到自架。</p>
<h3 id="撞牆訊號">撞牆訊號</h3>
<p>以下訊號出現時，代表路徑 B 的天花板已到、該評估遷到路徑 C 或 D：</p>
<p><strong>連線數瓶頸</strong>：Supabase Free 的 PostgreSQL 約 20 個 concurrent connection。Serverless function 每次 invocation 開新連線，高併發時可能耗盡連線池。Supabase 內建 PgBouncer 做 connection pooling 可緩解，但免費方案的 pooler 有自己的連線上限。</p>
<p><strong>Cold start 延遲</strong>：Vercel serverless function 的 cold start 約 200ms、Supabase Edge Function 約 100ms。對監控 ingestion（不是使用者面向 API）通常可接受，但如果 SDK 的 flush timeout 設得很短（&lt; 1s），cold start 可能造成偶發超時。</p>
<p><strong>Background job 限制</strong>：Downsample 和 purge 需要外部 cron。Vercel Hobby 支援最多 2 個 cron job、每個最頻繁每天觸發 1 次 — 如果需要每小時 downsample，要用 Supabase pg_cron（Free 方案支援）或外部 scheduler。</p>
<p><strong>免費額度耗盡</strong>：Supabase 的 500K Edge Function invocations/月 ≈ 每天 16K requests。如果每個 request 攢批 100 筆事件，可處理每天 160 萬筆事件。超過後進入按量付費。Vercel Hobby 無明確 invocation 上限、瓶頸在 bandwidth（100GB/月）和 execution duration。</p>
<p><strong>合規限制</strong>：Supabase Free 的 PostgreSQL 部署在特定 region。有 GDPR data residency 需求的 app（歐盟使用者的資料必須留在 EU）需確認 vendor 的 region 支援 — 免費方案的 region 選擇可能有限。</p>
<h2 id="路徑-cpaas">路徑 C：PaaS</h2>
<p>PaaS 跑的是和完全自架相同的 Go collector 原始碼，差異只在部署方式。<code>git push</code> 觸發自動 build 和 deploy，平台管 server provisioning、TLS 憑證、process supervision。Collector 的 channel 背壓、single-writer pattern、SQLite storage 全部適用 — 和本機開發環境的行為一致。</p>
<p>Railway 和 Fly.io 都支援 persistent volume — Railway Hobby 含 1GB、Fly.io Free 含 1GB（限單 region）。SQLite 的 WAL 檔案需要持久化，persistent volume 是必要條件。Render 的免費方案沒有 persistent disk — SQLite 在每次 deploy 後重置，不適合需要保留歷史事件的場景。PaaS 平台以 container 形式運行 collector，SQLite 在 container 中的 I/O 和持久化考量見 <a href="/blog/monitoring/04-collector/container-deployment/" data-link-title="Container 部署設計" data-link-desc="Docker 部署 collector 的設計 — SQLite 在 overlay filesystem 的 I/O 考量、volume mount、graceful shutdown、資源限制">Container 部署設計</a>。</p>
<p>路徑 C 適合：想用自架 collector 但不想管 server / TLS / systemd 的團隊。程式碼完全相同，遷到自架（路徑 D）的成本接近零 — 把 binary 複製到 VPS、設定 systemd service 就完成。</p>
<p>路徑 C 的天花板在平台定價 — Railway Hobby 有 $5/月的資源上限、Fly.io Free 有 3 個 shared VM。流量成長到免費額度不夠時，PaaS 的按量付費和 VPS 月租費的交叉點是遷到自架的判讀訊號。</p>
<h2 id="路徑間的遷移">路徑間的遷移</h2>
<p>遷移成本取決於起點和終點之間有多少層需要重寫。</p>
<table>
  <thead>
      <tr>
          <th>遷移方向</th>
          <th>成本</th>
          <th>主要工作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>B → C</td>
          <td>中</td>
          <td>Serverless function → Go binary（重寫 collector 邏輯）；DB 可保留或遷移</td>
      </tr>
      <tr>
          <td>B → D</td>
          <td>中</td>
          <td>同上 + 自己管 server</td>
      </tr>
      <tr>
          <td>C → D</td>
          <td>低</td>
          <td>同程式碼不同部署（複製 binary + systemd）</td>
      </tr>
      <tr>
          <td>D → C</td>
          <td>低</td>
          <td>同程式碼推到 PaaS</td>
      </tr>
      <tr>
          <td>D → A</td>
          <td>低</td>
          <td>SDK 改 endpoint 指向商業方案、不改 SDK 程式碼</td>
      </tr>
      <tr>
          <td>A → D</td>
          <td>高</td>
          <td>從零建 collector + storage + dashboard</td>
      </tr>
      <tr>
          <td>A → B</td>
          <td>高</td>
          <td>從零寫 serverless collector + 設定 managed DB</td>
      </tr>
      <tr>
          <td>A → C</td>
          <td>高</td>
          <td>從零寫 Go collector + 推到 PaaS</td>
      </tr>
  </tbody>
</table>
<p>路徑 B → C 或 B → D 的遷移代價主要在 collector 邏輯的重寫 — serverless function 的 request-level 處理和 Go binary 的 channel-based pipeline 是不同的架構，不能直接搬。資料層的遷移代價較低 — Supabase 的 PostgreSQL 資料可以用 <code>pg_dump</code> 匯出、匯入自管 PostgreSQL。</p>
<p>交付形態遷出的通用框架（資產線盤點、並行期設計、回切窗口）見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">託管形態遷出</a>。</p>
<h2 id="外包深度對照">外包深度對照</h2>
<p>用 <a href="/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度</a> 的三層框架（managed 基礎設施 / feature SaaS / BaaS bundle）看四條路徑：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>外包深度</th>
          <th>控制權</th>
          <th>遷出代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A. 商業監控 SaaS</td>
          <td>feature SaaS（最深）</td>
          <td>SDK 埋點 API、vendor 定義 schema 和查詢</td>
          <td>高</td>
      </tr>
      <tr>
          <td>B. BaaS + Serverless</td>
          <td>managed 基礎設施 + 自寫 function（中間）</td>
          <td>自訂 schema、自訂查詢、自訂 collector 邏輯</td>
          <td>中</td>
      </tr>
      <tr>
          <td>C. PaaS</td>
          <td>managed 基礎設施（淺）</td>
          <td>和自架相同、只有部署平台交出去</td>
          <td>低</td>
      </tr>
      <tr>
          <td>D. 完全自架</td>
          <td>不外包</td>
          <td>完全控制</td>
          <td>無</td>
      </tr>
  </tbody>
</table>
<p>路徑 B 在外包深度上介於 managed 基礎設施和 BaaS bundle 之間 — DB 和 runtime 交給平台，但 collector 邏輯和 schema 仍由開發者控制。這和 <a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS</a> 的「前端 SDK 直連平台資料庫」模式不同 — 監控場景的路徑 B 仍然有一個自己寫的中間層（serverless function），只是這個中間層跑在平台上而非自己的 server。</p>
<h2 id="選擇建議">選擇建議</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>建議路徑</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自用工具、同機或同網段</td>
          <td>D</td>
          <td>成本最低、複雜度最低</td>
      </tr>
      <tr>
          <td>APP 上線初期、使用者 &lt; 100、零成本起步</td>
          <td>B 或 A</td>
          <td>B 保留自訂彈性、A 開箱即用</td>
      </tr>
      <tr>
          <td>小型團隊、想用自架 collector 但不想管 server</td>
          <td>C</td>
          <td>程式碼相同、部署簡單、遷出成本低</td>
      </tr>
      <tr>
          <td>使用者 &gt; 1000、需要 dashboard + 告警 + replay</td>
          <td>A</td>
          <td>商業方案的功能完成度遠高於自建</td>
      </tr>
      <tr>
          <td>合規要求資料不離開自有設施</td>
          <td>D</td>
          <td>完全控制資料位置</td>
      </tr>
  </tbody>
</table>
<p>APP 上線初期選 B 或 A 取決於自訂需求 — 需要自訂 schema 和查詢邏輯（例如自定義 error fingerprint、行為事件命名規範）選 B，只需要開箱即用的 error tracking 或行為分析選 A。B 保留遷到自架的彈性（資料在自己的 PostgreSQL），A 的功能完成度更高（dashboard、告警、session replay 開箱即用）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>自架 vs 商業的詳細決策 → <a href="/blog/monitoring/06-commercial-comparison/self-hosted-vs-commercial/" data-link-title="自架 vs 商業的判斷決策表" data-link-desc="使用者數、網路範圍、功能需求、合規要求四個維度判斷該自架還是用商業方案">自架 vs 商業的判斷決策表</a></li>
<li>自架 collector 的完整設計 → <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 設計</a></li>
<li>Backend 交付形態光譜 → <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">交付形態選型</a></li>
<li>能力級買 vs 建判斷 → <a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">能力級買 vs 建</a></li>
<li>外包深度概念 → <a href="/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度</a></li>
<li>BaaS 概念 → <a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS</a></li>
<li>遷出劇本 → <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">託管形態遷出</a></li>
<li>Vendor lock-in 概念 → <a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">Vendor Lock-In</a></li>
</ul>
]]></content:encoded></item><item><title>5.C2 Condé Nast：EKS 平台整併與標準化</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/</guid><description>&lt;p>這個案例的核心責任是說明平台整併常是組織治理問題，技術選型只是其中一層。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Condé Nast 旗下多個小團隊各自維護獨立的 Kubernetes 環境，各團隊使用不同的 Kubernetes 版本、操作模型、部署流程與存取模式。Self-managed Kubernetes 跑在 EC2 上，每個團隊自行維護 control plane、AMI、安全修補與 IAM credential 管理（使用 kube2iam 等開源工具）。&lt;/p>
&lt;p>整併後成立一個 single global platform team，遷移到 Amazon EKS。技術棧標準化為 Bottlerocket OS、VPC CNI、AWS Load Balancer Controller、IRSA（IAM Roles for Service Accounts）。Multi-tenancy 用 Kubernetes namespace 隔離，搭配 resource quotas 與 limits 防止 noisy neighbor。&lt;/p>
&lt;p>結果面：搭配 CloudFront 與 AWS Global Accelerator 後，end user latency 降低達 50%。團隊可以在 guardrails 內快速建立新叢集，operational overhead 顯著降低。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>平台碎片化的代價分兩層。表面層是重工——每個團隊各自處理安全修補、版本升級、credential 管理，相同工作做了 N 遍。深層是一致性缺失——不同團隊的安全基線不同，某個團隊漏修的 CVE 可能成為整個組織的入口。&lt;/p>
&lt;p>整併的工程價值在於把「每個團隊各自解決平台問題」變成「平台團隊解決一次、所有團隊共用」。這個轉換的前提是平台團隊能提供足夠彈性的 multi-tenancy 模型——resource quotas 防止資源搶占、namespace 隔離防止互相影響、IRSA 讓每個 workload 有獨立的 AWS 權限而非共用 node-level credential。&lt;/p>
&lt;p>kube2iam → IRSA 的切換是這個案例中安全基線提升最顯著的一步。kube2iam 依賴 iptables 攔截 metadata endpoint，在多租戶環境下有 race condition 與 credential leak 風險。IRSA 用 OIDC federation 讓每個 service account 直接取得 scoped IAM role，消除了 node-level 的 credential 共用。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>盤點既有叢集的差異維度&lt;/strong>：Kubernetes 版本、CNI、ingress controller、credential 管理方式、部署流程、監控工具。差異清單是遷移計畫的輸入。&lt;/li>
&lt;li>&lt;strong>定義統一平台基線&lt;/strong>：選定 EKS + Bottlerocket + VPC CNI + IRSA 作為所有叢集的共通配置。基線要涵蓋安全（pod 唯讀 filesystem、禁 root）、資源（quotas、limits）、網路（CNI、LB controller）。&lt;/li>
&lt;li>&lt;strong>用 namespace multi-tenancy 取代獨立叢集&lt;/strong>：每個團隊一個 namespace，resource quotas 限制資源用量。這比一個團隊一個叢集的運維成本低，但需要在 namespace 層級做好隔離（NetworkPolicy、ResourceQuota、RBAC scope）。&lt;/li>
&lt;li>&lt;strong>漸進切換業務流量&lt;/strong>：按 region / 市場分批遷移，每批遷移後驗證 latency 與 error rate。搭配 CloudFront 做 edge 層的流量管理。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫的章節段落">可回寫的章節段落&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%a4%a7%e8%a6%8f%e6%a8%a1-k8s-%e7%9a%84%e8%a8%ad%e8%a8%88%e5%8f%96%e6%8d%a8" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 大規模 K8s 的設計取捨&lt;/a>：single-cluster multi-namespace 的治理單位選擇&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界&lt;/a>：global platform team 的職責重訂&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract&lt;/a>：AWS LB Controller + CloudFront 的流量入口配置&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/containers/how-conde-nast-modernized-its-container-platform-on-amazon-elastic-kubernetes-service/">How Condé Nast modernized its container platform on Amazon EKS&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明平台整併常是組織治理問題，技術選型只是其中一層。</p>
<h2 id="觀察">觀察</h2>
<p>Condé Nast 旗下多個小團隊各自維護獨立的 Kubernetes 環境，各團隊使用不同的 Kubernetes 版本、操作模型、部署流程與存取模式。Self-managed Kubernetes 跑在 EC2 上，每個團隊自行維護 control plane、AMI、安全修補與 IAM credential 管理（使用 kube2iam 等開源工具）。</p>
<p>整併後成立一個 single global platform team，遷移到 Amazon EKS。技術棧標準化為 Bottlerocket OS、VPC CNI、AWS Load Balancer Controller、IRSA（IAM Roles for Service Accounts）。Multi-tenancy 用 Kubernetes namespace 隔離，搭配 resource quotas 與 limits 防止 noisy neighbor。</p>
<p>結果面：搭配 CloudFront 與 AWS Global Accelerator 後，end user latency 降低達 50%。團隊可以在 guardrails 內快速建立新叢集，operational overhead 顯著降低。</p>
<h2 id="判讀">判讀</h2>
<p>平台碎片化的代價分兩層。表面層是重工——每個團隊各自處理安全修補、版本升級、credential 管理，相同工作做了 N 遍。深層是一致性缺失——不同團隊的安全基線不同，某個團隊漏修的 CVE 可能成為整個組織的入口。</p>
<p>整併的工程價值在於把「每個團隊各自解決平台問題」變成「平台團隊解決一次、所有團隊共用」。這個轉換的前提是平台團隊能提供足夠彈性的 multi-tenancy 模型——resource quotas 防止資源搶占、namespace 隔離防止互相影響、IRSA 讓每個 workload 有獨立的 AWS 權限而非共用 node-level credential。</p>
<p>kube2iam → IRSA 的切換是這個案例中安全基線提升最顯著的一步。kube2iam 依賴 iptables 攔截 metadata endpoint，在多租戶環境下有 race condition 與 credential leak 風險。IRSA 用 OIDC federation 讓每個 service account 直接取得 scoped IAM role，消除了 node-level 的 credential 共用。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>盤點既有叢集的差異維度</strong>：Kubernetes 版本、CNI、ingress controller、credential 管理方式、部署流程、監控工具。差異清單是遷移計畫的輸入。</li>
<li><strong>定義統一平台基線</strong>：選定 EKS + Bottlerocket + VPC CNI + IRSA 作為所有叢集的共通配置。基線要涵蓋安全（pod 唯讀 filesystem、禁 root）、資源（quotas、limits）、網路（CNI、LB controller）。</li>
<li><strong>用 namespace multi-tenancy 取代獨立叢集</strong>：每個團隊一個 namespace，resource quotas 限制資源用量。這比一個團隊一個叢集的運維成本低，但需要在 namespace 層級做好隔離（NetworkPolicy、ResourceQuota、RBAC scope）。</li>
<li><strong>漸進切換業務流量</strong>：按 region / 市場分批遷移，每批遷移後驗證 latency 與 error rate。搭配 CloudFront 做 edge 層的流量管理。</li>
</ol>
<h2 id="可回寫的章節段落">可回寫的章節段落</h2>
<ul>
<li><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%a4%a7%e8%a6%8f%e6%a8%a1-k8s-%e7%9a%84%e8%a8%ad%e8%a8%88%e5%8f%96%e6%8d%a8" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 大規模 K8s 的設計取捨</a>：single-cluster multi-namespace 的治理單位選擇</li>
<li><a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>：global platform team 的職責重訂</li>
<li><a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract</a>：AWS LB Controller + CloudFront 的流量入口配置</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/containers/how-conde-nast-modernized-its-container-platform-on-amazon-elastic-kubernetes-service/">How Condé Nast modernized its container platform on Amazon EKS</a></li>
</ul>
]]></content:encoded></item><item><title>Docker</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/</guid><description>&lt;p>Docker 是最早 popularize container 的工具、承擔三個責任：container image build（Dockerfile / BuildKit）、local container runtime（docker run / Compose）、image distribution（Docker Hub / private registry）。設計取捨偏向「dev experience + image format standard」、production orchestration 多被 Kubernetes + containerd 取代、但 image build / dev workflow / OCI image 仍是事實標準。&lt;/p>
&lt;p>對「Local dev / CI container 工具、image build pipeline、小規模 dev 環境」這條路徑、Docker 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 Dockerfile + 跑 docker build / run&lt;/li>
&lt;li>用 multi-stage build / BuildKit 優化 image&lt;/li>
&lt;li>用 Docker Compose 編排 dev 環境&lt;/li>
&lt;li>配置 image registry + scanning + SBOM&lt;/li>
&lt;li>評估 Docker Desktop license 對團隊的影響、選替代（Podman / Rancher Desktop）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-docker-跑起來">最短路徑：5 分鐘把 Docker 跑起來&lt;/h2>





&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"># 1. 安裝（macOS 擇一）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">brew install --cask docker &lt;span class="c1"># Docker Desktop（商業企業需付費授權）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1"># brew install podman # 替代方案：Podman（無 daemon、免費）&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;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 跑 container&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">docker run -d -p 8080:80 --name web nginx:stable-alpine
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">docker ps &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> docker logs web
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. Build + push image&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">docker build -t myapp:1 .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">docker tag myapp:1 ghcr.io/&amp;lt;org&amp;gt;/myapp:1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">docker push ghcr.io/&amp;lt;org&amp;gt;/myapp:1&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="dockerfile-設計">Dockerfile 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>FROM / RUN / COPY / WORKDIR / EXPOSE / CMD / ENTRYPOINT&lt;/li>
&lt;li>Multi-stage build（build stage + runtime stage 分離）&lt;/li>
&lt;li>Layer cache 設計（COPY 順序影響 cache hit）&lt;/li>
&lt;li>對應指令：&lt;code>docker build --no-cache&lt;/code>、&lt;code>docker history &amp;lt;image&amp;gt;&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="buildkit--buildx">BuildKit / Buildx&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>BuildKit：新 builder、parallel + cache mount + secret + SSH agent&lt;/li>
&lt;li>Buildx：cross-platform build（amd64 / arm64）&lt;/li>
&lt;li>Cache backend（local / registry / S3 / GHA）&lt;/li>
&lt;li>對應指令：&lt;code>docker buildx create --use&lt;/code>、&lt;code>docker buildx build --platform=linux/amd64,linux/arm64&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="docker-compose">Docker Compose&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Docker 是最早 popularize container 的工具、承擔三個責任：container image build（Dockerfile / BuildKit）、local container runtime（docker run / Compose）、image distribution（Docker Hub / private registry）。設計取捨偏向「dev experience + image format standard」、production orchestration 多被 Kubernetes + containerd 取代、但 image build / dev workflow / OCI image 仍是事實標準。</p>
<p>對「Local dev / CI container 工具、image build pipeline、小規模 dev 環境」這條路徑、Docker 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 Dockerfile + 跑 docker build / run</li>
<li>用 multi-stage build / BuildKit 優化 image</li>
<li>用 Docker Compose 編排 dev 環境</li>
<li>配置 image registry + scanning + SBOM</li>
<li>評估 Docker Desktop license 對團隊的影響、選替代（Podman / Rancher Desktop）</li>
</ol>
<h2 id="最短路徑5-分鐘把-docker-跑起來">最短路徑：5 分鐘把 Docker 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 安裝（macOS 擇一）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">brew install --cask docker            <span class="c1"># Docker Desktop（商業企業需付費授權）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># brew install podman                 # 替代方案：Podman（無 daemon、免費）</span>
</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="c1"># 2. 跑 container</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">docker run -d -p 8080:80 --name web nginx:stable-alpine
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">docker ps <span class="o">&amp;&amp;</span> docker logs web
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 3. Build + push image</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">docker build -t myapp:1 .
</span></span><span class="line"><span class="ln">11</span><span class="cl">docker tag myapp:1 ghcr.io/&lt;org&gt;/myapp:1
</span></span><span class="line"><span class="ln">12</span><span class="cl">docker push ghcr.io/&lt;org&gt;/myapp:1</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="dockerfile-設計">Dockerfile 設計</h3>
<p>子議題：</p>
<ul>
<li>FROM / RUN / COPY / WORKDIR / EXPOSE / CMD / ENTRYPOINT</li>
<li>Multi-stage build（build stage + runtime stage 分離）</li>
<li>Layer cache 設計（COPY 順序影響 cache hit）</li>
<li>對應指令：<code>docker build --no-cache</code>、<code>docker history &lt;image&gt;</code></li>
</ul>
<h3 id="buildkit--buildx">BuildKit / Buildx</h3>
<p>子議題：</p>
<ul>
<li>BuildKit：新 builder、parallel + cache mount + secret + SSH agent</li>
<li>Buildx：cross-platform build（amd64 / arm64）</li>
<li>Cache backend（local / registry / S3 / GHA）</li>
<li>對應指令：<code>docker buildx create --use</code>、<code>docker buildx build --platform=linux/amd64,linux/arm64</code></li>
</ul>
<h3 id="docker-compose">Docker Compose</h3>
<p>子議題：</p>
<ul>
<li>docker-compose.yml：service / network / volume 配置</li>
<li>適合：local dev 多 container（DB + cache + app）</li>
<li>不適合：production（用 K8s）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 K8s deployment</a></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="image-security--scanning--sbom">Image security / scanning / SBOM</h3>
<p>子議題：</p>
<ul>
<li>Trivy / Grype / Snyk image vulnerability scanning</li>
<li>SBOM 產生（syft / Docker scout）</li>
<li>Sign image（cosign / notary v2）</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a> supply chain</li>
</ul>
<h3 id="image-registry-選擇">Image registry 選擇</h3>
<p>子議題：</p>
<ul>
<li>Docker Hub（public + rate limit issue）</li>
<li>雲端：ECR / GCR / Artifact Registry / ACR</li>
<li>Self-host：Harbor / GitLab Container Registry / Nexus</li>
<li>對應 image pull credentials 管理</li>
</ul>
<h3 id="docker-desktop-license">Docker Desktop license</h3>
<p>子議題：</p>
<ul>
<li>2021 改授權：商業企業（&gt; 250 員工 / &gt; $10M）需付費</li>
<li>替代：Podman Desktop / Rancher Desktop / Colima / Lima</li>
<li>替代品的 daemon / rootless 差異</li>
<li>對應企業 IT 採購決策</li>
</ul>
<h3 id="containerd--cri-o-在-production">Containerd / CRI-O 在 production</h3>
<p>子議題：</p>
<ul>
<li>K8s 1.24+ 移除 dockershim、改用 containerd / CRI-O</li>
<li>Docker image 跟 containerd 相容（OCI standard）</li>
<li>production 不用 Docker、用 containerd</li>
</ul>
<h3 id="image-size-優化">Image size 優化</h3>
<p>子議題：</p>
<ul>
<li>Base image 選擇（distroless / alpine / scratch）</li>
<li>Multi-stage build + layer combine</li>
<li>Build context（.dockerignore）</li>
<li>跟 image scanning 跟 deploy speed 對應</li>
</ul>
<h3 id="rootless--安全強化">Rootless / 安全強化</h3>
<p>子議題：</p>
<ul>
<li>Rootless mode（Docker / Podman 都支援）</li>
<li>User namespace mapping</li>
<li>Seccomp / AppArmor / SELinux profile</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a> container security</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="image-build-cache-不命中">Image build cache 不命中</h3>
<p>操作原則：COPY 順序錯、<code>.dockerignore</code> 缺、變動的 layer 在前面。</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">docker build --progress<span class="o">=</span>plain --no-cache -t myapp:debug .   <span class="c1"># 逐層輸出、比對哪層吃時間</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker <span class="nb">history</span> myapp:debug                                  <span class="c1"># 看每層大小</span></span></span></code></pre></div><h3 id="image-過大">Image 過大</h3>
<p>操作原則：base image 太重 / 沒 multi-stage / build context 過大。判讀：<code>docker history</code> 看 layer 大小。</p>
<h3 id="container-起不來">Container 起不來</h3>
<p>操作原則：<code>docker logs</code> + <code>docker inspect</code> 看 exit code + state。</p>
<h3 id="network-port-不通">Network port 不通</h3>
<p>操作原則：<code>-p</code> mapping vs <code>EXPOSE</code> 差異、host network vs bridge network、firewall。</p>
<h3 id="volume-權限問題">Volume 權限問題</h3>
<p>操作原則：container UID 跟 host UID 不對齊、rootless mode 特別容易踩。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production orchestration</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a></td>
      </tr>
      <tr>
          <td>Rootless / 安全強化</td>
          <td>Podman</td>
      </tr>
      <tr>
          <td>替代 Docker Desktop（cost）</td>
          <td>Rancher Desktop / Colima / Lima</td>
      </tr>
      <tr>
          <td>純單機 service</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd</a></td>
      </tr>
      <tr>
          <td>雲端 managed container</td>
          <td>ECS / Cloud Run / Container Apps</td>
      </tr>
      <tr>
          <td>Build-only（無 daemon）</td>
          <td>Buildah / Kaniko / BuildKit standalone</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Dockerfile 完整 reference</li>
<li>Docker Compose v2 進階配置</li>
<li>Container runtime spec（runc / OCI）</li>
<li>各 registry 完整 API</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Docker 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s</a></td>
          <td>Container image 是平台遷移的可攜介面、orchestrator 換但 image 不換</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小規模直接 Docker / Compose、中大型才走 K8s（Docker 退到 build only）</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Docker 案例</strong>：Docker Hub rate limit incident、企業 license 遷移到 Podman 案例、image scanning supply chain 案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a>、<a href="/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd</a></li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a>（image scanning / SBOM）</li>
</ul>
]]></content:encoded></item><item><title>5.2 Kubernetes 部署策略</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/</guid><description>&lt;p>Kubernetes 部署策略（Kubernetes deployment strategy）的核心責任是把服務版本切換做成可預測流程。Deployment 把副本數、健康訊號、流量承接、設定變更與回退條件組成同一條交付路徑。&lt;/p>
&lt;h2 id="deploymentreplica-與-rollout">deployment、replica 與 rollout&lt;/h2>
&lt;p>Deployment 的責任是宣告目標狀態：期望副本數、版本、更新策略。rollout 的責任是把現況收斂到目標狀態，並在過程中維持可服務能力。這兩者分開理解後，才能在異常時判斷是目標設定問題，還是收斂過程問題。&lt;/p>
&lt;p>rolling update 常用來降低單次切換風險。rolling update 的判讀重點是批次大小與節奏：每批新增多少新副本、每批回收多少舊副本、每批觀察多長時間。這些參數以服務容量曲線與回退時間目標校準、名稱本身只是工具標籤、不是判讀條件。&lt;/p>
&lt;h2 id="probe-對齊服務生命週期">probe 對齊服務生命週期&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe&lt;/a> 要對齊服務生命週期，不同 probe 有不同責任：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/startup-probe/" data-link-title="Startup Probe" data-link-desc="保護慢啟動服務不被 liveness probe 過早重啟的探針">startup probe&lt;/a>：確認服務啟動完成，避免慢啟動服務被過早重啟。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> probe：確認服務可安全接流量。&lt;/li>
&lt;li>liveness probe：確認服務仍可維持基本運作，必要時觸發重建。&lt;/li>
&lt;/ol>
&lt;p>probe 設計若只回傳固定成功，rollout 期間會出現「容器在線但服務未就緒」的流量抖動。穩定做法是讓 readiness 反映依賴就緒條件，例如資料庫連線池、必要配置、關鍵背景任務狀態。&lt;/p>
&lt;h3 id="startup-probe-設計注意事項">Startup probe 設計注意事項&lt;/h3>
&lt;p>startup probe 跟 &lt;code>initialDelaySeconds&lt;/code> 解決同一個問題（避免慢啟動服務被 liveness 殺掉），但機制不同。&lt;code>initialDelaySeconds&lt;/code> 是 liveness / readiness probe 的延遲啟動——在等待期間 probe 完全不跑，無法觀測啟動進度。startup probe 在啟動期間持續探測，一旦成功就交棒給 liveness / readiness，啟動失敗時能更快偵測到。&lt;/p>
&lt;p>startup probe 的總容忍時間 = &lt;code>failureThreshold × periodSeconds&lt;/code>。例如 &lt;code>failureThreshold: 30, periodSeconds: 10&lt;/code> 給服務 300 秒啟動窗口。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線建立），再加 20-30% headroom 作為總容忍時間。&lt;/p>
&lt;h3 id="readiness-probe-的深度選擇">Readiness probe 的深度選擇&lt;/h3>
&lt;p>readiness probe 的檢查深度決定它能攔截多少「可啟動但不可服務」的狀態。三個常見層級：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Port check&lt;/strong>（TCP probe）：確認進程在監聽。最淺，無法偵測依賴未就緒。適合依賴簡單、啟動快的服務。&lt;/li>
&lt;li>&lt;strong>Dependency check&lt;/strong>（HTTP endpoint 檢查必要依賴）：確認資料庫連線池、cache 連線可用。涵蓋多數「啟動完但依賴不通」的場景。常用做法是在 &lt;code>/ready&lt;/code> endpoint 內驗證必要依賴的連線狀態。&lt;/li>
&lt;li>&lt;strong>Deep health&lt;/strong>（業務路徑驗證）：執行一次簡化的業務查詢確認端到端通路。最深但代價最高——probe 本身消耗資源，且可能被下游延遲拖慢導致 readiness 抖動。&lt;/li>
&lt;/ol>
&lt;p>依賴分類（必要 / 可降級 / 觀測）的判讀框架見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Readiness 設計的核心取捨&lt;/a>。&lt;/p>
&lt;h2 id="config-rollout-與版本相容">config rollout 與版本相容&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout&lt;/a> 需要和應用版本一起治理。設定先行、版本後行，或版本先行、設定後行，都要保留相容窗口。相容窗口存在時，才有漸進 rollout 與快速回退空間。&lt;/p>
&lt;p>跨版本配置遷移要先定義停止條件：錯誤率上升、延遲尖峰、關鍵路徑失敗或下游壓力超標。停止條件明確後，部署決策才能一致。&lt;/p>
&lt;h3 id="n-1-相容與-feature-flag-gating">N-1 相容與 Feature Flag Gating&lt;/h3>
&lt;p>版本相容窗口的操作基線是 N-1 相容：版本 N 的程式碼可以處理版本 N-1 的設定，反之亦然。這讓 rollback 從「版本 + config 必須同時回退」降級成「版本先回退、config 稍後再處理」，回退操作的原子性要求降低。&lt;/p>
&lt;p>N-1 相容的實作通常搭配 feature flag gating：新功能在程式碼中預設關閉，先部署程式碼（版本 N 上線但新功能 off），確認版本穩定後再開啟 feature flag。這讓版本部署跟功能啟用分成兩個獨立決策，rollback 時只需關 flag 而不必回退版本。&lt;/p>
&lt;p>N-1 相容窗口的壽命要有明確終點。長期維護雙版本相容會累積技術債——舊欄位不能刪、舊路徑不能移除。穩定做法是在 rollout 完成 + 觀測確認穩定後設定移除 deadline，把 N-1 相容視為暫時性保護而非永久設計。設定注入方式與版本追蹤見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨&lt;/a>。&lt;/p>
&lt;h2 id="autoscaling-與部署策略協同">Autoscaling 與部署策略協同&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaling&lt;/a> 在部署期間扮演容量緩衝角色。部署批次若超過服務可承受變動幅度，autoscaling 會被動補償並延長收斂時間。穩定做法是讓 rollout 節奏與容量策略同時設計：先保證服務穩態，再提高切換速度。&lt;/p></description><content:encoded><![CDATA[<p>Kubernetes 部署策略（Kubernetes deployment strategy）的核心責任是把服務版本切換做成可預測流程。Deployment 把副本數、健康訊號、流量承接、設定變更與回退條件組成同一條交付路徑。</p>
<h2 id="deploymentreplica-與-rollout">deployment、replica 與 rollout</h2>
<p>Deployment 的責任是宣告目標狀態：期望副本數、版本、更新策略。rollout 的責任是把現況收斂到目標狀態，並在過程中維持可服務能力。這兩者分開理解後，才能在異常時判斷是目標設定問題，還是收斂過程問題。</p>
<p>rolling update 常用來降低單次切換風險。rolling update 的判讀重點是批次大小與節奏：每批新增多少新副本、每批回收多少舊副本、每批觀察多長時間。這些參數以服務容量曲線與回退時間目標校準、名稱本身只是工具標籤、不是判讀條件。</p>
<h2 id="probe-對齊服務生命週期">probe 對齊服務生命週期</h2>
<p><a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a> 要對齊服務生命週期，不同 probe 有不同責任：</p>
<ol>
<li><a href="/blog/backend/knowledge-cards/startup-probe/" data-link-title="Startup Probe" data-link-desc="保護慢啟動服務不被 liveness probe 過早重啟的探針">startup probe</a>：確認服務啟動完成，避免慢啟動服務被過早重啟。</li>
<li><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> probe：確認服務可安全接流量。</li>
<li>liveness probe：確認服務仍可維持基本運作，必要時觸發重建。</li>
</ol>
<p>probe 設計若只回傳固定成功，rollout 期間會出現「容器在線但服務未就緒」的流量抖動。穩定做法是讓 readiness 反映依賴就緒條件，例如資料庫連線池、必要配置、關鍵背景任務狀態。</p>
<h3 id="startup-probe-設計注意事項">Startup probe 設計注意事項</h3>
<p>startup probe 跟 <code>initialDelaySeconds</code> 解決同一個問題（避免慢啟動服務被 liveness 殺掉），但機制不同。<code>initialDelaySeconds</code> 是 liveness / readiness probe 的延遲啟動——在等待期間 probe 完全不跑，無法觀測啟動進度。startup probe 在啟動期間持續探測，一旦成功就交棒給 liveness / readiness，啟動失敗時能更快偵測到。</p>
<p>startup probe 的總容忍時間 = <code>failureThreshold × periodSeconds</code>。例如 <code>failureThreshold: 30, periodSeconds: 10</code> 給服務 300 秒啟動窗口。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線建立），再加 20-30% headroom 作為總容忍時間。</p>
<h3 id="readiness-probe-的深度選擇">Readiness probe 的深度選擇</h3>
<p>readiness probe 的檢查深度決定它能攔截多少「可啟動但不可服務」的狀態。三個常見層級：</p>
<ol>
<li><strong>Port check</strong>（TCP probe）：確認進程在監聽。最淺，無法偵測依賴未就緒。適合依賴簡單、啟動快的服務。</li>
<li><strong>Dependency check</strong>（HTTP endpoint 檢查必要依賴）：確認資料庫連線池、cache 連線可用。涵蓋多數「啟動完但依賴不通」的場景。常用做法是在 <code>/ready</code> endpoint 內驗證必要依賴的連線狀態。</li>
<li><strong>Deep health</strong>（業務路徑驗證）：執行一次簡化的業務查詢確認端到端通路。最深但代價最高——probe 本身消耗資源，且可能被下游延遲拖慢導致 readiness 抖動。</li>
</ol>
<p>依賴分類（必要 / 可降級 / 觀測）的判讀框架見 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Readiness 設計的核心取捨</a>。</p>
<h2 id="config-rollout-與版本相容">config rollout 與版本相容</h2>
<p><a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 需要和應用版本一起治理。設定先行、版本後行，或版本先行、設定後行，都要保留相容窗口。相容窗口存在時，才有漸進 rollout 與快速回退空間。</p>
<p>跨版本配置遷移要先定義停止條件：錯誤率上升、延遲尖峰、關鍵路徑失敗或下游壓力超標。停止條件明確後，部署決策才能一致。</p>
<h3 id="n-1-相容與-feature-flag-gating">N-1 相容與 Feature Flag Gating</h3>
<p>版本相容窗口的操作基線是 N-1 相容：版本 N 的程式碼可以處理版本 N-1 的設定，反之亦然。這讓 rollback 從「版本 + config 必須同時回退」降級成「版本先回退、config 稍後再處理」，回退操作的原子性要求降低。</p>
<p>N-1 相容的實作通常搭配 feature flag gating：新功能在程式碼中預設關閉，先部署程式碼（版本 N 上線但新功能 off），確認版本穩定後再開啟 feature flag。這讓版本部署跟功能啟用分成兩個獨立決策，rollback 時只需關 flag 而不必回退版本。</p>
<p>N-1 相容窗口的壽命要有明確終點。長期維護雙版本相容會累積技術債——舊欄位不能刪、舊路徑不能移除。穩定做法是在 rollout 完成 + 觀測確認穩定後設定移除 deadline，把 N-1 相容視為暫時性保護而非永久設計。設定注入方式與版本追蹤見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨</a>。</p>
<h2 id="autoscaling-與部署策略協同">Autoscaling 與部署策略協同</h2>
<p><a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaling</a> 在部署期間扮演容量緩衝角色。部署批次若超過服務可承受變動幅度，autoscaling 會被動補償並延長收斂時間。穩定做法是讓 rollout 節奏與容量策略同時設計：先保證服務穩態，再提高切換速度。</p>
<p>長連線服務或有大量背景任務的 workload，通常需要比 stateless API 更保守的 rollout 策略，並額外搭配 drain 與 reconnect 設計。</p>
<p>擴縮策略的演進需要版本化跟可回放。對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6 Airbnb K8s 叢集擴縮演進</a>：揭露「擴縮策略版本化跟可回放」「不同 workload 區分擴縮政策」「容量治理跟事故指標綁定」三個方向。以下基於通用工程知識展開。</p>
<p>可重複套用的做法：</p>
<ol>
<li><strong>擴縮策略進 IaC</strong>：HPA / VPA / Karpenter / Cluster Autoscaler 的配置都進 git、變更走 release flow、避免手動調整在事故後被遺忘。IaC + 自動化的 ownership 邊界見 [5.7 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> boundary](/backend/05-deployment-platform/traffic-config-control-plane-boundary/)。</li>
<li><strong>workload 分群擴縮</strong>：stateless API、長連線服務、batch job、background worker 對擴縮的需求不同。把不同 workload 用不同 namespace + 不同 autoscaler policy 隔離，避免一套規則套全部。</li>
<li><strong>擴縮事件接事故指標</strong>：HPA 觸發、scale-up 延遲、scale-down 過快、cluster autoscaler 加 node 失敗，都該在事故 timeline 上可見。回到 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 service topology</a> 的擴縮事件 vs 事故區分。</li>
</ol>
<h2 id="分階段平台遷移">分階段平台遷移</h2>
<p>平台遷移的本質是流量跟依賴的分段切換。遷移期內新舊叢集同時存在，rollout 策略要把跨叢集流量切換納入批次節奏、視為連續多批決策。本段聚焦流量 / 依賴切換時序；遷移期的團隊職責邊界重訂見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift：self-managed K8s → EKS</a>：揭露「零停機遷移要把切換做成分段策略」「難點通常在跨叢集服務依賴跟流量切換、不在 Kubernetes API 本身」。對應 <a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4 Mobileye workloads 遷移</a>：揭露「分批遷移 workload、保留觀測對照」「明確切換 / 回退條件」「新平台先驗證容量跟恢復節奏」。以下基於通用工程知識展開。</p>
<p>可重複套用的分階段做法：</p>
<ol>
<li><strong>新叢集 + 共通配置基線</strong>：先在新叢集上建立跟舊叢集對等的配置基線（namespace、ResourceQuota、NetworkPolicy、Ingress class、storage class），讓 workload 可以無縫部署。</li>
<li><strong>小流量先導服務</strong>：選擇影響面小、依賴單純的服務作為先導，先在新叢集跑完整 deployment cycle（rollout、drain、rollback 驗證）、累積信心後再擴大。</li>
<li><strong>可控流量分批切換</strong>：用 DNS 加權、service mesh 流量切分或 LB 規則把流量分批從舊叢集導到新叢集。每批切換後驗證 SLI 偏差、再進下一批。</li>
<li><strong>每批保留回退路徑</strong>：舊叢集服務不立即下線，保留作為回退目標。回退條件先驗證（rollback script、流量切回 DNS / LB 規則），再開始下一批切換。</li>
</ol>
<p>延伸 5.C1 揭露的「跨叢集服務依賴是難點」、5.C10 中型組織判讀「服務本身切過去了、但資料面、認證面、觀測面還沒同步」也指向同類問題。跨叢集遷移最容易出的事故是「服務切過去了、依賴沒切過去」。Database、cache、message queue、observability pipeline、auth service 的切換時機要分別規劃，避免應用層在新叢集但仍跨網路打舊叢集的依賴，造成隱性 latency 或單點失效。規模差異下的同類問題見 <a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照</a>。</p>
<h2 id="大規模-k8s-的設計取捨">大規模 K8s 的設計取捨</h2>
<p>K8s 在不同規模下的設計取捨會明顯分歧。小規模叢集追求簡單跟低運維成本，大規模叢集追求隔離跟自動化治理。同一套部署策略放到不同規模會在某個量級開始失效。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games：246 個 EKS cluster</a>：揭露架構決策從 multi-tenant cluster 改成 single-tenant per game、Karpenter + Terraform 的 cluster 級自動化、35ms 延遲門檻 + Local Zones / Outposts 區域部署（case 中「35ms 反推 region 部署」屬作者判讀層、本章引用此推論）。對應 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34 GCP 130,000-node GKE cluster</a>：揭露 control plane 極限取決於 storage backend（GCP 用 Spanner 替代 etcd）、AI workload 跟 web workload 容量規劃差異。對應 <a href="/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33 Maersk + Bosch AKS</a>：揭露 Maersk 工程訴求引語「focus on things that makes the most business impact」、傳統產業上 K8s 動機是治理一致性（作者判讀）、適合 single-cluster-multi-namespace。</p>
<p>可重複套用的取捨判讀：</p>
<ol>
<li><strong>single-tenant per workload vs single-cluster multi-namespace</strong>：高隔離需求（每個 workload 失效不能影響其他）、高延遲敏感度（需 region cluster）→ 多 cluster；治理一致性訴求（統一 release flow、合規邊界）→ 單一 cluster 多 namespace。</li>
<li><strong>Cluster 容量極限取決於 control plane</strong>：data plane（worker nodes）擴容容易、control plane（API server、etcd / storage）擴容難、瓶頸通常在 control plane。etcd 撐 5K-10K node 後吃力、需要替換 storage backend（Spanner / PostgreSQL / 自家 KV）才能撐萬級節點（見 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34</a>）。control plane 的 ownership 邊界由 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 control plane boundary</a> 處理。</li>
<li><strong>Multi-cluster 治理需要 IaC + 自動化</strong>：Terraform / Crossplane / Cluster API + Karpenter / Cluster Autoscaler 是基本工具。手動管理超過數十個 cluster 不可行。</li>
<li><strong>AI workload 跟 web workload 容量規劃完全不同</strong>：AI workload 短時間爆量創建 Pods（萬級 / 秒）、preempt 頻繁；web workload 節點生命週期長、變動緩。把 web 經驗套到 AI workload 容量規劃會嚴重低估壓力。</li>
</ol>
<p>關鍵判讀是「先決定 cluster 是隔離單位還是治理單位」。Riot Games 把 cluster 當隔離單位（246 個獨立 cluster），Maersk / Bosch 把 cluster 當治理單位（單 cluster 多 namespace）。同一個工具兩種用法、決定整體運維模型。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast：EKS 平台整併與標準化</a>：揭露多叢集整併到單一控制面的場景、跟 Maersk-Bosch 同屬「治理一致性」取捨方向（治理單位優先於隔離單位）。Condé Nast 的整併路徑是「盤點既有叢集差異 → 建立統一平台基線 → 藍綠或漸進切換業務流量」、對應前面「分階段平台遷移」段的批次節奏。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rollout 卡在中段且新副本反覆重啟</td>
          <td>probe 與啟動路徑不匹配</td>
          <td>校正 startup/readiness 探針與超時參數</td>
      </tr>
      <tr>
          <td>rollout 完成後延遲與錯誤率短期上升</td>
          <td>批次切換過快或下游未對齊</td>
          <td>降低批次、延長觀察窗口、回退再重試</td>
      </tr>
      <tr>
          <td>config 變更後特定路徑失敗率飆升</td>
          <td>設定與版本相容窗口不足</td>
          <td>啟動回退配置、補雙軌相容</td>
      </tr>
      <tr>
          <td>autoscaling 在部署期間頻繁抖動</td>
          <td>容量閾值與 rollout 節奏衝突</td>
          <td>分離部署窗口與擴縮窗口、調整資源策略</td>
      </tr>
      <tr>
          <td>長連線服務切版後 reconnect storm</td>
          <td>drain 與連線生命週期控制不足</td>
          <td>拉長 drain、分批切流、校正 timeout</td>
      </tr>
      <tr>
          <td>跨叢集遷移後特定路徑 latency 升高</td>
          <td>應用切過去但依賴未切、跨網路</td>
          <td>規劃依賴切換時機、分批一致</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 Kubernetes 部署看成 YAML 套版，會忽略服務語意差異。相同 deployment 參數在不同服務上，可能代表完全不同風險。</p>
<p>把 probe 當成健康檢查 URL，會讓服務在邊界條件下過早接流量。probe 的工程價值在於反映服務真實可用條件。</p>
<p>把 cluster scale-up 想成「加 node 就好」也是常見誤判。當 cluster 規模超過 control plane 預設邊界，etcd / API server 會先撐不住，加 node 反而加重 control plane 負擔。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>部署切換語意可用 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 做回寫。先看事件中的失敗是在 rollout 批次、probe 判斷、還是 drain 時序，再對照本章的 rollout 節奏與停止條件。</p>
<p>這個案例主要支撐的是「部署批次與切換時序」判讀，不直接支撐資料庫交易切分或 consumer 冪等；若問題落在提交一致性或重播補償，應轉到 1.3 或 3.4。</p>
<p>若版本已切換但錯誤率延遲上升，先回到 probe 與 config 相容窗口，再把證據欄位接到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>Kubernetes 部署策略要和觀測、驗證、事故流程同時對齊。</p>
<ol>
<li>與 5.6 的交接：startup / readiness / liveness / drain 的生命週期定義回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 5.1 的交接：image、entrypoint、resource limit 的 runtime 層回到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">container 與 runtime</a>。</li>
<li>與 5.3 的交接：流量承接與退出落在 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.4 的交接：endpoint 註冊與摘除回到 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">service discovery</a>。</li>
<li>與 5.7 的交接：control plane 跟 data plane 邊界落在 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">Traffic、Config 與 Control Plane Boundary</a>。</li>
<li>與 4.20 的交接：版本切換證據進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.8 的交接：放行與停損條件進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 8.19 的交接：部署中止與回退判斷進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把部署與流量切換一起治理，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看切換失敗與回退判讀，接著讀 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>。要看大規模 K8s 容量設計，接著讀 <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games</a> 跟 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34 GCP 130K-node</a>。</p>
]]></content:encoded></item><item><title>6.2 健康檢查與診斷 endpoint</title><link>https://tarrragon.github.io/blog/go-advanced/06-production-operations/health-diagnostics/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/06-production-operations/health-diagnostics/</guid><description>&lt;p>健康檢查與診斷 endpoint 的核心差異是使用者與風險不同。&lt;code>/health&lt;/code> 給監控或負載平衡器判斷 process 是否活著，&lt;code>/ready&lt;/code> 判斷是否應接流量，&lt;code>/debug/...&lt;/code> 則給工程師排查問題且必須限制存取。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>分辨 health、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a>、diagnostics 的語意&lt;/li>
&lt;li>設計快速穩定的 &lt;code>/health&lt;/code>&lt;/li>
&lt;li>用 &lt;code>/ready&lt;/code> 控制是否接新流量&lt;/li>
&lt;li>條件啟用 pprof、runtime stats 等診斷入口&lt;/li>
&lt;li>測試 status code 與 JSON response 合約&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察所有狀態都塞進-health-會讓監控失真">【觀察】所有狀態都塞進 health 會讓監控失真&lt;/h2>
&lt;p>Health endpoint 的核心風險是語意混亂。若 &lt;code>/health&lt;/code> 同時檢查 process、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、外部 API、cache、背景同步，任何依賴短暫波動都可能讓服務被判定死亡。&lt;/p>
&lt;p>問題範例：&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">/health
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ├── process alive?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> ├── database reachable?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ├── queue lag small?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> ├── external API reachable?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> └── background sync fresh?&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這些問題不應全部塞進同一個 endpoint。Process 活著、可接流量、依賴降級、工程診斷，是不同操作訊號。&lt;/p>
&lt;h2 id="判讀healthreadydiagnostics-回答不同問題">【判讀】health、ready、diagnostics 回答不同問題&lt;/h2>
&lt;p>操作 endpoint 的核心設計是每個 endpoint 只回答一個問題。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Endpoint&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>/health&lt;/code>&lt;/td>
 &lt;td>process monitor&lt;/td>
 &lt;td>process 是否基本活著&lt;/td>
 &lt;td>可能重啟 process&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>/ready&lt;/code>&lt;/td>
 &lt;td>load balancer&lt;/td>
 &lt;td>是否應接新流量&lt;/td>
 &lt;td>暫停導流&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>/debug/...&lt;/code>&lt;/td>
 &lt;td>工程師&lt;/td>
 &lt;td>服務內部狀態如何&lt;/td>
 &lt;td>不應公開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>/metrics&lt;/code>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> collector&lt;/td>
 &lt;td>可聚合監控資料&lt;/td>
 &lt;td>監控缺資料&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這樣切分後，某個外部依賴故障不一定要讓 process 被重啟；服務可能只是不 ready，或處於 degraded 狀態。&lt;/p>
&lt;h2 id="執行health-endpoint-應簡單快速">【執行】health endpoint 應簡單快速&lt;/h2>
&lt;p>Health endpoint 的核心責任是快速回答 process 是否能處理基本 HTTP request。它應該簡單、快速、穩定。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">HandleHealth&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Method&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">MethodGet&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;method not allowed&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusMethodNotAllowed&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;application/json&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusOK&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Write&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="nb">byte&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">`{&amp;#34;status&amp;#34;:&amp;#34;ok&amp;#34;}`&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>/health&lt;/code> 不應執行昂貴查詢，也不應依賴大量下游服務。若健康檢查本身很慢，監控會把診斷工具變成新問題。&lt;/p>
&lt;h2 id="執行readiness-控制是否接流量">【執行】readiness 控制是否接流量&lt;/h2>
&lt;p>Readiness 的核心責任是回答「服務現在是否應該接新流量」。它可以檢查啟動狀態、必要依賴、shutdown 狀態。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Readiness&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ready&lt;/span> &lt;span class="nx">atomic&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Bool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">shuttingDown&lt;/span> &lt;span class="nx">atomic&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Bool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Readiness&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Ready&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kt">bool&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ready&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Load&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">shuttingDown&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Load&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&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 class="kd">func&lt;/span> &lt;span class="nf">HandleReady&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">readiness&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Readiness&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">HandlerFunc&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&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 class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;application/json&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">readiness&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Ready&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusServiceUnavailable&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Write&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="nb">byte&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">`{&amp;#34;status&amp;#34;:&amp;#34;not_ready&amp;#34;}`&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusOK&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Write&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="nb">byte&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">`{&amp;#34;status&amp;#34;:&amp;#34;ready&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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>服務啟動尚未完成、必要背景同步尚未就緒、或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a> 已開始時，readiness 應回 &lt;code>503&lt;/code>。Process 仍然活著，但不應接新流量。&lt;/p>
&lt;h2 id="策略dependency-check-依照監控語意分層">【策略】dependency check 依照監控語意分層&lt;/h2>
&lt;p>依賴檢查的核心判斷是故障是否代表 process 應重啟。Database 暫時不可用不一定代表 process 壞掉；重啟可能無法修復，反而造成更多負載。&lt;/p></description><content:encoded><![CDATA[<p>健康檢查與診斷 endpoint 的核心差異是使用者與風險不同。<code>/health</code> 給監控或負載平衡器判斷 process 是否活著，<code>/ready</code> 判斷是否應接流量，<code>/debug/...</code> 則給工程師排查問題且必須限制存取。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>分辨 health、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、diagnostics 的語意</li>
<li>設計快速穩定的 <code>/health</code></li>
<li>用 <code>/ready</code> 控制是否接新流量</li>
<li>條件啟用 pprof、runtime stats 等診斷入口</li>
<li>測試 status code 與 JSON response 合約</li>
</ol>
<hr>
<h2 id="觀察所有狀態都塞進-health-會讓監控失真">【觀察】所有狀態都塞進 health 會讓監控失真</h2>
<p>Health endpoint 的核心風險是語意混亂。若 <code>/health</code> 同時檢查 process、<a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a>、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、外部 API、cache、背景同步，任何依賴短暫波動都可能讓服務被判定死亡。</p>
<p>問題範例：</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">/health
</span></span><span class="line"><span class="ln">2</span><span class="cl">  ├── process alive?
</span></span><span class="line"><span class="ln">3</span><span class="cl">  ├── database reachable?
</span></span><span class="line"><span class="ln">4</span><span class="cl">  ├── queue lag small?
</span></span><span class="line"><span class="ln">5</span><span class="cl">  ├── external API reachable?
</span></span><span class="line"><span class="ln">6</span><span class="cl">  └── background sync fresh?</span></span></code></pre></div><p>這些問題不應全部塞進同一個 endpoint。Process 活著、可接流量、依賴降級、工程診斷，是不同操作訊號。</p>
<h2 id="判讀healthreadydiagnostics-回答不同問題">【判讀】health、ready、diagnostics 回答不同問題</h2>
<p>操作 endpoint 的核心設計是每個 endpoint 只回答一個問題。</p>
<table>
  <thead>
      <tr>
          <th>Endpoint</th>
          <th>使用者</th>
          <th>回答的問題</th>
          <th>失敗影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/health</code></td>
          <td>process monitor</td>
          <td>process 是否基本活著</td>
          <td>可能重啟 process</td>
      </tr>
      <tr>
          <td><code>/ready</code></td>
          <td>load balancer</td>
          <td>是否應接新流量</td>
          <td>暫停導流</td>
      </tr>
      <tr>
          <td><code>/debug/...</code></td>
          <td>工程師</td>
          <td>服務內部狀態如何</td>
          <td>不應公開</td>
      </tr>
      <tr>
          <td><code>/metrics</code></td>
          <td><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> collector</td>
          <td>可聚合監控資料</td>
          <td>監控缺資料</td>
      </tr>
  </tbody>
</table>
<p>這樣切分後，某個外部依賴故障不一定要讓 process 被重啟；服務可能只是不 ready，或處於 degraded 狀態。</p>
<h2 id="執行health-endpoint-應簡單快速">【執行】health endpoint 應簡單快速</h2>
<p>Health endpoint 的核心責任是快速回答 process 是否能處理基本 HTTP request。它應該簡單、快速、穩定。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">HandleHealth</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">if</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Method</span> <span class="o">!=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">MethodGet</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;method not allowed&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusMethodNotAllowed</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <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 class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span> <span class="s">&#34;application/json&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusOK</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">_</span><span class="p">,</span> <span class="nx">_</span> <span class="p">=</span> <span class="nx">w</span><span class="p">.</span><span class="nf">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="s">`{&#34;status&#34;:&#34;ok&#34;}`</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>/health</code> 不應執行昂貴查詢，也不應依賴大量下游服務。若健康檢查本身很慢，監控會把診斷工具變成新問題。</p>
<h2 id="執行readiness-控制是否接流量">【執行】readiness 控制是否接流量</h2>
<p>Readiness 的核心責任是回答「服務現在是否應該接新流量」。它可以檢查啟動狀態、必要依賴、shutdown 狀態。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">Readiness</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ready</span>        <span class="nx">atomic</span><span class="p">.</span><span class="nx">Bool</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">shuttingDown</span> <span class="nx">atomic</span><span class="p">.</span><span class="nx">Bool</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">Readiness</span><span class="p">)</span> <span class="nf">Ready</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">ready</span><span class="p">.</span><span class="nf">Load</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="p">!</span><span class="nx">r</span><span class="p">.</span><span class="nx">shuttingDown</span><span class="p">.</span><span class="nf">Load</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">func</span> <span class="nf">HandleReady</span><span class="p">(</span><span class="nx">readiness</span> <span class="o">*</span><span class="nx">Readiness</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">HandlerFunc</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span> <span class="s">&#34;application/json&#34;</span><span class="p">)</span>
</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 class="k">if</span> <span class="p">!</span><span class="nx">readiness</span><span class="p">.</span><span class="nf">Ready</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusServiceUnavailable</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="nx">_</span><span class="p">,</span> <span class="nx">_</span> <span class="p">=</span> <span class="nx">w</span><span class="p">.</span><span class="nf">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="s">`{&#34;status&#34;:&#34;not_ready&#34;}`</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusOK</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="nx">_</span><span class="p">,</span> <span class="nx">_</span> <span class="p">=</span> <span class="nx">w</span><span class="p">.</span><span class="nf">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="s">`{&#34;status&#34;:&#34;ready&#34;}`</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>服務啟動尚未完成、必要背景同步尚未就緒、或 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 已開始時，readiness 應回 <code>503</code>。Process 仍然活著，但不應接新流量。</p>
<h2 id="策略dependency-check-依照監控語意分層">【策略】dependency check 依照監控語意分層</h2>
<p>依賴檢查的核心判斷是故障是否代表 process 應重啟。Database 暫時不可用不一定代表 process 壞掉；重啟可能無法修復，反而造成更多負載。</p>
<p>建議分層：</p>
<ul>
<li><code>/health</code>：只確認 process alive。</li>
<li><code>/ready</code>：確認必要依賴是否足以接新流量。</li>
<li><code>/diagnostics/dependencies</code>：提供工程師查看細節。</li>
</ul>
<p>診斷 response 可以包含穩定欄位：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;status&#34;</span><span class="p">:</span> <span class="s2">&#34;degraded&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;dependencies&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nt">&#34;database&#34;</span><span class="p">:</span> <span class="s2">&#34;ok&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nt">&#34;queue&#34;</span><span class="p">:</span> <span class="s2">&#34;lagging&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>監控應依賴 status code 與穩定欄位，工程師再用 body 細節診斷問題。自由文字可以輔助閱讀，但不應成為監控規則的依據。</p>
<h2 id="執行diagnostics-endpoint-要條件啟用">【執行】diagnostics endpoint 要條件啟用</h2>
<p>Diagnostics endpoint 的核心用途是提供工程師排查問題的資料。pprof、runtime metrics、internal queue length、goroutine count 都屬於這類。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">RegisterDiagnostics</span><span class="p">(</span><span class="nx">mux</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">ServeMux</span><span class="p">,</span> <span class="nx">enabled</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">enabled</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">mux</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/debug/runtime&#34;</span><span class="p">,</span> <span class="nx">HandleRuntimeStats</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">func</span> <span class="nf">HandleRuntimeStats</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="kd">var</span> <span class="nx">stats</span> <span class="nx">runtime</span><span class="p">.</span><span class="nx">MemStats</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">runtime</span><span class="p">.</span><span class="nf">ReadMemStats</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">stats</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="nx">response</span> <span class="o">:=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">any</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="s">&#34;heap_alloc&#34;</span><span class="p">:</span>  <span class="nx">stats</span><span class="p">.</span><span class="nx">HeapAlloc</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="s">&#34;num_gc&#34;</span><span class="p">:</span>      <span class="nx">stats</span><span class="p">.</span><span class="nx">NumGC</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="s">&#34;goroutines&#34;</span><span class="p">:</span>  <span class="nx">runtime</span><span class="p">.</span><span class="nf">NumGoroutine</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="nx">_</span> <span class="p">=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">NewEncoder</span><span class="p">(</span><span class="nx">w</span><span class="p">).</span><span class="nf">Encode</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Diagnostics 可能揭露內部狀態、記憶體資訊、goroutine 數量、路徑與部署細節，不應公開給一般使用者。若需要長期保留，至少應限制在內網、管理 port、認證或防火牆後。</p>
<h2 id="判讀status-code-是監控合約">【判讀】status code 是監控合約</h2>
<p>健康檢查的核心合約是 status code。監控系統通常先看 HTTP code 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>，不會理解複雜 body。</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>200 OK</code></td>
          <td>符合該 endpoint 的健康條件</td>
      </tr>
      <tr>
          <td><code>503 Service Unavailable</code></td>
          <td>暫時不可用或不應接流量</td>
      </tr>
      <tr>
          <td><code>405 Method Not Allowed</code></td>
          <td>呼叫方式錯誤</td>
      </tr>
      <tr>
          <td>timeout</td>
          <td>endpoint 無法在預期時間內回應</td>
      </tr>
  </tbody>
</table>
<p>Body 可以提供人類可讀資訊，但不應讓監控依賴自由文字。若要機器讀取，使用穩定 JSON 欄位，例如 <code>status</code>、<code>reason</code>、<code>dependencies</code>。</p>
<h2 id="測試endpoint-測試要鎖定-status-code">【測試】endpoint 測試要鎖定 status code</h2>
<p>Endpoint 測試的核心是驗證 status code 與穩定 JSON 欄位，而不是完整自由文字。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestReadyReturnsUnavailableWhenShuttingDown</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">readiness</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">Readiness</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">readiness</span><span class="p">.</span><span class="nx">ready</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">readiness</span><span class="p">.</span><span class="nx">shuttingDown</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">req</span> <span class="o">:=</span> <span class="nx">httptest</span><span class="p">.</span><span class="nf">NewRequest</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">MethodGet</span><span class="p">,</span> <span class="s">&#34;/ready&#34;</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">rec</span> <span class="o">:=</span> <span class="nx">httptest</span><span class="p">.</span><span class="nf">NewRecorder</span><span class="p">()</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 class="nf">HandleReady</span><span class="p">(</span><span class="nx">readiness</span><span class="p">).</span><span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">rec</span><span class="p">,</span> <span class="nx">req</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">if</span> <span class="nx">rec</span><span class="p">.</span><span class="nx">Code</span> <span class="o">!=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusServiceUnavailable</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;status = %d, want %d&#34;</span><span class="p">,</span> <span class="nx">rec</span><span class="p">.</span><span class="nx">Code</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusServiceUnavailable</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Diagnostics endpoint 也應測 gate 關閉時不註冊或回 404，避免診斷入口不小心暴露。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理 health、readiness 與 diagnostics 的語意切分；Prometheus、OpenTelemetry 與平台設定，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go-advanced/07-distributed-operations/observability-pipeline/" data-link-title="7.4 Observability pipeline、metrics 與 tracing" data-link-desc="把 structured log、metric、trace 與 profile 組成可操作的診斷系統">Go 進階：Observability pipeline、metrics 與 tracing</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 pprof、runtime metrics 與 deploy readiness；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go-advanced/03-runtime-profiling/pprof/" data-link-title="3.2 pprof 基礎診斷流程" data-link-desc="用 pprof endpoint 診斷 heap、goroutine 與 CPU 問題">Go：pprof 基礎診斷流程</a></li>
<li><a href="/blog/go-advanced/03-runtime-profiling/gc-memory-limit/" data-link-title="3.1 GC 與 memory limit" data-link-desc="理解 debug.SetMemoryLimit 在長時間服務中的用途">Go：GC 與 memory limit</a></li>
<li><a href="/blog/go/03-stdlib/slog/" data-link-title="3.6 log/slog：結構化日誌" data-link-desc="用 key-value log 設計可查詢、可過濾的程式訊號">Go：結構化日誌</a></li>
<li><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go：graceful shutdown 與 signal handling</a></li>
<li><a href="/blog/go-advanced/07-distributed-operations/deployment-contracts/" data-link-title="7.5 Kubernetes、systemd 與 load balancer 合約" data-link-desc="理解部署平台如何影響 Go 服務的 shutdown、health 與資源限制">Go 進階：Kubernetes、systemd 與 load balancer 合約</a></li>
<li><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Backend：可觀測性平台</a></li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a></li>
</ul>
<h2 id="小結">小結</h2>
<p><code>/health</code>、<code>/ready</code>、diagnostics endpoint 解決不同問題。Health 檢查 process 基本可用性，readiness 控制是否接新流量，diagnostics 支援工程排查且應限制存取。Status code 是監控合約，JSON body 是補充細節；把這些訊號混在一起會讓操作判斷與安全邊界都變模糊。</p>
]]></content:encoded></item><item><title>5.C3 Orbitera：遷移到 Managed Kubernetes</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/</guid><description>&lt;p>這個案例的核心責任是說明平台遷移的關鍵在服務連續性與能力重建，單次技術替換只是其中一步。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Orbitera 原本在 AWS 上以 EC2 為基礎運行 monolithic 架構，使用 EC2 + S3 + RDS + RedShift 組合。被 Google Cloud 收購後，在產品持續運作的前提下遷移到 Google Kubernetes Engine（GKE），同時從 monolith 重構為 microservices 架構。&lt;/p>
&lt;p>遷移後的架構運行在 multi-zone 配置下，每個 zone 維持 3 個 replica，確保單一 zone 故障時服務不中斷。整合 Cloud SQL（取代 RDS）、Google 的 load balancer、Stackdriver（觀測）。遷移完成後取得的操作能力包含 on-demand scaling、快速部署到新 region/zone、以及快速 rollback 失敗的 build。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>跨平台遷移本質是能力遷移：部署、觀測、恢復與團隊流程都需要同步重建。Orbitera 的遷移同時改變了兩個維度——平台（AWS → GCP）和架構（monolith → microservices）。雙維度同時改變放大了遷移風險，但也讓團隊避免了「先遷平台再拆架構」的兩階段成本。&lt;/p>
&lt;p>這個案例揭露的隱性工作量在「能力對等重建」。原本在 AWS 上已經建好的觀測（CloudWatch → Stackdriver）、資料庫操作（RDS → Cloud SQL）、load balancing 都要在新平台上重新建立並驗證。這些能力不會隨著 workload 遷移自動出現——需要明確的 checklist 和驗證流程。&lt;/p>
&lt;p>monolith → microservices 的架構重構改變了 runtime 的基本假設。Monolith 的 readiness 是單一進程啟動完成；microservices 的 readiness 涉及多個服務之間的依賴就緒。&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 的 readiness 設計取捨在這類重構後需要重新定義——哪些是必要依賴、哪些是可降級依賴，從 monolith 時代的「全部在同一個進程」變成需要顯式判斷。&lt;/p>
&lt;p>Multi-zone HA（3 replicas/zone）是遷移後 managed 平台提供的基線能力。在 self-managed 環境下實現相同程度的跨 zone 冗餘需要大量手動配置（zone-aware scheduling、cross-zone load balancing）；managed 平台把這些收進平台層，團隊精力從「維持 HA 運作」轉向「定義 HA 目標」。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>先驗證新平台的最小可行服務&lt;/strong>：選擇一個依賴少、風險低的服務在 GKE 上完成完整 deployment cycle（build → deploy → observe → rollback），驗證 CI/CD pipeline、觀測整合、rollback 路徑都可運作。&lt;/li>
&lt;li>&lt;strong>建立能力對等 checklist&lt;/strong>：列出舊平台已有的操作能力（觀測、告警、backup、secret 管理、log 收集），逐一確認新平台有對應方案且經過驗證。未對等的能力是遷移的 blocking 條件。&lt;/li>
&lt;li>&lt;strong>逐步搬遷核心工作負載&lt;/strong>：按依賴關係排序遷移批次，保留舊平台的回切路徑。每批遷移後在新平台上跑 load test 驗證容量與恢復能力。&lt;/li>
&lt;li>&lt;strong>把平台能力納入日常治理節奏&lt;/strong>：遷移完成不是終點——GKE 版本升級、node pool 更新、Cloud SQL 維護窗口都要進入團隊的日常操作流程，避免遷移後進入「只部署不維護」的狀態。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫的章節段落">可回寫的章節段落&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 Container Runtime — 遷移期的 Runtime 穩定性&lt;/a>：monolith → microservices 改變 image 建置策略與啟動行為&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract — 遷移期的 Lifecycle 重新驗證&lt;/a>：readiness 條件在架構重構後需重新定義&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR/Rollback Rehearsal&lt;/a>：遷移後的回退路徑驗證&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://cloud.google.com/blog/products/gcp/why-we-migrated-orbitera-to-managed-kubernetes-on-google-cloud-platform/">Why we migrated Orbitera to managed Kubernetes on Google Cloud Platform&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明平台遷移的關鍵在服務連續性與能力重建，單次技術替換只是其中一步。</p>
<h2 id="觀察">觀察</h2>
<p>Orbitera 原本在 AWS 上以 EC2 為基礎運行 monolithic 架構，使用 EC2 + S3 + RDS + RedShift 組合。被 Google Cloud 收購後，在產品持續運作的前提下遷移到 Google Kubernetes Engine（GKE），同時從 monolith 重構為 microservices 架構。</p>
<p>遷移後的架構運行在 multi-zone 配置下，每個 zone 維持 3 個 replica，確保單一 zone 故障時服務不中斷。整合 Cloud SQL（取代 RDS）、Google 的 load balancer、Stackdriver（觀測）。遷移完成後取得的操作能力包含 on-demand scaling、快速部署到新 region/zone、以及快速 rollback 失敗的 build。</p>
<h2 id="判讀">判讀</h2>
<p>跨平台遷移本質是能力遷移：部署、觀測、恢復與團隊流程都需要同步重建。Orbitera 的遷移同時改變了兩個維度——平台（AWS → GCP）和架構（monolith → microservices）。雙維度同時改變放大了遷移風險，但也讓團隊避免了「先遷平台再拆架構」的兩階段成本。</p>
<p>這個案例揭露的隱性工作量在「能力對等重建」。原本在 AWS 上已經建好的觀測（CloudWatch → Stackdriver）、資料庫操作（RDS → Cloud SQL）、load balancing 都要在新平台上重新建立並驗證。這些能力不會隨著 workload 遷移自動出現——需要明確的 checklist 和驗證流程。</p>
<p>monolith → microservices 的架構重構改變了 runtime 的基本假設。Monolith 的 readiness 是單一進程啟動完成；microservices 的 readiness 涉及多個服務之間的依賴就緒。<a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 的 readiness 設計取捨在這類重構後需要重新定義——哪些是必要依賴、哪些是可降級依賴，從 monolith 時代的「全部在同一個進程」變成需要顯式判斷。</p>
<p>Multi-zone HA（3 replicas/zone）是遷移後 managed 平台提供的基線能力。在 self-managed 環境下實現相同程度的跨 zone 冗餘需要大量手動配置（zone-aware scheduling、cross-zone load balancing）；managed 平台把這些收進平台層，團隊精力從「維持 HA 運作」轉向「定義 HA 目標」。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>先驗證新平台的最小可行服務</strong>：選擇一個依賴少、風險低的服務在 GKE 上完成完整 deployment cycle（build → deploy → observe → rollback），驗證 CI/CD pipeline、觀測整合、rollback 路徑都可運作。</li>
<li><strong>建立能力對等 checklist</strong>：列出舊平台已有的操作能力（觀測、告警、backup、secret 管理、log 收集），逐一確認新平台有對應方案且經過驗證。未對等的能力是遷移的 blocking 條件。</li>
<li><strong>逐步搬遷核心工作負載</strong>：按依賴關係排序遷移批次，保留舊平台的回切路徑。每批遷移後在新平台上跑 load test 驗證容量與恢復能力。</li>
<li><strong>把平台能力納入日常治理節奏</strong>：遷移完成不是終點——GKE 版本升級、node pool 更新、Cloud SQL 維護窗口都要進入團隊的日常操作流程，避免遷移後進入「只部署不維護」的狀態。</li>
</ol>
<h2 id="可回寫的章節段落">可回寫的章節段落</h2>
<ul>
<li><a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 Container Runtime — 遷移期的 Runtime 穩定性</a>：monolith → microservices 改變 image 建置策略與啟動行為</li>
<li><a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract — 遷移期的 Lifecycle 重新驗證</a>：readiness 條件在架構重構後需重新定義</li>
<li><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR/Rollback Rehearsal</a>：遷移後的回退路徑驗證</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/gcp/why-we-migrated-orbitera-to-managed-kubernetes-on-google-cloud-platform/">Why we migrated Orbitera to managed Kubernetes on Google Cloud Platform</a></li>
</ul>
]]></content:encoded></item><item><title>systemd</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/systemd/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/systemd/</guid><description>&lt;p>systemd 是 Linux 主流 init system、承擔三個責任：service unit lifecycle（start / stop / restart / reload）、signal + journald + cgroups 整合、socket activation + timer（cron 替代）。設計取捨偏向「OS-level 整合 + 單機資源管理 + dependency graph」、適合 VM / bare metal 上單機服務、不需要 cluster orchestration 的場景。&lt;/p>
&lt;p>對「VM / bare metal 服務管理、邊緣 / appliance、單機 lifecycle + journal + cgroups」這條路徑、systemd 是 Linux 主流選擇。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 service unit file、配置 Type / Restart / ExecStart&lt;/li>
&lt;li>設計 signal handling + graceful shutdown&lt;/li>
&lt;li>用 journald + journalctl 查 logs&lt;/li>
&lt;li>設定 cgroups v2 resource limit&lt;/li>
&lt;li>用 socket activation / timer 替代 inetd / cron&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-systemd-service-跑起來">最短路徑：5 分鐘把 systemd service 跑起來&lt;/h2>





&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"># 1. 建 unit file（需 root 或 sudo）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">cat &amp;gt; /etc/systemd/system/myapp.service &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;UNIT&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s">[Unit]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s">Description=My Application
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s">After=network.target
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s">[Service]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s">ExecStart=/usr/bin/myapp --config /etc/myapp/config.yaml
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s">Restart=on-failure
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">RestartSec=5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s">[Install]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s">WantedBy=multi-user.target
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s">UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 啟用 + 啟動&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">systemctl daemon-reload
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">systemctl &lt;span class="nb">enable&lt;/span> --now myapp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">systemctl status myapp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">journalctl -u myapp -f&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="unit-file-設計">Unit file 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Unit type：service / socket / timer / target / mount / path&lt;/li>
&lt;li>Service Type：simple / forking / oneshot / notify / dbus&lt;/li>
&lt;li>Restart：no / on-failure / on-abnormal / always&lt;/li>
&lt;li>ExecStart / ExecStop / ExecReload&lt;/li>
&lt;li>對應指令：&lt;code>systemctl cat myapp.service&lt;/code>、&lt;code>systemctl edit&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="systemctl-指令">systemctl 指令&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Lifecycle：start / stop / restart / reload / enable / disable&lt;/li>
&lt;li>Status：status / is-active / is-enabled / list-units&lt;/li>
&lt;li>Reload after edit：daemon-reload&lt;/li>
&lt;li>對應指令範例：&lt;code>systemctl status myapp&lt;/code>、&lt;code>systemctl list-units --failed&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="journald-日誌">journald 日誌&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>systemd 是 Linux 主流 init system、承擔三個責任：service unit lifecycle（start / stop / restart / reload）、signal + journald + cgroups 整合、socket activation + timer（cron 替代）。設計取捨偏向「OS-level 整合 + 單機資源管理 + dependency graph」、適合 VM / bare metal 上單機服務、不需要 cluster orchestration 的場景。</p>
<p>對「VM / bare metal 服務管理、邊緣 / appliance、單機 lifecycle + journal + cgroups」這條路徑、systemd 是 Linux 主流選擇。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 service unit file、配置 Type / Restart / ExecStart</li>
<li>設計 signal handling + graceful shutdown</li>
<li>用 journald + journalctl 查 logs</li>
<li>設定 cgroups v2 resource limit</li>
<li>用 socket activation / timer 替代 inetd / cron</li>
</ol>
<h2 id="最短路徑5-分鐘把-systemd-service-跑起來">最短路徑：5 分鐘把 systemd service 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 建 unit file（需 root 或 sudo）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">cat &gt; /etc/systemd/system/myapp.service <span class="s">&lt;&lt;&#39;UNIT&#39;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">[Unit]
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">Description=My Application
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">After=network.target
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">[Service]
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">ExecStart=/usr/bin/myapp --config /etc/myapp/config.yaml
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">Restart=on-failure
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">RestartSec=5
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">[Install]
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">WantedBy=multi-user.target
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">UNIT</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 2. 啟用 + 啟動</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">systemctl daemon-reload
</span></span><span class="line"><span class="ln">18</span><span class="cl">systemctl <span class="nb">enable</span> --now myapp
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 3. 驗證</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">systemctl status myapp
</span></span><span class="line"><span class="ln">22</span><span class="cl">journalctl -u myapp -f</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="unit-file-設計">Unit file 設計</h3>
<p>子議題：</p>
<ul>
<li>Unit type：service / socket / timer / target / mount / path</li>
<li>Service Type：simple / forking / oneshot / notify / dbus</li>
<li>Restart：no / on-failure / on-abnormal / always</li>
<li>ExecStart / ExecStop / ExecReload</li>
<li>對應指令：<code>systemctl cat myapp.service</code>、<code>systemctl edit</code></li>
</ul>
<h3 id="systemctl-指令">systemctl 指令</h3>
<p>子議題：</p>
<ul>
<li>Lifecycle：start / stop / restart / reload / enable / disable</li>
<li>Status：status / is-active / is-enabled / list-units</li>
<li>Reload after edit：daemon-reload</li>
<li>對應指令範例：<code>systemctl status myapp</code>、<code>systemctl list-units --failed</code></li>
</ul>
<h3 id="journald-日誌">journald 日誌</h3>
<p>子議題：</p>
<ul>
<li>結構化日誌（kv pairs）</li>
<li>journalctl filter（-u / &ndash;since / -p / -f）</li>
<li>對應 logging：persistent vs runtime journal</li>
<li>跟外部 log forwarder（Vector / Fluent Bit）對接</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="signal-handling--graceful-shutdown">Signal handling + graceful shutdown</h3>
<p>子議題：</p>
<ul>
<li>SIGTERM（default stop signal）/ SIGKILL（force kill after timeout）</li>
<li>TimeoutStopSec：grace period</li>
<li>應用程式要 trap SIGTERM 做 cleanup</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform lifecycle contract</a>（concept 通用）</li>
</ul>
<h3 id="cgroups-v2--resource-limit">cgroups v2 + resource limit</h3>
<p>子議題：</p>
<ul>
<li>CPUQuota / MemoryMax / IOWeight / TasksMax</li>
<li>Slice unit（樹狀 resource 限制）</li>
<li>跟 Kubernetes 的 resource limit 對比（K8s 用 cgroups 但抽象更高）</li>
<li>對應指令：<code>systemd-cgls</code>、<code>systemd-cgtop</code></li>
</ul>
<h3 id="socket-activation">Socket activation</h3>
<p>子議題：</p>
<ul>
<li>用 .socket unit 持有 listening socket、service 啟動時繼承</li>
<li>啟動延遲：socket 一直在、service 按需起</li>
<li>替代 inetd</li>
<li>適合 occasional service / low-traffic</li>
</ul>
<h3 id="systemd-timer">systemd timer</h3>
<p>子議題：</p>
<ul>
<li>.timer unit 替代 cron</li>
<li>OnCalendar / OnUnitActiveSec / RandomizedDelaySec</li>
<li>跟對應 .service unit 配對</li>
<li>比 cron 強：journal log / dependency / 失敗 restart</li>
</ul>
<h3 id="portable-services--systemd-run">Portable services + systemd-run</h3>
<p>子議題：</p>
<ul>
<li>systemd-run：ad-hoc 跑 transient unit</li>
<li>Portable services：把 service + image 一起搬</li>
<li>systemd-nspawn 容器（systemd 自家輕量容器）</li>
</ul>
<h3 id="跟-container-整合">跟 container 整合</h3>
<p>子議題：</p>
<ul>
<li>跑 podman container 在 systemd（quadlet / generators）</li>
<li>Docker daemon 由 systemd 管</li>
<li>K8s kubelet 由 systemd 管（cluster node）</li>
<li>對應 single-node container management</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="service-start-failure">Service start failure</h3>
<p>操作原則：先 <code>systemctl status</code>、再 <code>journalctl -u</code> 看 log。</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">systemctl status myapp                <span class="c1"># 看 Active state + Main PID + 最近 log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">journalctl -u myapp --since<span class="o">=</span>-5m       <span class="c1"># 最近 5 分鐘的完整 log</span></span></span></code></pre></div><h3 id="restart-loop">Restart loop</h3>
<p>操作原則：Restart 配置不當 + StartLimit 觸發。判讀：<code>systemctl status</code> 看 restart count + RateLimit。</p>
<h3 id="journald-disk-full">journald disk full</h3>
<p>操作原則：journal storage 超 SystemMaxUse 設定。判讀：<code>journalctl --disk-usage</code>、<code>/etc/systemd/journald.conf</code> 設限。</p>
<h3 id="cgroup-oom">cgroup OOM</h3>
<p>操作原則：MemoryMax 超過、系統 OOM kill。判讀：<code>journalctl -k</code> 看 kernel oom 訊息。</p>
<h3 id="dependency-不對">Dependency 不對</h3>
<p>操作原則：unit 依賴 network / db 但 After= 沒設。判讀：<code>systemctl list-dependencies myapp</code>。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多實例 cluster</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a></td>
      </tr>
      <tr>
          <td>Container workflow 為主</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a> / Podman</td>
      </tr>
      <tr>
          <td>Process supervisor（非 init）</td>
          <td>supervisord / runit</td>
      </tr>
      <tr>
          <td>Cron-only 場景</td>
          <td>純 cron / systemd timer</td>
      </tr>
      <tr>
          <td>Non-Linux（Windows / macOS）</td>
          <td>Windows Service / launchd</td>
      </tr>
      <tr>
          <td>邊緣 K8s</td>
          <td>K3s（systemd 上跑 K3s）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 unit file directive reference</li>
<li>systemd internals（dbus / pid 1）</li>
<li>各 distro systemd 版本差異</li>
<li>systemd-resolved / systemd-networkd 等其他 component</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 systemd 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>systemd 服務切換要靠 ExecStop / TimeoutStopSec / SIGTERM trap 等價 drain</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小規模 VM 服務首選 systemd、跨規模升階到 K8s 時要保留 unit-level 回退腳本</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 systemd 案例</strong>：大規模 fleet（HashiCorp Nomad 跟 systemd 整合）、IoT / edge appliance 案例、systemd portable services 落地案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a>、<a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a></li>
<li>下游能力：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 reliability</a>（graceful shutdown）、<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a>（journald）</li>
</ul>
]]></content:encoded></item><item><title>5.3 load balancer 合約</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/</guid><description>&lt;p>流量平衡合約（load balancer contract）的核心責任是定義平台何時把流量交給服務，以及服務何時安全退出流量。這份合約一旦模糊，部署、擴容、回退與事故處理都會出現同型問題。&lt;/p>
&lt;h2 id="contract-組成">contract 組成&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">Load Balancer Contract&lt;/a> 可以拆成四個部分：&lt;/p>
&lt;ol>
&lt;li>routing contract：哪些路徑導向哪些服務，如何處理權重與版本。&lt;/li>
&lt;li>health contract：哪些訊號代表可接流量，何時摘除節點。&lt;/li>
&lt;li>connection contract：長短連線的 idle timeout、keepalive、重試規則。&lt;/li>
&lt;li>drain contract：版本切換時如何讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight&lt;/a> request 安全收斂。&lt;/li>
&lt;/ol>
&lt;p>這四個部分共同定義 rollout 的穩定性。服務端 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 與平台端健康檢查要對位，否則會出現「服務已啟動但尚未可服務」的切換抖動。&lt;/p>
&lt;h2 id="draining-與-shutdown">draining 與 shutdown&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining&lt;/a> 的責任是讓舊實例在下線前完成現有請求。drain 視窗的 workload 分類詳見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a>，本段聚焦 LB 如何配合 drain：短請求 API 的 drain 視窗可較短；長連線、串流或 websocket 場景需要更長窗口與明確 reconnect 策略。&lt;/p>
&lt;p>部署流程中，LB 摘流量、服務停止接新請求、服務完成在途請求、實例退出，這四步要有固定順序。順序穩定後，rollback 才能在同一套機制下運作。&lt;/p>
&lt;h2 id="timeout-與-sticky-session">timeout 與 sticky session&lt;/h2>
&lt;p>idle &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 是連線資源與使用者體驗的平衡點。timeout 太短會增加重連與錯誤，太長會占用連線與資源。設定時依請求型態與峰值流量校準、按 SLI 訊號迭代閾值。&lt;/p>
&lt;h3 id="timeout-層級串聯">Timeout 層級串聯&lt;/h3>
&lt;p>一條請求路徑上的 timeout 分佈在多個層級，每層各自有預設值。全路徑的 timeout 設計原則是由外到內遞減：外層（離使用者近）的 timeout 要大於內層（離資料源近），否則外層先放棄，內層還在處理一個已經沒人等的請求。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>典型 timeout 範圍&lt;/th>
 &lt;th>設定位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Client / Browser&lt;/td>
 &lt;td>30-120 秒&lt;/td>
 &lt;td>前端 fetch / axios / SDK 設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN edge&lt;/td>
 &lt;td>5-30 秒&lt;/td>
 &lt;td>CDN vendor 設定（Cloudflare / CloudFront）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Load balancer&lt;/td>
 &lt;td>30-60 秒&lt;/td>
 &lt;td>LB idle timeout / request timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application&lt;/td>
 &lt;td>5-30 秒&lt;/td>
 &lt;td>HTTP server read/write timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database / Cache&lt;/td>
 &lt;td>1-5 秒&lt;/td>
 &lt;td>連線池 query timeout / connect timeout&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的每一層 timeout 都要比它的下一層大。如果 LB timeout 30 秒但 application 設了 60 秒，LB 會在 30 秒回 504 給使用者，但 application 仍然持有連線等 DB 回應——佔用連線資源卻無法交付結果。&lt;/p>
&lt;p>timeout 設計的常見失誤是只調 LB 層：團隊看到使用者回報 timeout，直接把 LB timeout 從 30 秒調到 120 秒。結果是慢請求佔用 LB 連線更久、連線池被慢請求填滿、其他正常請求也開始排隊 timeout。穩定做法是先在 application 或 DB 層找出延遲根因，而非放大外層 timeout 來「等更久」。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session&lt;/a> 適合需要短期會話一致性的場景，但它會提高特定節點負載不均與失效轉移成本。採用 sticky policy 前要先定義會話狀態落點與失效時的回復路徑。&lt;/p></description><content:encoded><![CDATA[<p>流量平衡合約（load balancer contract）的核心責任是定義平台何時把流量交給服務，以及服務何時安全退出流量。這份合約一旦模糊，部署、擴容、回退與事故處理都會出現同型問題。</p>
<h2 id="contract-組成">contract 組成</h2>
<p><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">Load Balancer Contract</a> 可以拆成四個部分：</p>
<ol>
<li>routing contract：哪些路徑導向哪些服務，如何處理權重與版本。</li>
<li>health contract：哪些訊號代表可接流量，何時摘除節點。</li>
<li>connection contract：長短連線的 idle timeout、keepalive、重試規則。</li>
<li>drain contract：版本切換時如何讓 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request 安全收斂。</li>
</ol>
<p>這四個部分共同定義 rollout 的穩定性。服務端 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 與平台端健康檢查要對位，否則會出現「服務已啟動但尚未可服務」的切換抖動。</p>
<h2 id="draining-與-shutdown">draining 與 shutdown</h2>
<p><a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 的責任是讓舊實例在下線前完成現有請求。drain 視窗的 workload 分類詳見 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>，本段聚焦 LB 如何配合 drain：短請求 API 的 drain 視窗可較短；長連線、串流或 websocket 場景需要更長窗口與明確 reconnect 策略。</p>
<p>部署流程中，LB 摘流量、服務停止接新請求、服務完成在途請求、實例退出，這四步要有固定順序。順序穩定後，rollback 才能在同一套機制下運作。</p>
<h2 id="timeout-與-sticky-session">timeout 與 sticky session</h2>
<p>idle <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 是連線資源與使用者體驗的平衡點。timeout 太短會增加重連與錯誤，太長會占用連線與資源。設定時依請求型態與峰值流量校準、按 SLI 訊號迭代閾值。</p>
<h3 id="timeout-層級串聯">Timeout 層級串聯</h3>
<p>一條請求路徑上的 timeout 分佈在多個層級，每層各自有預設值。全路徑的 timeout 設計原則是由外到內遞減：外層（離使用者近）的 timeout 要大於內層（離資料源近），否則外層先放棄，內層還在處理一個已經沒人等的請求。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>典型 timeout 範圍</th>
          <th>設定位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Client / Browser</td>
          <td>30-120 秒</td>
          <td>前端 fetch / axios / SDK 設定</td>
      </tr>
      <tr>
          <td>CDN edge</td>
          <td>5-30 秒</td>
          <td>CDN vendor 設定（Cloudflare / CloudFront）</td>
      </tr>
      <tr>
          <td>Load balancer</td>
          <td>30-60 秒</td>
          <td>LB idle timeout / request timeout</td>
      </tr>
      <tr>
          <td>Application</td>
          <td>5-30 秒</td>
          <td>HTTP server read/write timeout</td>
      </tr>
      <tr>
          <td>Database / Cache</td>
          <td>1-5 秒</td>
          <td>連線池 query timeout / connect timeout</td>
      </tr>
  </tbody>
</table>
<p>這張表的每一層 timeout 都要比它的下一層大。如果 LB timeout 30 秒但 application 設了 60 秒，LB 會在 30 秒回 504 給使用者，但 application 仍然持有連線等 DB 回應——佔用連線資源卻無法交付結果。</p>
<p>timeout 設計的常見失誤是只調 LB 層：團隊看到使用者回報 timeout，直接把 LB timeout 從 30 秒調到 120 秒。結果是慢請求佔用 LB 連線更久、連線池被慢請求填滿、其他正常請求也開始排隊 timeout。穩定做法是先在 application 或 DB 層找出延遲根因，而非放大外層 timeout 來「等更久」。</p>
<p><a href="/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session</a> 適合需要短期會話一致性的場景，但它會提高特定節點負載不均與失效轉移成本。採用 sticky policy 前要先定義會話狀態落點與失效時的回復路徑。</p>
<h3 id="lb--cdn-連線生命週期協調">LB + CDN 連線生命週期協調</h3>
<p>當 LB 上游有 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">CDN</a> 時、兩層的 timeout / retry 行為要對齊、否則會出現「使用者已經 timeout 但 origin 還在處理」這類雙層不一致：</p>
<ul>
<li><strong>CDN edge timeout</strong> 通常比 origin LB timeout 短（5-30 秒）— edge 認定 origin 慢就放棄。若 origin LB timeout 是 60 秒、edge 在 30 秒已放棄回 504、origin 還在處理一個沒人在意的 request。應對齊兩邊的 timeout 上限。</li>
<li><strong>CDN retry policy</strong> 在 edge miss 後若拿不到 origin response、預設不會重試（避免雙倍 origin 流量）— LB 端的 idle timeout 設計要假設「只有一次機會」、不依賴上游重試</li>
<li><strong>長連線（WebSocket、SSE、gRPC）通常繞過 CDN</strong> — 直接連到 origin LB。這些連線的 idle timeout 跟一般 HTTP 不同、要單獨配置</li>
<li><strong>Edge cache HIT 時 LB 完全沒收到 request</strong> — 容量規劃時要把 cache hit ratio 算進 origin RPS、不是用使用者 RPS 直接 size LB</li>
</ul>
<p>詳見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a> 的 origin protection 段。</p>
<h2 id="切流失敗的回退判讀">切流失敗的回退判讀</h2>
<p>切流失敗的回退判讀第一步是先分辨「平台問題」跟「流量生命週期問題」、再決定回退手法。平台問題用重啟服務恢復、流量生命週期問題用凍結切換並等待震盪收斂。回退手法錯位會把事故推進第二階段。</p>
<p>切流失敗的本質是 connection lifecycle 跟切換時序錯位、平台元件本身往往是健康的。對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：平台切流未先 Draining</a>：揭露切流失敗常因 connection lifecycle 管理錯位、重啟動作會放大震盪。以下基於通用工程知識展開回退節奏。</p>
<p>回退節奏有兩個時序階段、性質不同。</p>
<p><strong>第一階段：先讓震盪不擴大</strong>。發現切流失敗的第一動作是凍結 rollout（不再擴大切換範圍）跟恢復舊入口權重（把 LB 規則 / DNS 加權 / service mesh 流量切回舊版本主導）。新版本不立即關閉、保留作為對照證據。這個階段的目標是穩定當前狀態、為後續分析爭取時間、所有動作要在分鐘級內完成。</p>
<p><strong>第二階段：再讓系統可恢復</strong>。震盪不擴大後、進入「等待 + 修正」狀態。長連線跟 reconnect 風暴需要時間消化、盲目重啟新版本實例會把重連集中在新一輪實例上、造成 thundering herd。觀察連線數、reconnect rate、5xx 趨勢回到 baseline 是進入修正階段的訊號。修正動作聚焦於 drain window、idle timeout、health check、client retry 之間的節奏錯位、找出後修正、重新進入小範圍驗證。這個階段的時間尺度通常是小時級、不能用第一階段的緊急節奏對待。</p>
<p>兩階段時序不能合併。把第一階段（凍結 + 切回）跟第二階段（等待 + 修正）並列執行、會在連線尚未穩定時嘗試修正、造成第二次震盪。</p>
<p>回退時最常見的誤判是「LB 顯示新節點 healthy = 服務可服務」。LB 的健康判斷通常是定期 health check 通過，跟「該節點能承受重連潮」是不同問題。事故中要把這兩個訊號分開看：節點層健康（health check pass）、連線層健康（reconnect rate、長連線錯誤率、tail latency）。</p>
<h2 id="切流告警條件">切流告警條件</h2>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 的「部署專屬告警條件」段：揭露切流期告警的三個核心訊號（批次內 5xx 突增、長連線重連率快速上升、rollback time 超過既定 RTO）。本段在 case 三條基礎上補第 4 條（per-version error rate 偏離）與操作建議。</p>
<p>切流期告警的核心責任是對應切流批次節奏、跟日常閾值分離。日常閾值在切流期會被切換本身的短暫波動觸發、變成 alert noise；切流期需要更嚴格的「批次內偏差」訊號。</p>
<p>可操作的切流期告警條件：</p>
<ul>
<li><strong>批次內 5xx 異常升高</strong>：當前批次相對於前一批的 5xx 升幅超過閾值、停止下一批。</li>
<li><strong>長連線重連率飆升</strong>：reconnect rate 超過 baseline N 倍、暗示 drain / timeout 錯位。</li>
<li><strong>回退時間超過 RTO</strong>：執行回退後恢復時間超過既定 RTO、升級為事故等級。</li>
<li><strong>per-version error rate 偏離</strong>：新舊版本 error rate 差距超過閾值、不收斂（屬本章補強、case 未明示）。</li>
</ul>
<p>這些告警的閾值要在 release plan 中先定義、進事故時直接套用、避免臨時拍定。把切流告警跟一般日常告警分流到不同 channel，避免事故團隊在切流期被日常 noise 淹沒。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rollout 期間 5xx 上升且集中在舊版本</td>
          <td>drain 順序或窗口不足</td>
          <td>拉長 drain 時間、調整摘流順序</td>
      </tr>
      <tr>
          <td>readiness 通過但首批請求延遲高</td>
          <td>應用啟動完成與可服務條件未對齊</td>
          <td>細化 readiness 指標、補 startup gate</td>
      </tr>
      <tr>
          <td>reconnect storm 出現在切版後</td>
          <td>timeout 與連線生命週期不匹配</td>
          <td>調整 idle timeout、分批切流</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a> 比例低時正常，擴到高比例出現抖動</td>
          <td>LB 權重策略與服務容量曲線不一致</td>
          <td>降低增量批次、補容量保護</td>
      </tr>
      <tr>
          <td>多租戶場景下單租戶延遲飆升</td>
          <td>sticky/routing policy 造成熱點聚集</td>
          <td>分離租戶路由、加入負載重平衡</td>
      </tr>
      <tr>
          <td>回退後 reconnect 風暴持續</td>
          <td>重啟動作放大震盪、未先恢復穩定路徑</td>
          <td>凍結切換、等連線數穩定、再修錯位點</td>
      </tr>
  </tbody>
</table>
<p>「回退後 reconnect 風暴持續」是切流事故中最容易誤判的訊號。判讀順序：先看是否「凍結切換」已執行（rollout 是否真的停了）、再看「舊入口權重」是否回到主導比例（DNS / LB 規則是否切回）、最後看連線數曲線是否進入下降。三項都做完仍見風暴持續、才考慮新版本實例層級的問題（image / config / runtime 漂移）、而非反向重啟新版本。解凍切換的條件是「連線數曲線回到 baseline + reconnect rate 低於閾值連續 N 分鐘」、不是「等夠久了就解凍」的時間導向。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把 load balancer 當成「只做轉發」的元件，會忽略它在部署與事故中的決策角色。LB 設定定義了流量切換節奏、回退可行性與故障擴散速度。</p>
<p>Health check 跟 readiness 的混淆會在切換時暴露隱性風險。health contract 要反映服務真實 readiness — 含依賴連線池、必要 config、關鍵背景任務狀態 — 而非停在單一探針成功訊號。</p>
<p>把「LB 顯示節點 healthy」當作「服務可承受流量」的訊號，也是事故中的常見誤判。健康檢查通過跟承受重連潮是不同層級的訊號。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>流量契約可用 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 回寫。先看事件中的摘流量順序、drain 視窗與連線重建節奏，再回到本章判讀 connection contract 與 drain contract 是否對齊。</p>
<p>這個案例主要支撐的是「連線生命週期與摘流量順序」判讀，不直接支撐 container build 可重現性；若根因在映像與 runtime 漂移，應回到 5.1。</p>
<p>當回退後錯誤率仍高或重連風暴延續，通常表示 timeout 與 sticky policy 仍在放大舊連線狀態。先重建連線生命週期時序，再把回退判斷同步到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>load balancer contract 是部署平台與操作控制面的匯流點。</p>
<ol>
<li>與 5.6 的交接：drain 的生命週期定義與 workload 分類回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 04 的交接：版本切換訊號與錯誤率證據進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 06 的交接：canary 放行與回退條件進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 07 的交接：入口治理與管理面保護進入 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>。</li>
<li>與 08 的交接：切換與回退判斷記錄到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
<li>與 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發</a> 的交接：CDN 是 origin LB 的上游、edge miss 後流量進 origin LB、timeout / retry 設定要協調。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 LB 合約放進整體部署流程，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a> 與 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>。要把部署切換接到事故流程，接著讀 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
]]></content:encoded></item><item><title>5.C4 Mobileye：Workloads 遷移到 EKS</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/</guid><description>&lt;p>這個案例的核心責任是把 workload 遷移從基礎設施作業改成服務可用性作業。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Mobileye 將大規模工作負載遷移到 EKS。遷移動機集中在運維一致性與可用性治理——原有環境中不同團隊各自維護部署流程，升級節奏、監控覆蓋、容量規劃的標準不統一。遷移目標是用 managed 平台統一這些操作基線，讓各團隊可以專注在 workload 本身。&lt;/p>
&lt;p>遷移範圍涵蓋多種 workload 類型：API 服務、資料處理 pipeline、ML 推論服務。這些 workload 的啟動時間、資源需求、drain 條件差異顯著，同一套遷移策略無法直接套用。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>工作負載遷移若缺乏分段驗證，容易在切流時放大依賴與資源風險。這個判讀的具體含義是：workload 從舊平台搬到新平台時，表面上看 pod 跑起來了、health check 通過了，但依賴路徑（資料庫連線、cache endpoint、queue consumer 註冊）可能還指向舊環境。這類錯位在小流量時不明顯，放大流量後才暴露延遲升高或認證失敗。&lt;/p>
&lt;p>另一個判讀是容量假設需要重新驗證。舊平台的 resource request/limit、HPA 設定是在舊環境的 node type、網路拓樸下校準的。新平台的 node 規格、storage driver、CNI 可能不同，原本的容量假設可能過鬆或過緊。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>分批遷移 workload、保留觀測對照&lt;/strong>：先遷移影響面小、依賴單純的 workload（如內部工具、非關鍵 API）。新舊平台同時跑相同 workload 時，比較 error rate、latency、資源使用率。觀測對照是驗證的基礎——沒有對照就無法判斷新平台行為是否符合預期。&lt;/li>
&lt;li>&lt;strong>明確定義每批次切換與回退條件&lt;/strong>：每批遷移前寫下「什麼條件算成功」和「什麼條件觸發回退」。成功條件用 SLI 偏差衡量（error rate 不超過基線 + N%、p99 latency 不超過基線 + M ms）。回退條件要可操作——回退腳本事先驗證、DNS/LB 規則切回路徑事先測試。&lt;/li>
&lt;li>&lt;strong>新平台先驗證容量與恢復節奏&lt;/strong>：在新平台上跑容量測試，確認 HPA 觸發、node scale-up、pod scheduling 的時間符合預期。恢復節奏驗證包含模擬 node 失效後 pod 重新調度的時間、模擬 deployment rollback 的完成時間。&lt;/li>
&lt;li>&lt;strong>workload 類型分群遷移&lt;/strong>：API 服務、batch job、ML 推論的遷移順序與驗證條件不同。API 服務看延遲與錯誤率；batch job 看完成時間與資料正確性；ML 推論看推論延遲與 GPU 資源分配。混在一批遷移會讓驗證條件模糊。&lt;/li>
&lt;/ol>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>這類遷移的回退判讀重點是「回退到舊平台時，舊平台是否仍在可服務狀態」。遷移進行中若舊平台的資源已被縮減（node 數降低、monitoring 設定已移除），回退路徑就失效。穩定做法是在該批 workload 的新平台觀測窗口結束前，舊平台維持原規模不動。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment&lt;/a> 看分階段平台遷移的流量切換節奏。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 platform lifecycle contract&lt;/a> 看不同 workload 類型的 lifecycle 差異。回 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review&lt;/a> 看遷移前的可靠性評估。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/solutions/case-studies/mobileye-amazon-eks/">Mobileye migration to Amazon EKS&lt;/a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把 workload 遷移從基礎設施作業改成服務可用性作業。</p>
<h2 id="觀察">觀察</h2>
<p>Mobileye 將大規模工作負載遷移到 EKS。遷移動機集中在運維一致性與可用性治理——原有環境中不同團隊各自維護部署流程，升級節奏、監控覆蓋、容量規劃的標準不統一。遷移目標是用 managed 平台統一這些操作基線，讓各團隊可以專注在 workload 本身。</p>
<p>遷移範圍涵蓋多種 workload 類型：API 服務、資料處理 pipeline、ML 推論服務。這些 workload 的啟動時間、資源需求、drain 條件差異顯著，同一套遷移策略無法直接套用。</p>
<h2 id="判讀">判讀</h2>
<p>工作負載遷移若缺乏分段驗證，容易在切流時放大依賴與資源風險。這個判讀的具體含義是：workload 從舊平台搬到新平台時，表面上看 pod 跑起來了、health check 通過了，但依賴路徑（資料庫連線、cache endpoint、queue consumer 註冊）可能還指向舊環境。這類錯位在小流量時不明顯，放大流量後才暴露延遲升高或認證失敗。</p>
<p>另一個判讀是容量假設需要重新驗證。舊平台的 resource request/limit、HPA 設定是在舊環境的 node type、網路拓樸下校準的。新平台的 node 規格、storage driver、CNI 可能不同，原本的容量假設可能過鬆或過緊。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>分批遷移 workload、保留觀測對照</strong>：先遷移影響面小、依賴單純的 workload（如內部工具、非關鍵 API）。新舊平台同時跑相同 workload 時，比較 error rate、latency、資源使用率。觀測對照是驗證的基礎——沒有對照就無法判斷新平台行為是否符合預期。</li>
<li><strong>明確定義每批次切換與回退條件</strong>：每批遷移前寫下「什麼條件算成功」和「什麼條件觸發回退」。成功條件用 SLI 偏差衡量（error rate 不超過基線 + N%、p99 latency 不超過基線 + M ms）。回退條件要可操作——回退腳本事先驗證、DNS/LB 規則切回路徑事先測試。</li>
<li><strong>新平台先驗證容量與恢復節奏</strong>：在新平台上跑容量測試，確認 HPA 觸發、node scale-up、pod scheduling 的時間符合預期。恢復節奏驗證包含模擬 node 失效後 pod 重新調度的時間、模擬 deployment rollback 的完成時間。</li>
<li><strong>workload 類型分群遷移</strong>：API 服務、batch job、ML 推論的遷移順序與驗證條件不同。API 服務看延遲與錯誤率；batch job 看完成時間與資料正確性；ML 推論看推論延遲與 GPU 資源分配。混在一批遷移會讓驗證條件模糊。</li>
</ol>
<h2 id="回退判讀">回退判讀</h2>
<p>這類遷移的回退判讀重點是「回退到舊平台時，舊平台是否仍在可服務狀態」。遷移進行中若舊平台的資源已被縮減（node 數降低、monitoring 設定已移除），回退路徑就失效。穩定做法是在該批 workload 的新平台觀測窗口結束前，舊平台維持原規模不動。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment</a> 看分階段平台遷移的流量切換節奏。回 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 platform lifecycle contract</a> 看不同 workload 類型的 lifecycle 差異。回 <a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review</a> 看遷移前的可靠性評估。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/mobileye-amazon-eks/">Mobileye migration to Amazon EKS</a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）</li>
</ul>
]]></content:encoded></item><item><title>nginx</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/nginx/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/nginx/</guid><description>&lt;p>nginx 是 HTTP server / reverse proxy / load balancer 的事實標準之一、承擔三個責任：HTTP 7 層處理（reverse proxy / TLS termination / static content）、L4 / L7 load balancing、Kubernetes ingress controller（ingress-nginx）。設計取捨偏向「配置簡單 + 效能穩定 + reload 機制成熟」、跟 envoy 比是靜態 config-driven（無 dynamic xDS）。F5 收購後 nginx Plus 是商業版、社群 fork 有 Freenginx / angie。&lt;/p>
&lt;p>對「HTTP reverse proxy / LB、TLS termination、K8s ingress、API gateway 入門」這條路徑、nginx 是穩定首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 nginx config（server / location / upstream）&lt;/li>
&lt;li>配置 TLS / mTLS + SNI&lt;/li>
&lt;li>設計 rate limiting + connection limit&lt;/li>
&lt;li>部署 ingress-nginx 到 Kubernetes&lt;/li>
&lt;li>評估 nginx vs nginx Plus / OSS fork（Freenginx / angie）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-nginx-跑起來">最短路徑：5 分鐘把 nginx 跑起來&lt;/h2>





&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"># 1. 啟動 nginx（docker）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name nginx-demo -p 80:80 &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> -v &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/nginx.conf:/etc/nginx/nginx.conf:ro&amp;#34;&lt;/span> nginx:stable-alpine
&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 class="c1"># 2. 寫 reverse proxy config（nginx.conf 範例）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;CONF&amp;#39; &amp;gt; nginx.conf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s">events { worker_connections 1024; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s">http {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s"> upstream backend {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s"> server app:8080;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s"> server {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s"> listen 80;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s"> location / {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s"> proxy_pass http://backend;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s"> proxy_set_header Host $host;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s"> proxy_set_header X-Real-IP $remote_addr;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="s">CONF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. reload + 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">nginx -t &lt;span class="c1"># test config syntax&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">nginx -s reload &lt;span class="c1"># reload without restart（zero-downtime config update）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="nginx-config-設計">nginx config 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>階層：events / http / server / location / upstream&lt;/li>
&lt;li>變數：$host / $remote_addr / $http_&lt;name>&lt;/li>
&lt;li>Include 拆分大 config&lt;/li>
&lt;li>對應指令：&lt;code>nginx -T&lt;/code>（dump full config）、&lt;code>nginx -t&lt;/code>（test）、&lt;code>nginx -s reload&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="reverse-proxy-配置">Reverse proxy 配置&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>nginx 是 HTTP server / reverse proxy / load balancer 的事實標準之一、承擔三個責任：HTTP 7 層處理（reverse proxy / TLS termination / static content）、L4 / L7 load balancing、Kubernetes ingress controller（ingress-nginx）。設計取捨偏向「配置簡單 + 效能穩定 + reload 機制成熟」、跟 envoy 比是靜態 config-driven（無 dynamic xDS）。F5 收購後 nginx Plus 是商業版、社群 fork 有 Freenginx / angie。</p>
<p>對「HTTP reverse proxy / LB、TLS termination、K8s ingress、API gateway 入門」這條路徑、nginx 是穩定首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 nginx config（server / location / upstream）</li>
<li>配置 TLS / mTLS + SNI</li>
<li>設計 rate limiting + connection limit</li>
<li>部署 ingress-nginx 到 Kubernetes</li>
<li>評估 nginx vs nginx Plus / OSS fork（Freenginx / angie）</li>
</ol>
<h2 id="最短路徑5-分鐘把-nginx-跑起來">最短路徑：5 分鐘把 nginx 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 啟動 nginx（docker）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name nginx-demo -p 80:80 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/nginx.conf:/etc/nginx/nginx.conf:ro&#34;</span> nginx:stable-alpine
</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="c1"># 2. 寫 reverse proxy config（nginx.conf 範例）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">cat <span class="s">&lt;&lt;&#39;CONF&#39; &gt; nginx.conf
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">events { worker_connections 1024; }
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">http {
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  upstream backend {
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">    server app:8080;
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  server {
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">    listen 80;
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">    location / {
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">      proxy_pass http://backend;
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">      proxy_set_header Host $host;
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">      proxy_set_header X-Real-IP $remote_addr;
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">CONF</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># 3. reload + 驗證</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">nginx -t            <span class="c1"># test config syntax</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">nginx -s reload     <span class="c1"># reload without restart（zero-downtime config update）</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="nginx-config-設計">nginx config 設計</h3>
<p>子議題：</p>
<ul>
<li>階層：events / http / server / location / upstream</li>
<li>變數：$host / $remote_addr / $http_<name></li>
<li>Include 拆分大 config</li>
<li>對應指令：<code>nginx -T</code>（dump full config）、<code>nginx -t</code>（test）、<code>nginx -s reload</code></li>
</ul>
<h3 id="reverse-proxy-配置">Reverse proxy 配置</h3>
<p>子議題：</p>
<ul>
<li>proxy_pass / proxy_set_header / proxy_http_version</li>
<li>proxy_buffering / proxy_request_buffering</li>
<li>upstream load balancing（round_robin / least_conn / ip_hash）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB contract</a></li>
</ul>
<h3 id="tls-termination">TLS termination</h3>
<p>子議題：</p>
<ul>
<li>ssl_certificate / ssl_certificate_key / ssl_protocols</li>
<li>SNI（server_name + listen 443 ssl）</li>
<li>mTLS：ssl_client_certificate + ssl_verify_client</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a> TLS 章</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="rate-limiting--connection-limit">Rate limiting / connection limit</h3>
<p>子議題：</p>
<ul>
<li>limit_req_zone + limit_req（leaky bucket）</li>
<li>limit_conn_zone + limit_conn</li>
<li>跟 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">knowledge cards rate-limit</a> 對照</li>
<li>對應威脅建模: <a href="/blog/backend/02-cache-redis/attacker-view-cache-risks/" data-link-title="2.6 快取威脅建模（Threat Modeling）" data-link-desc="從快取污染、一致性偏移與流量放大風險，盤點 cache/redis 的主要弱點">2.6 快取威脅建模</a></li>
</ul>
<h3 id="ingress-nginx-for-kubernetes">ingress-nginx for Kubernetes</h3>
<p>子議題：</p>
<ul>
<li>Helm chart 部署</li>
<li>Ingress resource + Annotations 配置</li>
<li>ConfigMap + Snippets（power users）</li>
<li>跟 <a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a> / Gateway API 對比</li>
</ul>
<h3 id="openresty--lua-extension">OpenResty / Lua extension</h3>
<p>子議題：</p>
<ul>
<li>OpenResty：nginx + LuaJIT、可寫 Lua handler</li>
<li>ngx_lua: access / content / log phase handler</li>
<li>適合：自訂 auth / dynamic routing</li>
<li>對應 envoy WASM extension 對比</li>
</ul>
<h3 id="nginx-vs-nginx-plus--freenginx--angie">nginx vs nginx Plus / Freenginx / angie</h3>
<p>子議題：</p>
<ul>
<li>nginx OSS（F5 維護）：basic feature</li>
<li>nginx Plus（商業）：active health check / dynamic config API / DNS upstream</li>
<li>Freenginx：2024 社群 fork（不滿 F5 治理）</li>
<li>angie：另一個 fork、多 commercial extension</li>
<li>選擇判讀：dynamic config 重要 → 看 Envoy / Plus；OSS 純社群 → Freenginx / angie</li>
</ul>
<h3 id="performance-tuning">Performance tuning</h3>
<p>子議題：</p>
<ul>
<li>worker_processes / worker_connections</li>
<li>keepalive_timeout / keepalive_requests</li>
<li>sendfile / tcp_nopush / tcp_nodelay</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a> 對照</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="502-bad-gateway">502 Bad Gateway</h3>
<p>操作原則：upstream 不可達 / 回應錯。判讀：<code>error.log</code> + upstream health。</p>
<h3 id="504-gateway-timeout">504 Gateway Timeout</h3>
<p>操作原則：proxy_read_timeout / proxy_send_timeout 超過。判讀：upstream 處理時間 vs timeout 配置。</p>
<h3 id="connection-limit--502-under-load">Connection limit / 502 under load</h3>
<p>操作原則：worker_connections 不夠、ephemeral port 耗盡、upstream keepalive 不對。判讀：<code>netstat</code> + nginx stub_status。</p>
<h3 id="ssl-handshake-failure">SSL handshake failure</h3>
<p>操作原則：cipher / protocol mismatch、cert chain incomplete、SNI 不對。判讀：<code>openssl s_client -connect host:443 -servername host</code>。</p>
<h3 id="reload-不生效">Reload 不生效</h3>
<p>操作原則：<code>nginx -t</code> 先 test、新 worker 起來舊 worker drain。若行為怪、檢查是否拿到舊 listening socket。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dynamic config / xDS</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></td>
      </tr>
      <tr>
          <td>Cloud-native auto-discovery</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a>（ALB / NLB）</td>
      </tr>
      <tr>
          <td>L4 為主 / 高吞吐</td>
          <td>HAProxy / NLB</td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>Istio / Linkerd / Consul Connect</td>
      </tr>
      <tr>
          <td>API Gateway 進階</td>
          <td>Kong / Tyk / Apigee</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 nginx directive reference</li>
<li>ngx_lua / OpenResty 完整教學</li>
<li>各 distro nginx 版本差異</li>
<li>nginx internal architecture</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 nginx 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>切流時 nginx upstream / ingress-nginx 沒做 graceful drain、長連線跟 5xx 一起放大</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小型直接 nginx reverse proxy、中型走 ingress-nginx、大型才考慮 envoy 或 service mesh</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 nginx 案例</strong>：Cloudflare 為何 fork（freenginx）、大規模 ingress-nginx 客戶案例、OpenResty 在 production 的擴展案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a>、<a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a>、<a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a></li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a>（TLS / WAF）、<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance</a></li>
</ul>
]]></content:encoded></item><item><title>5.4 service discovery</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/</guid><description>&lt;p>服務發現（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery&lt;/a>）的核心責任是讓服務在變動環境中仍能找到正確目標實例。它處理的是定位與可用集合，不處理業務設定判斷；這個邊界清楚後，部署切換與故障回退才可預期。&lt;/p>
&lt;h2 id="dns-與-registry">DNS 與 registry&lt;/h2>
&lt;p>service discovery 常見兩種路徑：DNS 查詢與 service registry。DNS 提供簡化解析路徑，適合標準服務發現；registry 提供更細節的實例狀態與元資料，適合複雜路由與多租戶治理。&lt;/p>
&lt;p>選擇重點是變更頻率與一致性需求。實例變動頻繁或跨區路由複雜時，registry 能提供更細控制；穩定內網服務可優先 DNS 路徑降低操作成本。&lt;/p>
&lt;h3 id="dns-based-discovery-的運作與限制">DNS-based Discovery 的運作與限制&lt;/h3>
&lt;p>Kubernetes Service 的 ClusterIP 模式是最常見的 DNS-based discovery：kube-dns / CoreDNS 回覆一個虛擬 IP，kube-proxy 用 iptables / IPVS 做 L4 負載均衡到實際 pod IP。Headless Service（&lt;code>clusterIP: None&lt;/code>）則直接回傳所有 pod IP 的 A record，讓客戶端自行選擇目標。&lt;/p>
&lt;p>DNS-based discovery 的限制來自 DNS 本身的語意：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>TTL 與快取&lt;/strong>：DNS 回應帶 TTL，客戶端和中間 resolver 會快取。當 pod 被摘除但 DNS 快取尚未過期，客戶端仍會嘗試連到已不存在的 IP。Kubernetes CoreDNS 的 Service TTL 預設 30 秒，但客戶端語言 runtime 可能有自己的 DNS cache（JVM &lt;code>networkaddress.cache.ttl&lt;/code> 預設 30 秒、有些版本預設 -1 代表永不過期）。&lt;/li>
&lt;li>&lt;strong>無健康資訊&lt;/strong>：DNS A record 不帶健康狀態。回覆的 IP 可能對應已經 not-ready 但尚未被 endpoint controller 移除的 pod。這個時間窗口取決於 kubelet sync 頻率與 endpoint controller 的反應速度。&lt;/li>
&lt;li>&lt;strong>無權重 / 元資料&lt;/strong>：DNS 不原生支援流量權重、版本標記、區域偏好。需要這些能力時要靠 service mesh 或 client-side load balancing。&lt;/li>
&lt;/ol>
&lt;p>DNS 路徑的工程價值在於零侵入——任何能解析 DNS 的程式碼都自動取得 discovery 能力，不需要額外 SDK 或 sidecar。缺點是控制粒度只到 IP 層，無法表達更豐富的路由語意。&lt;/p>
&lt;h3 id="registry-based-discovery-的運作模式">Registry-based Discovery 的運作模式&lt;/h3>
&lt;p>Service registry（Consul、etcd、Eureka、Nacos）維護 key-value store，每個 service instance 主動註冊自己的地址、metadata 與健康狀態。Client 透過 registry API 或 local agent 取得可用 instance 清單。&lt;/p>
&lt;p>Registry 的工程價值在於提供 DNS 無法表達的元資料：instance 的版本、區域、權重、標籤都可以作為路由條件。代價是所有 service 都需要 registry 連線邏輯（SDK 或 sidecar），且 registry 本身成為基礎設施依賴——registry 不可用時，新 instance 無法註冊、現有 instance 無法被發現。&lt;/p>
&lt;p>Registry 跟 DNS 不互斥。常見做法是 registry 作為 source of truth，再用 DNS interface 對外提供查詢（Consul DNS Interface、CoreDNS 的 etcd plugin）。這讓簡單場景走 DNS、複雜路由走 registry API、兩者共用同一份 instance 清單。&lt;/p>
&lt;h3 id="選擇判讀框架">選擇判讀框架&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>需求&lt;/th>
 &lt;th>DNS-based&lt;/th>
 &lt;th>Registry-based&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>instance 變動頻率低、路由簡單&lt;/td>
 &lt;td>適合：低維護、零侵入&lt;/td>
 &lt;td>過度設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要權重路由或版本切流&lt;/td>
 &lt;td>不適合：DNS 不帶權重&lt;/td>
 &lt;td>適合：metadata + 路由規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要跨叢集 / 跨區域 discovery&lt;/td>
 &lt;td>需要外部 DNS 配合（困難）&lt;/td>
 &lt;td>適合：registry federation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務用多語言實作&lt;/td>
 &lt;td>適合：任何語言都能解 DNS&lt;/td>
 &lt;td>需要每個語言的 SDK 或 sidecar&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要即時健康反映&lt;/td>
 &lt;td>受 TTL 限制、有延遲窗口&lt;/td>
 &lt;td>適合：health check 即時更新&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="endpoint-discovery">endpoint discovery&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint&lt;/a> discovery 的責任是維持可連線目標集合。這包含註冊、健康檢查、摘除、重建後回註冊。服務端 readiness 與 discovery 健康判斷要對齊，否則會出現不可服務實例仍被路由的情況。&lt;/p></description><content:encoded><![CDATA[<p>服務發現（<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a>）的核心責任是讓服務在變動環境中仍能找到正確目標實例。它處理的是定位與可用集合，不處理業務設定判斷；這個邊界清楚後，部署切換與故障回退才可預期。</p>
<h2 id="dns-與-registry">DNS 與 registry</h2>
<p>service discovery 常見兩種路徑：DNS 查詢與 service registry。DNS 提供簡化解析路徑，適合標準服務發現；registry 提供更細節的實例狀態與元資料，適合複雜路由與多租戶治理。</p>
<p>選擇重點是變更頻率與一致性需求。實例變動頻繁或跨區路由複雜時，registry 能提供更細控制；穩定內網服務可優先 DNS 路徑降低操作成本。</p>
<h3 id="dns-based-discovery-的運作與限制">DNS-based Discovery 的運作與限制</h3>
<p>Kubernetes Service 的 ClusterIP 模式是最常見的 DNS-based discovery：kube-dns / CoreDNS 回覆一個虛擬 IP，kube-proxy 用 iptables / IPVS 做 L4 負載均衡到實際 pod IP。Headless Service（<code>clusterIP: None</code>）則直接回傳所有 pod IP 的 A record，讓客戶端自行選擇目標。</p>
<p>DNS-based discovery 的限制來自 DNS 本身的語意：</p>
<ol>
<li><strong>TTL 與快取</strong>：DNS 回應帶 TTL，客戶端和中間 resolver 會快取。當 pod 被摘除但 DNS 快取尚未過期，客戶端仍會嘗試連到已不存在的 IP。Kubernetes CoreDNS 的 Service TTL 預設 30 秒，但客戶端語言 runtime 可能有自己的 DNS cache（JVM <code>networkaddress.cache.ttl</code> 預設 30 秒、有些版本預設 -1 代表永不過期）。</li>
<li><strong>無健康資訊</strong>：DNS A record 不帶健康狀態。回覆的 IP 可能對應已經 not-ready 但尚未被 endpoint controller 移除的 pod。這個時間窗口取決於 kubelet sync 頻率與 endpoint controller 的反應速度。</li>
<li><strong>無權重 / 元資料</strong>：DNS 不原生支援流量權重、版本標記、區域偏好。需要這些能力時要靠 service mesh 或 client-side load balancing。</li>
</ol>
<p>DNS 路徑的工程價值在於零侵入——任何能解析 DNS 的程式碼都自動取得 discovery 能力，不需要額外 SDK 或 sidecar。缺點是控制粒度只到 IP 層，無法表達更豐富的路由語意。</p>
<h3 id="registry-based-discovery-的運作模式">Registry-based Discovery 的運作模式</h3>
<p>Service registry（Consul、etcd、Eureka、Nacos）維護 key-value store，每個 service instance 主動註冊自己的地址、metadata 與健康狀態。Client 透過 registry API 或 local agent 取得可用 instance 清單。</p>
<p>Registry 的工程價值在於提供 DNS 無法表達的元資料：instance 的版本、區域、權重、標籤都可以作為路由條件。代價是所有 service 都需要 registry 連線邏輯（SDK 或 sidecar），且 registry 本身成為基礎設施依賴——registry 不可用時，新 instance 無法註冊、現有 instance 無法被發現。</p>
<p>Registry 跟 DNS 不互斥。常見做法是 registry 作為 source of truth，再用 DNS interface 對外提供查詢（Consul DNS Interface、CoreDNS 的 etcd plugin）。這讓簡單場景走 DNS、複雜路由走 registry API、兩者共用同一份 instance 清單。</p>
<h3 id="選擇判讀框架">選擇判讀框架</h3>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>DNS-based</th>
          <th>Registry-based</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>instance 變動頻率低、路由簡單</td>
          <td>適合：低維護、零侵入</td>
          <td>過度設計</td>
      </tr>
      <tr>
          <td>需要權重路由或版本切流</td>
          <td>不適合：DNS 不帶權重</td>
          <td>適合：metadata + 路由規則</td>
      </tr>
      <tr>
          <td>需要跨叢集 / 跨區域 discovery</td>
          <td>需要外部 DNS 配合（困難）</td>
          <td>適合：registry federation</td>
      </tr>
      <tr>
          <td>服務用多語言實作</td>
          <td>適合：任何語言都能解 DNS</td>
          <td>需要每個語言的 SDK 或 sidecar</td>
      </tr>
      <tr>
          <td>需要即時健康反映</td>
          <td>受 TTL 限制、有延遲窗口</td>
          <td>適合：health check 即時更新</td>
      </tr>
  </tbody>
</table>
<h2 id="endpoint-discovery">endpoint discovery</h2>
<p><a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> discovery 的責任是維持可連線目標集合。這包含註冊、健康檢查、摘除、重建後回註冊。服務端 readiness 與 discovery 健康判斷要對齊，否則會出現不可服務實例仍被路由的情況。</p>
<p>endpoint 變更需要可追溯訊號，讓事故期間能快速判讀是路由失真、註冊延遲，還是下游本身不可用。</p>
<h3 id="註冊時序與-readiness-對齊">註冊時序與 Readiness 對齊</h3>
<p>endpoint 的註冊時機是 discovery 穩定性的關鍵變數。註冊太早（服務尚未 ready 就被加入可用集合）會導致客戶端打到未就緒實例；註冊太晚（服務已 ready 但尚未被 discovery 看到）會導致容量不足。</p>
<p>Kubernetes 的做法是把 endpoint 跟 readinessProbe 綁定：readiness pass 才把 pod IP 加入 Endpoints 物件。這個設計讓 readiness 定義直接決定 discovery 行為。但 readiness probe 的判斷到 Endpoints 更新之間仍有延遲（endpoint controller sync 週期 + kube-proxy rules 更新），這個延遲窗口內的行為要理解：</p>
<ul>
<li>Pod 剛從 not-ready 變 ready：endpoint controller 需要同步周期把 pod IP 加入 Endpoints → kube-proxy 更新 iptables / IPVS → 流量才會到。期間該 pod 不接流量但已可服務。</li>
<li>Pod 從 ready 變 not-ready：同樣有延遲。期間客戶端仍可能打到已 not-ready 的 pod。drain 設計要覆蓋這段窗口。</li>
</ul>
<h3 id="摘除節奏與-drain-的配合">摘除節奏與 Drain 的配合</h3>
<p>endpoint 摘除不是瞬時的。從 pod 標記 not-ready 到所有 client 停止向它送流量，中間經過多個同步步驟。這段時間內，被摘除的 pod 仍會收到流量。</p>
<p>穩定做法是在 preStop hook 加入短暫等待（通常 5-15 秒），讓 endpoint 更新有時間傳播到所有 kube-proxy / envoy，然後再開始 graceful shutdown。這段 preStop 等待是 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 中 drain 總窗口（短 API 通常 5-30 秒）的 endpoint 傳播子區間，drain 總窗口還要覆蓋 preStop 之後的在途請求收斂時間。</p>
<h3 id="跨叢集-discovery-的挑戰">跨叢集 Discovery 的挑戰</h3>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed K8s → EKS</a>：揭露「遷移難點通常在跨叢集服務依賴與流量切換、不在 Kubernetes API 本身」。跨叢集 discovery 是遷移期的核心難題——服務 A 在新叢集、服務 B 在舊叢集，A 要能找到 B。</p>
<p>跨叢集 discovery 的常見做法：</p>
<ol>
<li><strong>外部 DNS + 加權路由</strong>：兩個叢集的 service 都註冊到外部 DNS（Route 53、Cloud DNS），用權重控制流量比例。簡單但粒度粗，只能整體切、不能 per-service 切。</li>
<li><strong>Service mesh federation</strong>：Istio multi-cluster、Linkerd multi-cluster 把跨叢集 endpoint 統一管理。粒度細、可以 per-service 切流量，但引入 mesh 的複雜度。</li>
<li><strong>Application-level routing</strong>：應用自己管理多叢集 endpoint（通常透過 config 或 feature flag），切換時改 config。最靈活但最手動，適合遷移期的過渡方案。</li>
</ol>
<p>遷移期最危險的狀態是「服務切過去了、discovery 沒切過去」——新叢集的服務 A 仍透過舊 discovery 找舊叢集的 B，跨網路延遲從微秒級跳到毫秒級，或在網路分區時完全斷開。discovery 切換要跟服務切換同批規劃。</p>
<h2 id="failure-fallback">failure fallback</h2>
<p><a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 在 discovery 層的責任是縮小定位失敗影響。常見策略包含本地快取最後可用集合、區域優先回退、受控重試與短暫降級。</p>
<p>fallback 設計要明確停止條件。長期依賴過期 endpoint 快取會造成隱性錯誤累積，事故期反而更難收斂。</p>
<h3 id="fallback-的三層防線">Fallback 的三層防線</h3>
<p>discovery 故障的 fallback 可分三層，每層有不同的代價與風險：</p>
<p><strong>第一層：本地 endpoint 快取</strong>。Client 維持最後一次成功查詢的 endpoint 清單。discovery 服務不可用時，繼續用快取 endpoint。風險是快取中的 endpoint 可能已經下線或不健康。有效期要設上限——超過 N 分鐘的快取視為不可信，進入第二層。</p>
<p><strong>第二層：區域降級</strong>。本區域的 endpoint 全部不可用時，降級到其他區域的 endpoint。代價是跨區延遲增加。風險是其他區域也可能因為同源故障而不可用。降級時要觀測跨區延遲是否在 SLO 內，超出則進第三層。</p>
<p><strong>第三層：服務降級</strong>。discovery 完全失效時，服務本身降級——返回快取回應、靜態頁面、或明確的錯誤訊息。這一層的設計責任落在應用的 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 策略，discovery 只負責提供「目前無可用 endpoint」的訊號。</p>
<p>三層防線的共同原則是每一層都有明確的進入條件和退出條件。進入 fallback 不是終點——要持續嘗試恢復正常路徑，fallback 狀態持續時間要被觀測和告警。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務延遲上升且下游錯誤分布不均</td>
          <td>路由到不可用或高負載實例</td>
          <td>檢查註冊健康、刷新 endpoint 集合</td>
      </tr>
      <tr>
          <td>節點重啟後短時間大量 5xx</td>
          <td>註冊與 readiness 時序不對齊</td>
          <td>延後註冊時機、收斂就緒條件</td>
      </tr>
      <tr>
          <td>跨區呼叫比例異常升高</td>
          <td>區域內可用集合失真或容量不足</td>
          <td>檢查區域路由策略、恢復本地優先</td>
      </tr>
      <tr>
          <td>discovery 查詢成功但連線失敗率升高</td>
          <td>endpoint 新鮮度不足或 DNS 快取漂移</td>
          <td>縮短 TTL、加入主動刷新</td>
      </tr>
      <tr>
          <td>fallback 命中率長期偏高</td>
          <td>主路徑失效被掩蓋</td>
          <td>啟動故障調查、限制 fallback 存活時間</td>
      </tr>
      <tr>
          <td>擴容後新 pod 遲遲不接流量</td>
          <td>endpoint 註冊延遲或 kube-proxy 同步慢</td>
          <td>檢查 endpoint controller 延遲</td>
      </tr>
      <tr>
          <td>遷移期跨叢集延遲突增</td>
          <td>discovery 沒切過去、跨網路打舊叢集</td>
          <td>規劃 discovery 切換與服務切換同批</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>Service discovery 跟 DNS 設定的混淆，會讓註冊時序、健康判斷與摘除節奏的缺口在平時被忽略。這類缺口在平時不明顯，通常在切版、擴縮容或區域異常時集中爆發。</p>
<p>把 fallback 命中率視為穩定指標也容易誤判。fallback 長期偏高代表主路徑問題被遮蔽，應回頭檢查 endpoint 新鮮度與註冊健康，而不是只放寬重試。</p>
<p>把 DNS TTL 設成 0 試圖取得即時一致性，會大幅增加 DNS 查詢量。DNS 的設計前提是快取——TTL 0 在高流量服務下會讓 DNS server 成為瓶頸。穩定做法是設合理 TTL（5-30 秒）搭配 client-side 主動刷新。</p>
<p>把 JVM 的 DNS cache 當成 OS 的 DNS TTL——JVM <code>networkaddress.cache.ttl</code> 的預設值在不同版本不同（有些版本是 30 秒、有些是永不過期）。容器化部署時要顯式設定，避免 pod IP 變了但 JVM 還在打舊 IP。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>service discovery 專注「找到可用實例」。當問題進入設定分發、版本切換、策略開關，責任轉到 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 與部署策略章節。邊界分明能避免故障排查時把不同控制面混為一談。</p>
<p>discovery 跟 load balancing 的邊界：discovery 回答「有哪些 endpoint 可用」，load balancing 回答「在可用 endpoint 中選哪一個」。DNS round-robin 把兩者混在一起，registry-based 方案通常把兩者分開，讓 LB 策略（round-robin、least-connection、consistent hash）在 discovery 結果之上獨立運作。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>發現與定位鏈路可用 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera：managed K8s migration</a> 回寫。先看遷移期間實例註冊、摘除與 DNS/registry 同步節奏，再對照本章判讀 endpoint 新鮮度與 fallback 壽命是否合理。</p>
<p><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed K8s → EKS</a> 從跨叢集角度支撐：揭露遷移期的 discovery 挑戰——「難點在跨叢集服務依賴與流量切換」。遷移期 discovery 要處理新舊叢集的 endpoint 共存、切換時序、回退路徑。</p>
<p>這些案例主要支撐「定位集合新鮮度」與「跨叢集 discovery 同步」判讀。不直接支撐 LB 連線 timeout 或 runtime 建置一致性；若問題在連線生命週期或映像漂移，應轉到 5.3 或 5.1。</p>
<p>遇到「查詢成功但連線失敗率高」時，應拆成註冊時序、TTL 與快取刷新三條線同步驗證，避免把定位問題誤判成下游異常，再把證據分流到 <a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18 Incident Intake &amp; Evidence Triage</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 5.2 的交接：實例註冊與可用判定回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes 部署策略</a>。</li>
<li>與 5.3 的交接：路由目標與流量合約回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.6 的交接：endpoint 註冊時序與 readiness 的對齊回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 5.7 的交接：discovery 與 control plane boundary 的分責回到 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">Traffic、Config 與 Control Plane Boundary</a>。</li>
<li>與 4.13 的交接：依賴拓樸與發現信號回到 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">Service Topology 與 Dependency Map</a>。</li>
<li>與 8.18 的交接：定位故障的證據分流回到 <a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">Incident Intake &amp; Evidence Triage</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把發現機制放進流量契約，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看部署切換如何影響可用集合，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a>。要看 discovery 在 control plane 邊界中的定位，接著讀 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Traffic、Config 與 Control Plane Boundary</a>。</p>
]]></content:encoded></item><item><title>8.4 Microsoft：雲端基礎設施的一部分</title><link>https://tarrragon.github.io/blog/go/08-case-studies/microsoft/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/08-case-studies/microsoft/</guid><description>&lt;p>Microsoft 的官方案例文字不長，但方向很清楚：Go 被用來支撐雲端基礎設施的一部分。這類案例的重點通常在平台層、支援工具與雲端服務周邊。&lt;/p>
&lt;h2 id="你應該看什麼">你應該看什麼&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://go.dev/solutions/microsoft">How Microsoft Embraces Go&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="這個案例告訴我們什麼">這個案例告訴我們什麼&lt;/h2>
&lt;ol>
&lt;li>Go 很適合平台與基礎設施工具。&lt;/li>
&lt;li>雲端工程很重視部署單純性與長期可維護性。&lt;/li>
&lt;li>Go 常被放在內部治理、雲端元件與自動化流程中。&lt;/li>
&lt;/ol>
&lt;h2 id="可對照的公開原始碼">可對照的公開原始碼&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.com/Microsoft/cobalt">Microsoft/cobalt&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/azure/osdu-infrastructure">azure/osdu-infrastructure&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>這些公開 repo 可以用來理解 Microsoft 生態裡的雲端基礎設施與自動化工作方式。即使它們不一定只講一件產品，仍很適合對照 Go 的平台語言角色。&lt;/p></description><content:encoded><![CDATA[<p>Microsoft 的官方案例文字不長，但方向很清楚：Go 被用來支撐雲端基礎設施的一部分。這類案例的重點通常在平台層、支援工具與雲端服務周邊。</p>
<h2 id="你應該看什麼">你應該看什麼</h2>
<ul>
<li><a href="https://go.dev/solutions/microsoft">How Microsoft Embraces Go</a></li>
</ul>
<h2 id="這個案例告訴我們什麼">這個案例告訴我們什麼</h2>
<ol>
<li>Go 很適合平台與基礎設施工具。</li>
<li>雲端工程很重視部署單純性與長期可維護性。</li>
<li>Go 常被放在內部治理、雲端元件與自動化流程中。</li>
</ol>
<h2 id="可對照的公開原始碼">可對照的公開原始碼</h2>
<ul>
<li><a href="https://github.com/Microsoft/cobalt">Microsoft/cobalt</a></li>
<li><a href="https://github.com/azure/osdu-infrastructure">azure/osdu-infrastructure</a></li>
</ul>
<p>這些公開 repo 可以用來理解 Microsoft 生態裡的雲端基礎設施與自動化工作方式。即使它們不一定只講一件產品，仍很適合對照 Go 的平台語言角色。</p>
]]></content:encoded></item><item><title>6.4 版本偵測與 feature gate</title><link>https://tarrragon.github.io/blog/go-advanced/06-production-operations/feature-gate/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/06-production-operations/feature-gate/</guid><description>&lt;p>Feature gate 的核心目標是在外部能力、部署環境或版本不同時，讓服務保留可預期行為。它明確管理功能何時啟用、關閉時如何降級、錯誤時如何回報。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>用 config struct 集中載入 feature gate&lt;/li>
&lt;li>把外部版本偵測轉成 capability&lt;/li>
&lt;li>為 gate 關閉時定義降級、回錯或延後處理策略&lt;/li>
&lt;li>避免在程式各處直接讀環境變數&lt;/li>
&lt;li>同時測試 feature 開與關兩條路徑&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察新功能上線需要可控行為">【觀察】新功能上線需要可控行為&lt;/h2>
&lt;p>Feature gate 的核心需求來自生產環境差異。新功能可能只在部分部署環境可用，外部依賴可能版本不同，某些診斷入口只應在內網啟用，某些即時能力需要先灰度。&lt;/p>
&lt;p>沒有 gate 時常見問題：&lt;/p>
&lt;ul>
&lt;li>新功能只能一次性全開或全關。&lt;/li>
&lt;li>部署環境不支援時服務直接失敗。&lt;/li>
&lt;li>測試只能覆蓋預設路徑。&lt;/li>
&lt;li>問題發生時無法快速降級。&lt;/li>
&lt;li>程式各處用環境變數判斷，行為難以推理。&lt;/li>
&lt;/ul>
&lt;p>Feature gate 的目的是讓行為決策集中、可測、可回滾。&lt;/p>
&lt;h2 id="判讀feature-gate-是行為合約">【判讀】feature gate 是行為合約&lt;/h2>
&lt;p>Feature gate 的核心語意是控制某段行為是否啟用，以及未啟用時系統要做什麼。它不只是 &lt;code>if&lt;/code>，而是一個操作合約。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Features&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">RealtimePush&lt;/span> &lt;span class="kt">bool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">Diagnostics&lt;/span> &lt;span class="kt">bool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">Pprof&lt;/span> &lt;span class="kt">bool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>開關名稱應描述功能，而不是描述臨時任務。&lt;code>RealtimePush&lt;/code> 比 &lt;code>NewCode&lt;/code> 更能長期維護；&lt;code>Diagnostics&lt;/code> 比 &lt;code>DebugStuff&lt;/code> 更清楚。&lt;/p>
&lt;p>Gate 應在應用啟動時集中載入，再傳給需要的元件。不要在程式各處反覆直接讀環境變數，否則測試與推理都會變困難。&lt;/p>
&lt;h2 id="執行集中載入-feature-config">【執行】集中載入 feature config&lt;/h2>
&lt;p>Feature config 的核心責任是把環境變數、設定檔或啟動參數轉成明確資料。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">LoadFeaturesFromEnv&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="nx">Features&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">Features&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">RealtimePush&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Getenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;FEATURE_REALTIME_PUSH&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">Diagnostics&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Getenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;APP_DIAGNOSTICS&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">Pprof&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Getenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;APP_PPROF&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>組裝時傳入元件：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">features&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">LoadFeaturesFromEnv&lt;/span>&lt;span class="p">()&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="nx">mux&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewServeMux&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nf">RegisterDiagnostics&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mux&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">features&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Diagnostics&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">publisher&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewPublisher&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">PublisherConfig&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">RealtimeEnabled&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">features&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">RealtimePush&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">})&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="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">publisher&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這樣功能測試可以直接建構 &lt;code>Features&lt;/code>，不必依賴全域環境變數。環境變數解析只需要在 &lt;code>LoadFeaturesFromEnv&lt;/code> 的測試中覆蓋。&lt;/p>
&lt;h2 id="判讀版本偵測要轉成能力">【判讀】版本偵測要轉成能力&lt;/h2>
&lt;p>版本偵測的核心原則是不要讓整個程式到處比較版本字串。應把外部版本轉成 capability，內部只判斷能力。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Capabilities&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">SupportsStreaming&lt;/span> &lt;span class="kt">bool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">SupportsMetadata&lt;/span> &lt;span class="kt">bool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">DetectCapabilities&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">version&lt;/span> &lt;span class="nx">semver&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Version&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">Capabilities&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">Capabilities&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">SupportsStreaming&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">version&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">GTE&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">semver&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">MustParse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;2.0.0&amp;#34;&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">SupportsMetadata&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">version&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">GTE&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">semver&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">MustParse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;2.1.0&amp;#34;&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>內部程式應寫成：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">caps&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SupportsStreaming&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nf">useStreaming&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&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;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">return&lt;/span> &lt;span class="nf">usePolling&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這比到處寫 &lt;code>if version &amp;gt;= ...&lt;/code> 更清楚，也更容易測試。版本字串是外部事實，capability 是內部行為判斷。&lt;/p>
&lt;h2 id="策略gate-關閉時要有降級策略">【策略】gate 關閉時要有降級策略&lt;/h2>
&lt;p>Feature gate 的核心問題是關閉時要做什麼。常見策略包括降級、回錯、隱藏入口、排程稍後處理。&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;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a>&lt;/td>
 &lt;td>使用舊流程&lt;/td>
 &lt;td>新能力只是效率改善&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>reject&lt;/td>
 &lt;td>回明確錯誤&lt;/td>
 &lt;td>功能沒有安全替代方案&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>hide&lt;/td>
 &lt;td>不註冊 endpoint 或不顯示入口&lt;/td>
 &lt;td>使用者不應看到該功能&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>store for later&lt;/td>
 &lt;td>先保存，稍後處理&lt;/td>
 &lt;td>即時能力暫不可用但資料不能丟&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>例如即時推送關閉時，可以改成保存待處理資料：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">p&lt;/span> &lt;span class="nx">Publisher&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Publish&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span> &lt;span class="nx">DomainEvent&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">realtimeEnabled&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">realtime&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Publish&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">repository&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SaveForLater&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>降級策略要符合資料語意。不能即時送出不代表可以直接丟掉重要事件。&lt;/p>
&lt;h2 id="執行http-endpoint-可用-gate-控制註冊或行為">【執行】HTTP endpoint 可用 gate 控制註冊或行為&lt;/h2>
&lt;p>HTTP feature gate 的核心選擇是「不註冊 endpoint」或「註冊但回明確錯誤」。兩者語意不同。&lt;/p>
&lt;p>不註冊 endpoint：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">features&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Diagnostics&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nf">RegisterDiagnostics&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mux&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>適合診斷入口、內部工具或不希望使用者看見的功能。&lt;/p>
&lt;p>註冊但回錯：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">HandleRealtimeExport&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">features&lt;/span> &lt;span class="nx">Features&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">HandlerFunc&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">features&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">RealtimePush&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;realtime export is disabled&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusNotImplemented&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nf">startRealtimeExport&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>適合公開 API，讓呼叫端知道功能存在但目前不可用。&lt;/p>
&lt;h2 id="策略gate-不應散落成巢狀-if">【策略】gate 不應散落成巢狀 if&lt;/h2>
&lt;p>Feature gate 的核心維護風險是判斷散落在多層呼叫中，最後沒人知道功能到底何時啟用。&lt;/p></description><content:encoded><![CDATA[<p>Feature gate 的核心目標是在外部能力、部署環境或版本不同時，讓服務保留可預期行為。它明確管理功能何時啟用、關閉時如何降級、錯誤時如何回報。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>用 config struct 集中載入 feature gate</li>
<li>把外部版本偵測轉成 capability</li>
<li>為 gate 關閉時定義降級、回錯或延後處理策略</li>
<li>避免在程式各處直接讀環境變數</li>
<li>同時測試 feature 開與關兩條路徑</li>
</ol>
<hr>
<h2 id="觀察新功能上線需要可控行為">【觀察】新功能上線需要可控行為</h2>
<p>Feature gate 的核心需求來自生產環境差異。新功能可能只在部分部署環境可用，外部依賴可能版本不同，某些診斷入口只應在內網啟用，某些即時能力需要先灰度。</p>
<p>沒有 gate 時常見問題：</p>
<ul>
<li>新功能只能一次性全開或全關。</li>
<li>部署環境不支援時服務直接失敗。</li>
<li>測試只能覆蓋預設路徑。</li>
<li>問題發生時無法快速降級。</li>
<li>程式各處用環境變數判斷，行為難以推理。</li>
</ul>
<p>Feature gate 的目的是讓行為決策集中、可測、可回滾。</p>
<h2 id="判讀feature-gate-是行為合約">【判讀】feature gate 是行為合約</h2>
<p>Feature gate 的核心語意是控制某段行為是否啟用，以及未啟用時系統要做什麼。它不只是 <code>if</code>，而是一個操作合約。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">Features</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">RealtimePush</span> <span class="kt">bool</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Diagnostics</span>  <span class="kt">bool</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">Pprof</span>        <span class="kt">bool</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>開關名稱應描述功能，而不是描述臨時任務。<code>RealtimePush</code> 比 <code>NewCode</code> 更能長期維護；<code>Diagnostics</code> 比 <code>DebugStuff</code> 更清楚。</p>
<p>Gate 應在應用啟動時集中載入，再傳給需要的元件。不要在程式各處反覆直接讀環境變數，否則測試與推理都會變困難。</p>
<h2 id="執行集中載入-feature-config">【執行】集中載入 feature config</h2>
<p>Feature config 的核心責任是把環境變數、設定檔或啟動參數轉成明確資料。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">LoadFeaturesFromEnv</span><span class="p">()</span> <span class="nx">Features</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nx">Features</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="nx">RealtimePush</span><span class="p">:</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;FEATURE_REALTIME_PUSH&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s">&#34;1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">Diagnostics</span><span class="p">:</span>  <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;APP_DIAGNOSTICS&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s">&#34;1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nx">Pprof</span><span class="p">:</span>        <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;APP_PPROF&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s">&#34;1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>組裝時傳入元件：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">features</span> <span class="o">:=</span> <span class="nf">LoadFeaturesFromEnv</span><span class="p">()</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="nx">mux</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nf">NewServeMux</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nf">RegisterDiagnostics</span><span class="p">(</span><span class="nx">mux</span><span class="p">,</span> <span class="nx">features</span><span class="p">.</span><span class="nx">Diagnostics</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 class="nx">publisher</span> <span class="o">:=</span> <span class="nf">NewPublisher</span><span class="p">(</span><span class="nx">PublisherConfig</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">RealtimeEnabled</span><span class="p">:</span> <span class="nx">features</span><span class="p">.</span><span class="nx">RealtimePush</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">})</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="nx">_</span> <span class="p">=</span> <span class="nx">publisher</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這樣功能測試可以直接建構 <code>Features</code>，不必依賴全域環境變數。環境變數解析只需要在 <code>LoadFeaturesFromEnv</code> 的測試中覆蓋。</p>
<h2 id="判讀版本偵測要轉成能力">【判讀】版本偵測要轉成能力</h2>
<p>版本偵測的核心原則是不要讓整個程式到處比較版本字串。應把外部版本轉成 capability，內部只判斷能力。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">Capabilities</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">SupportsStreaming</span> <span class="kt">bool</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">SupportsMetadata</span>  <span class="kt">bool</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">func</span> <span class="nf">DetectCapabilities</span><span class="p">(</span><span class="nx">version</span> <span class="nx">semver</span><span class="p">.</span><span class="nx">Version</span><span class="p">)</span> <span class="nx">Capabilities</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="nx">Capabilities</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">SupportsStreaming</span><span class="p">:</span> <span class="nx">version</span><span class="p">.</span><span class="nf">GTE</span><span class="p">(</span><span class="nx">semver</span><span class="p">.</span><span class="nf">MustParse</span><span class="p">(</span><span class="s">&#34;2.0.0&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">SupportsMetadata</span><span class="p">:</span>  <span class="nx">version</span><span class="p">.</span><span class="nf">GTE</span><span class="p">(</span><span class="nx">semver</span><span class="p">.</span><span class="nf">MustParse</span><span class="p">(</span><span class="s">&#34;2.1.0&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>內部程式應寫成：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="nx">caps</span><span class="p">.</span><span class="nx">SupportsStreaming</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nf">useStreaming</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">return</span> <span class="nf">usePolling</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span></span></span></code></pre></div><p>這比到處寫 <code>if version &gt;= ...</code> 更清楚，也更容易測試。版本字串是外部事實，capability 是內部行為判斷。</p>
<h2 id="策略gate-關閉時要有降級策略">【策略】gate 關閉時要有降級策略</h2>
<p>Feature gate 的核心問題是關閉時要做什麼。常見策略包括降級、回錯、隱藏入口、排程稍後處理。</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>行為</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a></td>
          <td>使用舊流程</td>
          <td>新能力只是效率改善</td>
      </tr>
      <tr>
          <td>reject</td>
          <td>回明確錯誤</td>
          <td>功能沒有安全替代方案</td>
      </tr>
      <tr>
          <td>hide</td>
          <td>不註冊 endpoint 或不顯示入口</td>
          <td>使用者不應看到該功能</td>
      </tr>
      <tr>
          <td>store for later</td>
          <td>先保存，稍後處理</td>
          <td>即時能力暫不可用但資料不能丟</td>
      </tr>
  </tbody>
</table>
<p>例如即時推送關閉時，可以改成保存待處理資料：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="nx">Publisher</span><span class="p">)</span> <span class="nf">Publish</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">DomainEvent</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="nx">p</span><span class="p">.</span><span class="nx">realtimeEnabled</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">return</span> <span class="nx">p</span><span class="p">.</span><span class="nx">realtime</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="nx">p</span><span class="p">.</span><span class="nx">repository</span><span class="p">.</span><span class="nf">SaveForLater</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>降級策略要符合資料語意。不能即時送出不代表可以直接丟掉重要事件。</p>
<h2 id="執行http-endpoint-可用-gate-控制註冊或行為">【執行】HTTP endpoint 可用 gate 控制註冊或行為</h2>
<p>HTTP feature gate 的核心選擇是「不註冊 endpoint」或「註冊但回明確錯誤」。兩者語意不同。</p>
<p>不註冊 endpoint：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="nx">features</span><span class="p">.</span><span class="nx">Diagnostics</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nf">RegisterDiagnostics</span><span class="p">(</span><span class="nx">mux</span><span class="p">,</span> <span class="kc">true</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>適合診斷入口、內部工具或不希望使用者看見的功能。</p>
<p>註冊但回錯：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">HandleRealtimeExport</span><span class="p">(</span><span class="nx">features</span> <span class="nx">Features</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">HandlerFunc</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">features</span><span class="p">.</span><span class="nx">RealtimePush</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;realtime export is disabled&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusNotImplemented</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <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 class="nf">startRealtimeExport</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>適合公開 API，讓呼叫端知道功能存在但目前不可用。</p>
<h2 id="策略gate-不應散落成巢狀-if">【策略】gate 不應散落成巢狀 if</h2>
<p>Feature gate 的核心維護風險是判斷散落在多層呼叫中，最後沒人知道功能到底何時啟用。</p>
<p>反模式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;FEATURE_REALTIME_PUSH&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s">&#34;1&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="nx">version</span> <span class="o">&gt;=</span> <span class="s">&#34;2.0.0&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">if</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Enabled</span> <span class="p">{</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="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>較清楚的做法是先組出 decision：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">RealtimeDecision</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">Enabled</span> <span class="kt">bool</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">Reason</span>  <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">func</span> <span class="nf">DecideRealtime</span><span class="p">(</span><span class="nx">features</span> <span class="nx">Features</span><span class="p">,</span> <span class="nx">caps</span> <span class="nx">Capabilities</span><span class="p">)</span> <span class="nx">RealtimeDecision</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">features</span><span class="p">.</span><span class="nx">RealtimePush</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">return</span> <span class="nx">RealtimeDecision</span><span class="p">{</span><span class="nx">Enabled</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">Reason</span><span class="p">:</span> <span class="s">&#34;feature_disabled&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">caps</span><span class="p">.</span><span class="nx">SupportsStreaming</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="nx">RealtimeDecision</span><span class="p">{</span><span class="nx">Enabled</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">Reason</span><span class="p">:</span> <span class="s">&#34;streaming_not_supported&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">return</span> <span class="nx">RealtimeDecision</span><span class="p">{</span><span class="nx">Enabled</span><span class="p">:</span> <span class="kc">true</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Decision 物件讓 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、測試與錯誤回應都能使用相同 reason。</p>
<h2 id="執行log-要記錄-gate-decision">【執行】log 要記錄 gate decision</h2>
<p>Feature gate 的核心操作需求是知道功能為何啟用或關閉。當 gate 影響行為時，應記錄穩定 reason。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">decision</span> <span class="o">:=</span> <span class="nf">DecideRealtime</span><span class="p">(</span><span class="nx">features</span><span class="p">,</span> <span class="nx">caps</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;realtime decision&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;feature&#34;</span><span class="p">,</span> <span class="s">&#34;realtime_push&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s">&#34;enabled&#34;</span><span class="p">,</span> <span class="nx">decision</span><span class="p">.</span><span class="nx">Enabled</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s">&#34;reason&#34;</span><span class="p">,</span> <span class="nx">decision</span><span class="p">.</span><span class="nx">Reason</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>這能回答「功能為什麼沒有走即時推送」這類問題。Reason 應是小集合，不要塞完整錯誤字串。</p>
<h2 id="測試開與關兩條路徑都要測">【測試】開與關兩條路徑都要測</h2>
<p>Feature gate 測試的核心規則是同時測啟用與停用路徑。只測預設值很容易讓另一條路徑壞掉。</p>
<p>停用路徑：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestHandleRealtimeExportFeatureDisabled</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">req</span> <span class="o">:=</span> <span class="nx">httptest</span><span class="p">.</span><span class="nf">NewRequest</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">MethodPost</span><span class="p">,</span> <span class="s">&#34;/export&#34;</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">rec</span> <span class="o">:=</span> <span class="nx">httptest</span><span class="p">.</span><span class="nf">NewRecorder</span><span class="p">()</span>
</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="nx">handler</span> <span class="o">:=</span> <span class="nf">HandleRealtimeExport</span><span class="p">(</span><span class="nx">Features</span><span class="p">{</span><span class="nx">RealtimePush</span><span class="p">:</span> <span class="kc">false</span><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">handler</span><span class="p">.</span><span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">rec</span><span class="p">,</span> <span class="nx">req</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 class="k">if</span> <span class="nx">rec</span><span class="p">.</span><span class="nx">Code</span> <span class="o">!=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusNotImplemented</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;status = %d, want %d&#34;</span><span class="p">,</span> <span class="nx">rec</span><span class="p">.</span><span class="nx">Code</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusNotImplemented</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>啟用路徑：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestDecideRealtimeEnabled</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">decision</span> <span class="o">:=</span> <span class="nf">DecideRealtime</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">Features</span><span class="p">{</span><span class="nx">RealtimePush</span><span class="p">:</span> <span class="kc">true</span><span class="p">},</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">Capabilities</span><span class="p">{</span><span class="nx">SupportsStreaming</span><span class="p">:</span> <span class="kc">true</span><span class="p">},</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <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 class="k">if</span> <span class="p">!</span><span class="nx">decision</span><span class="p">.</span><span class="nx">Enabled</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;realtime should be enabled, reason %q&#34;</span><span class="p">,</span> <span class="nx">decision</span><span class="p">.</span><span class="nx">Reason</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>環境變數解析應單獨測 <code>LoadFeaturesFromEnv</code>。功能測試應直接傳入 <code>Features</code>，不要依賴全域環境狀態。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理服務內部的 gate 行為邊界；遠端 <a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag</a> 平台與灰度流程，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 composition root、handler boundary 與 runtime gate；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/07-refactoring/composition-root/" data-link-title="7.7 composition root 與依賴組裝" data-link-desc="把具體 adapter、config 與 usecase wiring 留在應用入口層">Go：composition root 與依賴組裝</a></li>
<li><a href="/blog/go/07-refactoring/handler-boundary/" data-link-title="7.1 把 handler 邏輯拆成可測單元" data-link-desc="分離 HTTP 協定處理與核心邏輯">Go：把 handler 邏輯拆成可測單元</a></li>
<li><a href="/blog/go/07-refactoring/interface-boundary/" data-link-title="7.2 用 interface 隔離外部依賴" data-link-desc="建立小而穩定的測試替身">Go：用 interface 隔離外部依賴</a></li>
<li><a href="/blog/go/05-error-testing/testing-basics/" data-link-title="5.2 testing 基礎" data-link-desc="用 testing package 驗證函式行為">Go：testing 基礎</a></li>
<li><a href="/blog/go-advanced/07-distributed-operations/deployment-contracts/" data-link-title="7.5 Kubernetes、systemd 與 load balancer 合約" data-link-desc="理解部署平台如何影響 Go 服務的 shutdown、health 與資源限制">Go 進階：Kubernetes、systemd 與 load balancer 合約</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>Feature gate 是生產操作工具，也是程式設計邊界。好的 gate 會集中載入、轉成 capability、定義降級策略、輸出穩定 reason，並同時測試開與關兩條路徑。它控制的是行為合約，不只是把新程式碼藏在 <code>if</code> 後面。</p>
]]></content:encoded></item><item><title>5.C5 Miro：Managed EKS 遷移</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/</guid><description>&lt;p>這個案例的核心責任是說明平台遷移也會改變團隊職責分工。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Miro 從自維運 Kubernetes 遷移到 managed EKS。遷移前的狀態是平台團隊大部分精力花在叢集本身的運維——control plane 升級、node AMI 維護、etcd 備份、安全修補。這些工作是必要的，但它們跟「讓開發者更快交付功能」沒有直接關聯。&lt;/p>
&lt;p>遷移後 managed EKS 接管了 control plane 運維。平台團隊的工作重心從「維持叢集跑起來」轉向「定義 release flow、observability convention、developer experience」。這個轉變是 managed 平台的組織層面價值，技術層面的價值（省維運、自動升級）反而是次要的。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略。這個判讀成立的前提是組織主動重新定義職責邊界——managed 平台不會自動帶來組織轉型，它只是移除了一類維運負擔。如果平台團隊在遷移後沒有重新定義職責，很容易繼續用舊模式工作（只是工作量少了），錯失把省下的精力轉到更高價值工作的機會。&lt;/p>
&lt;p>另一個判讀是 managed 平台引入新的 grey zone。control plane 由供應商管理，但 cluster-internal 元件（CNI、ingress controller、service mesh、cluster DNS）的 ownership 需要顯式界定。Miro 的經驗顯示這些 grey zone 若不在 day-1 處理，後續會在事故時暴露——「以為供應商在管」跟「供應商認為客戶在管」的認知差距，會讓故障排查繞圈。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>先定義遷移後的平台責任邊界&lt;/strong>：列出四層責任矩陣——cluster 層（供應商管）、cluster-internal 層（platform team 管）、application 層（service team 管）、跨層議題（協作）。每層有明確 owner，避免 grey zone。責任矩陣的詳細結構見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界&lt;/a>。&lt;/li>
&lt;li>&lt;strong>以自動化流程取代手動平台操作&lt;/strong>：遷移前的手動操作（node 升級、cert rotation、backup restore）在 managed 平台上由供應商或 IaC 接管。剩餘的手動操作（namespace provisioning、resource quota 設定、network policy review）也要自動化或流程化，避免依賴個人經驗。&lt;/li>
&lt;li>&lt;strong>將 incident 與 release policy 接回平台治理&lt;/strong>：managed 平台的 incident 跟 self-managed 不同——control plane 故障由供應商處理，但供應商的 incident 訊號要進入自家的 incident timeline。release policy（升級節奏、canary 比例、rollback 條件）在 managed 平台上仍是 platform team 的責任。&lt;/li>
&lt;/ol>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>從 managed 回退到 self-managed 的成本極高（要重建 control plane 運維能力），因此這類遷移的回退策略通常是「在 managed 平台內回退」而非「回到 self-managed」。具體做法是保留舊叢集一段時間作為 fallback，但同時接受「回到 self-managed 不是選項」的設計假設。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime&lt;/a> 看遷移後 runtime 層的變化驗證。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 managed 平台與職責邊界&lt;/a> 看職責矩陣的完整結構。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5 平台與入口威脅建模&lt;/a> 看遷移期攻擊面變動。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/solutions/case-studies/miro-amazon-eks/">Miro on AWS containers and EKS&lt;/a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明平台遷移也會改變團隊職責分工。</p>
<h2 id="觀察">觀察</h2>
<p>Miro 從自維運 Kubernetes 遷移到 managed EKS。遷移前的狀態是平台團隊大部分精力花在叢集本身的運維——control plane 升級、node AMI 維護、etcd 備份、安全修補。這些工作是必要的，但它們跟「讓開發者更快交付功能」沒有直接關聯。</p>
<p>遷移後 managed EKS 接管了 control plane 運維。平台團隊的工作重心從「維持叢集跑起來」轉向「定義 release flow、observability convention、developer experience」。這個轉變是 managed 平台的組織層面價值，技術層面的價值（省維運、自動升級）反而是次要的。</p>
<h2 id="判讀">判讀</h2>
<p>平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略。這個判讀成立的前提是組織主動重新定義職責邊界——managed 平台不會自動帶來組織轉型，它只是移除了一類維運負擔。如果平台團隊在遷移後沒有重新定義職責，很容易繼續用舊模式工作（只是工作量少了），錯失把省下的精力轉到更高價值工作的機會。</p>
<p>另一個判讀是 managed 平台引入新的 grey zone。control plane 由供應商管理，但 cluster-internal 元件（CNI、ingress controller、service mesh、cluster DNS）的 ownership 需要顯式界定。Miro 的經驗顯示這些 grey zone 若不在 day-1 處理，後續會在事故時暴露——「以為供應商在管」跟「供應商認為客戶在管」的認知差距，會讓故障排查繞圈。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>先定義遷移後的平台責任邊界</strong>：列出四層責任矩陣——cluster 層（供應商管）、cluster-internal 層（platform team 管）、application 層（service team 管）、跨層議題（協作）。每層有明確 owner，避免 grey zone。責任矩陣的詳細結構見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>。</li>
<li><strong>以自動化流程取代手動平台操作</strong>：遷移前的手動操作（node 升級、cert rotation、backup restore）在 managed 平台上由供應商或 IaC 接管。剩餘的手動操作（namespace provisioning、resource quota 設定、network policy review）也要自動化或流程化，避免依賴個人經驗。</li>
<li><strong>將 incident 與 release policy 接回平台治理</strong>：managed 平台的 incident 跟 self-managed 不同——control plane 故障由供應商處理，但供應商的 incident 訊號要進入自家的 incident timeline。release policy（升級節奏、canary 比例、rollback 條件）在 managed 平台上仍是 platform team 的責任。</li>
</ol>
<h2 id="回退判讀">回退判讀</h2>
<p>從 managed 回退到 self-managed 的成本極高（要重建 control plane 運維能力），因此這類遷移的回退策略通常是「在 managed 平台內回退」而非「回到 self-managed」。具體做法是保留舊叢集一段時間作為 fallback，但同時接受「回到 self-managed 不是選項」的設計假設。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime</a> 看遷移後 runtime 層的變化驗證。回 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 managed 平台與職責邊界</a> 看職責矩陣的完整結構。回 <a href="/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5 平台與入口威脅建模</a> 看遷移期攻擊面變動。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/miro-amazon-eks/">Miro on AWS containers and EKS</a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）</li>
</ul>
]]></content:encoded></item><item><title>Envoy</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/envoy/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/envoy/</guid><description>&lt;p>Envoy 是 CNCF graduated 的 service proxy、承擔三個責任：cloud-native L7 + L4 proxy（HTTP/1.1 + HTTP/2 + HTTP/3 + gRPC）、xDS dynamic config（不需 reload）、observability 內建（access log / stats / tracing）。設計取捨偏向「dynamic config + advanced traffic management + filter chain extensibility」、是 Istio / Linkerd2-proxy / AWS App Mesh / Envoy Gateway 的底層實作。&lt;/p>
&lt;p>對「service mesh data plane、API Gateway、advanced traffic management、gRPC / HTTP/2 / HTTP/3」這條路徑、Envoy 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 Envoy + 基本 reverse proxy config&lt;/li>
&lt;li>用 xDS API 動態更新 config（不 reload）&lt;/li>
&lt;li>配置 listener / route / cluster / filter chain&lt;/li>
&lt;li>看懂 Envoy access log + stats + admin endpoint&lt;/li>
&lt;li>評估 Envoy 直接用 vs 用 Istio / Envoy Gateway 抽象&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-envoy-跑起來">最短路徑：5 分鐘把 Envoy 跑起來&lt;/h2>





&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"># 1. 啟動 Envoy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker run -d --name envoy-demo &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> -p 9901:9901 -p 10000:10000 &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> -v &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/envoy.yaml:/etc/envoy/envoy.yaml:ro&amp;#34;&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> envoyproxy/envoy:v1.31-latest&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Static config 範例（&lt;code>envoy.yaml&lt;/code>）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">static_resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">listener_0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">socket_address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">address: 0.0.0.0, port_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10000&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filter_chains&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">envoy.filters.network.http_connection_manager&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">typed_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;@type&amp;#34;: &lt;/span>&lt;span class="l">type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stat_prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ingress_http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">virtual_hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">domains&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">cluster&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_backend }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http_filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">envoy.filters.http.router&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">typed_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;@type&amp;#34;: &lt;/span>&lt;span class="l">type.googleapis.com/envoy.extensions.filters.http.router.v3.Router&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clusters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">connect_timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">STRICT_DNS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">load_assignment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cluster_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">lb_endpoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">socket_address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">address: app, port_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">admin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">socket_address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">address: 0.0.0.0, port_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">9901&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 驗證 + admin endpoint&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">curl http://localhost:10000 &lt;span class="c1"># proxy 路徑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">curl http://localhost:9901/stats &lt;span class="c1"># metrics&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">curl http://localhost:9901/clusters &lt;span class="c1"># upstream health&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">curl http://localhost:9901/config_dump &lt;span class="c1"># running config&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="envoy-config-結構">Envoy config 結構&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Envoy 是 CNCF graduated 的 service proxy、承擔三個責任：cloud-native L7 + L4 proxy（HTTP/1.1 + HTTP/2 + HTTP/3 + gRPC）、xDS dynamic config（不需 reload）、observability 內建（access log / stats / tracing）。設計取捨偏向「dynamic config + advanced traffic management + filter chain extensibility」、是 Istio / Linkerd2-proxy / AWS App Mesh / Envoy Gateway 的底層實作。</p>
<p>對「service mesh data plane、API Gateway、advanced traffic management、gRPC / HTTP/2 / HTTP/3」這條路徑、Envoy 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 Envoy + 基本 reverse proxy config</li>
<li>用 xDS API 動態更新 config（不 reload）</li>
<li>配置 listener / route / cluster / filter chain</li>
<li>看懂 Envoy access log + stats + admin endpoint</li>
<li>評估 Envoy 直接用 vs 用 Istio / Envoy Gateway 抽象</li>
</ol>
<h2 id="最短路徑5-分鐘把-envoy-跑起來">最短路徑：5 分鐘把 Envoy 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 啟動 Envoy</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d --name envoy-demo <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -p 9901:9901 -p 10000:10000 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  -v <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/envoy.yaml:/etc/envoy/envoy.yaml:ro&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  envoyproxy/envoy:v1.31-latest</span></span></code></pre></div><p>Static config 範例（<code>envoy.yaml</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">static_resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">listeners</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">listener_0</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">socket_address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">address: 0.0.0.0, port_value</span><span class="p">:</span><span class="w"> </span><span class="m">10000</span><span class="w"> </span>}<span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">filter_chains</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">filters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">envoy.filters.network.http_connection_manager</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">typed_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">          </span><span class="nt">&#34;@type&#34;: </span><span class="l">type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">          </span><span class="nt">stat_prefix</span><span class="p">:</span><span class="w"> </span><span class="l">ingress_http</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">          </span><span class="nt">route_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">            </span><span class="nt">virtual_hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">backend</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">              </span><span class="nt">domains</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;*&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">              </span><span class="nt">routes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">              </span>- <span class="nt">match</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/&#34;</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">                </span><span class="nt">route</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">cluster</span><span class="p">:</span><span class="w"> </span><span class="l">service_backend }</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">http_filters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">envoy.filters.http.router</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">            </span><span class="nt">typed_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">              </span><span class="nt">&#34;@type&#34;: </span><span class="l">type.googleapis.com/envoy.extensions.filters.http.router.v3.Router</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span><span class="nt">clusters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">service_backend</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">    </span><span class="nt">connect_timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">STRICT_DNS</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">    </span><span class="nt">load_assignment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">      </span><span class="nt">cluster_name</span><span class="p">:</span><span class="w"> </span><span class="l">service_backend</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">      </span><span class="nt">endpoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">      </span>- <span class="nt">lb_endpoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">        </span>- <span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">socket_address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">address: app, port_value</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w"> </span>}<span class="w"> </span>}<span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w"></span><span class="nt">admin</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">  </span><span class="nt">address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">socket_address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">address: 0.0.0.0, port_value</span><span class="p">:</span><span class="w"> </span><span class="m">9901</span><span class="w"> </span>}<span class="w"> </span>}</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 3. 驗證 + admin endpoint</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl http://localhost:10000                    <span class="c1"># proxy 路徑</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">curl http://localhost:9901/stats               <span class="c1"># metrics</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">curl http://localhost:9901/clusters            <span class="c1"># upstream health</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">curl http://localhost:9901/config_dump         <span class="c1"># running config</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="envoy-config-結構">Envoy config 結構</h3>
<p>子議題：</p>
<ul>
<li>Listener：listen address + filter chain</li>
<li>Route：path matching + cluster routing</li>
<li>Cluster：upstream endpoint discovery + load balancing</li>
<li>Endpoint：實際 backend</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
</ul>
<h3 id="static-vs-dynamic-config">Static vs Dynamic config</h3>
<p>子議題：</p>
<ul>
<li>Static：YAML 寫死、適合 dev / debug</li>
<li>Dynamic（xDS）：control plane push config</li>
<li>xDS protocol：LDS / RDS / CDS / EDS / SDS</li>
<li>對應 control plane：Istio / Gloo / 自寫</li>
</ul>
<h3 id="admin-endpoint">Admin endpoint</h3>
<p>子議題：</p>
<ul>
<li>/stats / /clusters / /config_dump / /listeners / /server_info</li>
<li>runtime config（/runtime_modify）</li>
<li>對應 observability 跟 debug</li>
<li>對應指令：<code>curl admin:9901/clusters</code></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="xds-api-細節">xDS API 細節</h3>
<p>子議題：</p>
<ul>
<li>LDS / RDS / CDS / EDS / SDS / RTDS / ECDS</li>
<li>ADS（Aggregated Discovery Service）統一通道</li>
<li>Delta xDS（incremental）vs SOTW（State of the World）</li>
<li>對應案例 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio</a></li>
</ul>
<h3 id="filter-chainhttp--network-filter">Filter chain（HTTP / network filter）</h3>
<p>子議題：</p>
<ul>
<li>HTTP filters：router / cors / fault / rate_limit / ext_authz / jwt_authn</li>
<li>Network filters：tcp_proxy / mongo_proxy / redis_proxy</li>
<li>自訂 filter（C++ / WebAssembly）</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">security 模組</a>（ext_authz）</li>
</ul>
<h3 id="observability-內建">Observability 內建</h3>
<p>子議題：</p>
<ul>
<li>Access log（structured / configurable format）</li>
<li>Stats（envoy 內建 metrics）</li>
<li>Distributed tracing（Jaeger / Zipkin / Datadog / OpenTelemetry）</li>
<li>對應 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a></li>
</ul>
<h3 id="envoy-gateway--emissary--gloo">Envoy Gateway / Emissary / Gloo</h3>
<p>子議題：</p>
<ul>
<li>Envoy Gateway：Gateway API native（CNCF project）</li>
<li>Emissary（前 Ambassador）：K8s ingress + API Gateway</li>
<li>Gloo：Solo.io 商業 Envoy 整合</li>
<li>選型判讀：純 K8s ingress → Envoy Gateway；商業支援 → Gloo / Emissary</li>
</ul>
<h3 id="service-mesh-data-plane">Service mesh data plane</h3>
<p>子議題：</p>
<ul>
<li>Istio：control plane + Envoy sidecar</li>
<li>Linkerd2：自家 Rust proxy（不是 Envoy）— Linkerd2-proxy</li>
<li>Cilium Service Mesh：eBPF + Envoy</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio governance</a></li>
</ul>
<h3 id="webassembly-extension">WebAssembly extension</h3>
<p>子議題：</p>
<ul>
<li>WASM filter：跨語言寫 Envoy extension（Rust / AssemblyScript / Go）</li>
<li>跟 Lua（OpenResty 模式）對比</li>
<li>適合：custom auth / rate limit / metric collection</li>
</ul>
<h3 id="advanced-traffic-management">Advanced traffic management</h3>
<p>子議題：</p>
<ul>
<li>Retry / Circuit breaker / Outlier detection</li>
<li>Timeout（connect / request / idle）</li>
<li>Traffic split（canary / blue-green / mirror）</li>
<li>Rate limit（local + global）</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="config-sync-失敗">Config sync 失敗</h3>
<p>操作原則：xDS control plane 連不上 / config 格式錯。判讀：admin /stats 看 update_failure、/config_dump 看當前 config。</p>
<h3 id="listener-config-error">Listener config error</h3>
<p>操作原則：YAML 格式錯、port 衝突、bind address 錯。判讀：startup log + admin /listeners。</p>
<h3 id="cluster-endpoint-全-unhealthy">Cluster endpoint 全 unhealthy</h3>
<p>操作原則：health check 失敗、SDS 沒提供 cert、network 不通。判讀：admin /clusters 看 endpoint state。</p>
<h3 id="circuit-breaker-trip">Circuit breaker trip</h3>
<p>操作原則：upstream 失敗率 &gt; threshold、Envoy 主動切。判讀：admin /stats 看 cb 相關 metric。</p>
<h3 id="tracing-missing-spans">Tracing missing spans</h3>
<p>操作原則：tracer config + sampler rate 設錯、context propagation 不對。對應 <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">04 observability OTel</a>。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>配置簡單 / 小場景</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a></td>
      </tr>
      <tr>
          <td>Cloud-native auto-discovery</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a></td>
      </tr>
      <tr>
          <td>K8s ingress only</td>
          <td>Ingress-nginx / Envoy Gateway / Gateway API</td>
      </tr>
      <tr>
          <td>Service mesh control plane</td>
          <td>Istio / Linkerd / Consul Connect</td>
      </tr>
      <tr>
          <td>Edge proxy / CDN</td>
          <td>Cloudflare / Fastly / CloudFront</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 Envoy YAML schema reference</li>
<li>xDS protocol binary format</li>
<li>各 Istio / Gloo / Emissary 細節（見各自 docs）</li>
<li>Envoy C++ filter 開發</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio governance</a></td>
          <td>Envoy-based service mesh 在大規模叢集的分批升級與可重播流程</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Envoy 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>Tradeshift 選 Linkerd（非 Envoy）做切流、對照 Envoy/Istio 的取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>Envoy outlier detection / circuit breaker / draining listener 是回退面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>大規模 / 複雜 traffic / 多 DC → Envoy mesh 才能撐住協同節奏</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Envoy 案例</strong>：Lyft 自家 Envoy production 案例、Stripe / Reddit 用 Envoy 邊緣案例、Envoy Gateway 早期 adopter。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a>、<a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">04 observability OTel</a>、<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a></li>
</ul>
]]></content:encoded></item><item><title>5.5 平台與入口威脅建模（Threat Modeling）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/</guid><description>&lt;p>平台與入口威脅建模的核心責任是把部署平台的弱點維持在可操作的概念層。本章的輸出是平台問題地圖、案例對照與交接條件，讓實作前決策可先對齊，避免進入 YAML / unit file / LB rule 前就已經漏掉攻擊面。&lt;/p>
&lt;h2 id="服務環節問題地圖">服務環節問題地圖&lt;/h2>
&lt;p>平台弱點盤點的第一層是把服務環節跟攻擊面對齊。同一個服務交付路徑上、入口、生命週期、設定、交付節奏各自有不同失分模式。&lt;/p>
&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;/td>
 &lt;td>入口分級與實際可達範圍不一致&lt;/td>
 &lt;td>入口清單與責任鏈要先對齊&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生命週期訊號&lt;/td>
 &lt;td>readiness、draining、shutdown 節奏不一致&lt;/td>
 &lt;td>平台合約要先定義再驗證&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設定與密鑰下發&lt;/td>
 &lt;td>設定漂移與權限擴張同時發生&lt;/td>
 &lt;td>高風險設定要進 release gate，並分離 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/" data-link-title="7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過" data-link-desc="BIG-IP 組態管理入口認證繞過如何放大邊界設備治理壓力">F5 BIG-IP 2023&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交付切換節奏&lt;/td>
 &lt;td>回滾與切換條件不清晰&lt;/td>
 &lt;td>先定停損條件再定交付速度&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="入口暴露面">入口暴露面&lt;/h3>
&lt;p>入口暴露面的主要弱點判讀是「實際可達範圍是否超過設計意圖」。容器化、service mesh、ingress controller 升級、新增 LoadBalancer 都可能無意中把內部服務暴露到公網。入口清單跟責任鏈先對齊、能避免發版本就改變了攻擊面。升級流程跟回退窗口設計見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程&lt;/a>。&lt;/p>
&lt;p>入口暴露面的盤點要區分三類入口，各自有不同的失分模式：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>設計意圖內的入口&lt;/strong>（Ingress / LoadBalancer Service / API Gateway）：這些入口有明確 owner、有 WAF / TLS 保護。弱點在於設定漂移——port 範圍擴大、路由規則放寬、wildcard host 引入。盤點方式是定期比對實際 Ingress 規則與設計意圖。&lt;/li>
&lt;li>&lt;strong>隱性入口&lt;/strong>（NodePort、hostNetwork pod、debug endpoint、metrics endpoint）：這些入口在設計時不被視為外部可達，但在特定網路拓樸下可能從外部存取。NodePort 預設 range 30000-32767 在某些雲端 security group 設定下可能對外開放。metrics endpoint（/metrics、/debug/pprof）常不帶認證、暴露服務內部狀態。&lt;/li>
&lt;li>&lt;strong>暫態入口&lt;/strong>（kubectl port-forward、臨時 LoadBalancer、tunnel 測試）：開發或除錯時臨時打開的入口，使用後忘記關閉。這類入口沒有 WAF、沒有 TLS、沒有 audit log，是攻擊面中最難盤點的部分。&lt;/li>
&lt;/ol>
&lt;p>Tunnel 形態的入口（cloudflared、Tailscale Funnel）有獨立的弱點盤點框架，見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口&lt;/a> 的認證疊法段。&lt;/p>
&lt;h3 id="生命週期訊號">生命週期訊號&lt;/h3>
&lt;p>生命週期訊號的弱點聚焦於脆弱窗口期被利用：readiness 過早通過、shutdown 階段仍在處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight&lt;/a> request、drain 視窗內接收新請求，都會把短暫的脆弱窗口拉長。&lt;/p>
&lt;p>脆弱窗口的判讀要跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 的生命週期狀態對齊：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>startup → readiness 窗口&lt;/strong>：服務正在初始化、依賴尚未驗證、安全中介軟體（WAF sidecar、auth proxy）可能還沒就緒。此時如果 readiness 過早通過讓流量進來，請求可能繞過安全層直接打到後端。&lt;/li>
&lt;li>&lt;strong>readiness → drain 窗口&lt;/strong>：正常服務狀態，弱點集中在 readiness 條件太鬆——只檢查 port 可達但 auth middleware 沒初始化。&lt;/li>
&lt;li>&lt;strong>drain → shutdown 窗口&lt;/strong>：服務正在收斂，此時安全元件（rate limiter、WAF）可能已停止更新規則但仍在處理請求。攻擊者若在 drain 窗口送入惡意請求，安全元件可能無法正常攔截。&lt;/li>
&lt;/ul>
&lt;h3 id="設定與密鑰下發">設定與密鑰下發&lt;/h3>
&lt;p>設定與密鑰下發是最容易被忽略的維度。Image 沒變但 config / secret 變了、權限因 RBAC 漂移擴張、feature flag 在 production 偷偷開啟未經 review 的新行為。這些變更不走 release gate 的話，攻擊者有大量低噪音入口可以利用。&lt;/p></description><content:encoded><![CDATA[<p>平台與入口威脅建模的核心責任是把部署平台的弱點維持在可操作的概念層。本章的輸出是平台問題地圖、案例對照與交接條件，讓實作前決策可先對齊，避免進入 YAML / unit file / LB rule 前就已經漏掉攻擊面。</p>
<h2 id="服務環節問題地圖">服務環節問題地圖</h2>
<p>平台弱點盤點的第一層是把服務環節跟攻擊面對齊。同一個服務交付路徑上、入口、生命週期、設定、交付節奏各自有不同失分模式。</p>
<table>
  <thead>
      <tr>
          <th>環節</th>
          <th>主要問題</th>
          <th>注意事項</th>
          <th>優先案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>入口暴露面</td>
          <td>入口分級與實際可達範圍不一致</td>
          <td>入口清單與責任鏈要先對齊</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023</a></td>
      </tr>
      <tr>
          <td>生命週期訊號</td>
          <td>readiness、draining、shutdown 節奏不一致</td>
          <td>平台合約要先定義再驗證</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024</a></td>
      </tr>
      <tr>
          <td>設定與密鑰下發</td>
          <td>設定漂移與權限擴張同時發生</td>
          <td>高風險設定要進 release gate，並分離 <a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane</a></td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/" data-link-title="7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過" data-link-desc="BIG-IP 組態管理入口認證繞過如何放大邊界設備治理壓力">F5 BIG-IP 2023</a></td>
      </tr>
      <tr>
          <td>交付切換節奏</td>
          <td>回滾與切換條件不清晰</td>
          <td>先定停損條件再定交付速度</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024</a></td>
      </tr>
  </tbody>
</table>
<h3 id="入口暴露面">入口暴露面</h3>
<p>入口暴露面的主要弱點判讀是「實際可達範圍是否超過設計意圖」。容器化、service mesh、ingress controller 升級、新增 LoadBalancer 都可能無意中把內部服務暴露到公網。入口清單跟責任鏈先對齊、能避免發版本就改變了攻擊面。升級流程跟回退窗口設計見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程</a>。</p>
<p>入口暴露面的盤點要區分三類入口，各自有不同的失分模式：</p>
<ol>
<li><strong>設計意圖內的入口</strong>（Ingress / LoadBalancer Service / API Gateway）：這些入口有明確 owner、有 WAF / TLS 保護。弱點在於設定漂移——port 範圍擴大、路由規則放寬、wildcard host 引入。盤點方式是定期比對實際 Ingress 規則與設計意圖。</li>
<li><strong>隱性入口</strong>（NodePort、hostNetwork pod、debug endpoint、metrics endpoint）：這些入口在設計時不被視為外部可達，但在特定網路拓樸下可能從外部存取。NodePort 預設 range 30000-32767 在某些雲端 security group 設定下可能對外開放。metrics endpoint（/metrics、/debug/pprof）常不帶認證、暴露服務內部狀態。</li>
<li><strong>暫態入口</strong>（kubectl port-forward、臨時 LoadBalancer、tunnel 測試）：開發或除錯時臨時打開的入口，使用後忘記關閉。這類入口沒有 WAF、沒有 TLS、沒有 audit log，是攻擊面中最難盤點的部分。</li>
</ol>
<p>Tunnel 形態的入口（cloudflared、Tailscale Funnel）有獨立的弱點盤點框架，見 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a> 的認證疊法段。</p>
<h3 id="生命週期訊號">生命週期訊號</h3>
<p>生命週期訊號的弱點聚焦於脆弱窗口期被利用：readiness 過早通過、shutdown 階段仍在處理 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request、drain 視窗內接收新請求，都會把短暫的脆弱窗口拉長。</p>
<p>脆弱窗口的判讀要跟 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 的生命週期狀態對齊：</p>
<ul>
<li><strong>startup → readiness 窗口</strong>：服務正在初始化、依賴尚未驗證、安全中介軟體（WAF sidecar、auth proxy）可能還沒就緒。此時如果 readiness 過早通過讓流量進來，請求可能繞過安全層直接打到後端。</li>
<li><strong>readiness → drain 窗口</strong>：正常服務狀態，弱點集中在 readiness 條件太鬆——只檢查 port 可達但 auth middleware 沒初始化。</li>
<li><strong>drain → shutdown 窗口</strong>：服務正在收斂，此時安全元件（rate limiter、WAF）可能已停止更新規則但仍在處理請求。攻擊者若在 drain 窗口送入惡意請求，安全元件可能無法正常攔截。</li>
</ul>
<h3 id="設定與密鑰下發">設定與密鑰下發</h3>
<p>設定與密鑰下發是最容易被忽略的維度。Image 沒變但 config / secret 變了、權限因 RBAC 漂移擴張、feature flag 在 production 偷偷開啟未經 review 的新行為。這些變更不走 release gate 的話，攻擊者有大量低噪音入口可以利用。</p>
<p>設定變更的弱點盤點要分兩個方向：</p>
<p><strong>顯式設定變更</strong>（ConfigMap、Secret、feature flag 更新）：變更本身是可追蹤的，弱點在於 review 機制是否涵蓋高風險設定。payment endpoint、auth provider URL、rate limit 閾值、CORS 允許來源——這些設定的變更影響跟程式碼變更等量，應走同等 review 流程。設定變更的 review 與 rollout 策略見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Config Boundary</a>。</p>
<p><strong>隱式設定漂移</strong>（RBAC 逐步放寬、network policy 例外累積、service account 權限擴張）：這類變更是多次小修改累積的結果，單次變更看起來合理但累積後超出安全邊界。盤點方式是定期用 policy-as-code（OPA/Gatekeeper、Kyverno）掃描 cluster 內的 RBAC binding、network policy、pod security 設定，跟 baseline 比對偏移程度。</p>
<h3 id="交付切換節奏">交付切換節奏</h3>
<p>交付切換節奏的弱點判讀是「在不穩定窗口期、系統是否還有防禦能力」。Canary / rollout / rollback 期間 5xx 升高、connection 重建、auth 短暫失敗，會掩蓋同期間的攻擊訊號。沒有先定停損條件就推交付速度、是把切換期變成攻擊期的常見做法。</p>
<p>交付窗口期的防禦能力退化有兩個機制：</p>
<p><strong>訊號淹沒</strong>：rollout 本身產生的短暫錯誤（5xx spike、reconnect、auth retry）跟攻擊訊號長得一樣。事故團隊在切流期把所有異常歸因於部署變更，攻擊者剛好利用這個注意力盲區。對策是把切流期 alert 跟安全 alert 分流到不同 channel，安全訊號走獨立通道、由 security on-call 獨立判讀。</p>
<p><strong>防禦元件版本不一致</strong>：<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a> 期間新舊版本同時在線，WAF 規則、rate limit 設定、auth middleware 版本可能不同。攻擊者可以針對舊版本的已知弱點送流量，利用 canary 期間的路由特性讓流量到達舊版本。對策是把安全元件的更新跟應用版本解耦——WAF 規則、rate limit 是平台層設定，應在所有版本一致生效。</p>
<h2 id="案例對照表情境--判讀--注意事項--路由章節">案例對照表（情境 → 判讀 → 注意事項 → 路由章節）</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>判讀</th>
          <th>注意事項</th>
          <th>路由章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>外網可達入口在發版後增加</td>
          <td>入口分級與交付節奏存在脫鉤</td>
          <td>入口盤點要成為交付前條件</td>
          <td><a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract</a></td>
      </tr>
      <tr>
          <td>readiness 通過但實際流量錯誤率上升</td>
          <td>生命週期合約與流量模型不一致</td>
          <td>探針、draining、shutdown 要同批驗證</td>
          <td><a href="/blog/backend/06-reliability/failure-mode-pre-mortem/" data-link-title="6.5 失敗模式預判（Pre-mortem 與 FMEA）" data-link-desc="用 pre-mortem 反向推導失敗路徑、用 FMEA 分類軸評估驗證缺口，把可靠性盲區變成可排序的改善輸入">6.5 失敗模式預判</a></td>
      </tr>
      <tr>
          <td>設定異動與異常事件同時出現</td>
          <td>設定漂移可能已跨越安全邊界</td>
          <td>設定審查與責任追蹤要同步維護</td>
          <td><a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.5 復盤與改進追蹤</a></td>
      </tr>
      <tr>
          <td>切流期間入侵告警被淹沒</td>
          <td>rollout 噪音掩蓋攻擊訊號</td>
          <td>切流期 alert 分流、攻擊訊號獨立通道</td>
          <td><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a></td>
      </tr>
  </tbody>
</table>
<p>「外網可達入口在發版後增加」是平台變更弱點盤點的頭號議題。Ingress class 換、Service type 改、LB 規則重組都可能讓原本內部服務獲得外部 IP。把入口盤點放進 release pre-check、能讓這類變更在合併前被擋下。</p>
<p>「readiness 通過但實際流量錯誤率上升」揭露 readiness probe 設計失誤的弱點面向。Probe 只回 200 OK 不代表服務可承受真實流量、攻擊者剛好可以在這個窗口送高頻 request 看是否壓垮服務。Readiness 反映依賴就緒條件而非單一探針成功、能縮短這個窗口。</p>
<p>「設定異動與異常事件同時出現」是 config rollout 的弱點風險。Config 變更後出現異常事件、可能是設定本身的問題、也可能是攻擊者剛好利用了設定窗口。Config 審查跟責任追蹤同步維護、能讓事後復盤分辨兩者。</p>
<p>「切流期間入侵告警被淹沒」是新加入的議題。切流產生大量短暫 5xx、reconnect、auth retry、可能淹沒真正的攻擊訊號。把切流期 alert 跟一般 alert 分流、攻擊訊號走獨立通道、能避免攻擊在切流窗口下被忽略。</p>
<h2 id="平台遷移期的攻擊面變動">平台遷移期的攻擊面變動</h2>
<p>對應 5.C1 / 5.C4 / 5.C5 揭露的遷移分段切換流程、本段從弱點盤點角度補充其攻擊面變動風險（case 庫未直接揭露此角度、屬通用工程經驗）。遷移期的職責邊界重訂見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>、弱點盤點跟治理視角合用才完整。</p>
<p>平台遷移（self-managed → managed、單 cluster → 多 cluster、舊版本 → 新版本）會短期擴大攻擊面、然後逐步收斂。遷移期顯式管理攻擊面變化、避免雙軌期變成攻擊面雙倍期。</p>
<p>可重複套用的弱點判讀：</p>
<ol>
<li><strong>盤點雙軌期入口</strong>：舊平台跟新平台的入口清單分別列出、確認新平台不繼承舊平台已知漏洞、舊平台的廢棄入口確實關閉。</li>
<li><strong>identity / credential 重新對位</strong>：service account、API token、TLS cert 在新平台是否走新的 rotation flow、舊平台的 credential 是否在切換完成後撤除。</li>
<li><strong>observability 對應更新</strong>：新平台的 audit log、access log、security event 是否進入同一個 SIEM / 告警通道、避免遷移期內攻擊訊號掉到觀測缺口。</li>
<li><strong>回退路徑的攻擊面評估</strong>：回退到舊平台時、舊平台是否仍處於最新 patch 狀態、回退本身會不會把已修補的漏洞重新引入。</li>
</ol>
<p>遷移計畫要把資安 review 列為 gate 之一、讓遷移期攻擊面變動進入可見治理流程。沒有這道 gate、遷移期容易被當成純技術項目處理、漏掉攻擊面的隱性擴大。</p>
<h2 id="到實作前的最後一層">到實作前的最後一層</h2>
<p>弱點盤點在概念層回答的是平台風險判讀與交接節奏。當討論進入 Kubernetes 欄位、LB 規則、系統服務參數或腳本配置時，就代表已進入實作層。</p>
<p>實作層的防護驗證跟概念層分工：實作層看具體 YAML / config / rule 是否符合 hardening baseline、概念層看交付路徑跟責任鏈是否完整。兩者都做才能讓平台變更的攻擊面在 release 前可見。</p>
<p>進實作層後接 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護模組</a> 的具體 hardening 章節、跟 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 對齊入口分級。</p>
]]></content:encoded></item><item><title>7.5 Kubernetes、systemd 與 load balancer 合約</title><link>https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/deployment-contracts/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/deployment-contracts/</guid><description>&lt;p>部署平台合約的核心責任是讓 Go 服務的生命週期和外部調度系統對齊。程式內部需要清楚的 context、shutdown &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check-liveness/" data-link-title="Liveness" data-link-desc="說明平台如何判斷 process 是否仍然存活，以及何時應重啟">health / liveness&lt;/a> 與 memory limit；Kubernetes、systemd、load balancer 或雲端平台則決定這些訊號何時被觸發與如何被解讀。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 shutdown、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 與 connection draining 的順序&lt;/li>
&lt;li>看懂平台 timeout 對 Go server 的影響&lt;/li>
&lt;li>分辨 health 與 readiness 的不同責任&lt;/li>
&lt;li>把 memory limit 與 Go runtime 的資源管理接在一起&lt;/li>
&lt;li>讓部署平台和程式彼此遵守同一份合約&lt;/li>
&lt;/ol>
&lt;h2 id="前置章節">前置章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/gc-memory-limit/" data-link-title="3.1 GC 與 memory limit" data-link-desc="理解 debug.SetMemoryLimit 在長時間服務中的用途">Go 進階：GC 與 memory limit&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go 進階：graceful shutdown 與 signal handling&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/06-production-operations/health-diagnostics/" data-link-title="6.2 健康檢查與診斷 endpoint" data-link-desc="區分服務可用性與工程診斷入口">Go 進階：健康檢查與診斷 endpoint&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Backend：Graceful Shutdown&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">Backend：Failover&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="後續撰寫方向">後續撰寫方向&lt;/h2>
&lt;ol>
&lt;li>SIGTERM、shutdown timeout、readiness false 與 connection draining 的順序。&lt;/li>
&lt;li>Kubernetes &lt;code>terminationGracePeriodSeconds&lt;/code> 與 Go &lt;code>http.Server.Shutdown&lt;/code> 如何配合。&lt;/li>
&lt;li>Load balancer idle timeout 如何影響 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> heartbeat 參數。&lt;/li>
&lt;li>Container memory limit、Go memory limit 與 OOM killer 之間的關係。&lt;/li>
&lt;li>systemd restart policy 與 health endpoint 的責任分工。&lt;/li>
&lt;/ol>
&lt;h2 id="觀察平台會主動改變服務生命週期">【觀察】平台會主動改變服務生命週期&lt;/h2>
&lt;p>Go 程式不會在真空裡執行。Kubernetes、systemd、load balancer、container runtime 都會影響服務何時接新請求、何時開始收尾、何時被強制終止。這表示程式不只要「能跑」，還要能跟平台協調。&lt;/p>
&lt;p>常見的生命週期訊號有：&lt;/p>
&lt;ul>
&lt;li>SIGTERM&lt;/li>
&lt;li>readiness false&lt;/li>
&lt;li>HTTP shutdown&lt;/li>
&lt;li>connection draining&lt;/li>
&lt;li>memory pressure&lt;/li>
&lt;/ul>
&lt;h2 id="判讀health-與-readiness-有不同合約">【判讀】health 與 readiness 有不同合約&lt;/h2>
&lt;p>health 通常表示服務自己還活著，readiness 則表示它是否適合接新流量。&lt;/p>
&lt;ul>
&lt;li>health 可以用來讓平台知道 process 還活著。&lt;/li>
&lt;li>readiness 可以用來讓 load balancer 停止送新請求。&lt;/li>
&lt;/ul>
&lt;p>如果兩者混在一起，部署時就容易出現「服務還沒收尾就被塞新流量」或「其實還能接流量卻被誤判下線」的問題。&lt;/p>
&lt;h2 id="策略shutdown-應該是可預期流程">【策略】shutdown 應該是可預期流程&lt;/h2>
&lt;p>典型的 shutdown 順序是：&lt;/p>
&lt;ol>
&lt;li>接收到停止訊號。&lt;/li>
&lt;li>先把 readiness 關掉。&lt;/li>
&lt;li>停止接新流量。&lt;/li>
&lt;li>讓現有 request / worker / websocket 收尾。&lt;/li>
&lt;li>超時後強制結束。&lt;/li>
&lt;/ol>
&lt;p>這個順序能讓平台有時間把流量移走，也讓應用有時間清理資源。&lt;/p>
&lt;h2 id="執行資源限制要和-runtime-觀念一起看">【執行】資源限制要和 runtime 觀念一起看&lt;/h2>
&lt;p>container memory limit 不只是部署平台的事，也會影響 Go runtime 的行為。當可用記憶體變少時，應用更需要控制：&lt;/p>
&lt;ul>
&lt;li>goroutine 數量&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 大小&lt;/li>
&lt;li>cache 體積&lt;/li>
&lt;li>in-memory &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 長度&lt;/li>
&lt;/ul>
&lt;p>如果這些沒有限制，平台的 OOM killer 可能會比你的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a> 先來。&lt;/p></description><content:encoded><![CDATA[<p>部署平台合約的核心責任是讓 Go 服務的生命週期和外部調度系統對齊。程式內部需要清楚的 context、shutdown <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/health-check-liveness/" data-link-title="Liveness" data-link-desc="說明平台如何判斷 process 是否仍然存活，以及何時應重啟">health / liveness</a> 與 memory limit；Kubernetes、systemd、load balancer 或雲端平台則決定這些訊號何時被觸發與如何被解讀。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 shutdown、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 與 connection draining 的順序</li>
<li>看懂平台 timeout 對 Go server 的影響</li>
<li>分辨 health 與 readiness 的不同責任</li>
<li>把 memory limit 與 Go runtime 的資源管理接在一起</li>
<li>讓部署平台和程式彼此遵守同一份合約</li>
</ol>
<h2 id="前置章節">前置章節</h2>
<ul>
<li><a href="/blog/go-advanced/03-runtime-profiling/gc-memory-limit/" data-link-title="3.1 GC 與 memory limit" data-link-desc="理解 debug.SetMemoryLimit 在長時間服務中的用途">Go 進階：GC 與 memory limit</a></li>
<li><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go 進階：graceful shutdown 與 signal handling</a></li>
<li><a href="/blog/go-advanced/06-production-operations/health-diagnostics/" data-link-title="6.2 健康檢查與診斷 endpoint" data-link-desc="區分服務可用性與工程診斷入口">Go 進階：健康檢查與診斷 endpoint</a></li>
<li><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Backend：Graceful Shutdown</a></li>
<li><a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">Backend：Failover</a></li>
</ul>
<h2 id="後續撰寫方向">後續撰寫方向</h2>
<ol>
<li>SIGTERM、shutdown timeout、readiness false 與 connection draining 的順序。</li>
<li>Kubernetes <code>terminationGracePeriodSeconds</code> 與 Go <code>http.Server.Shutdown</code> 如何配合。</li>
<li>Load balancer idle timeout 如何影響 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> heartbeat 參數。</li>
<li>Container memory limit、Go memory limit 與 OOM killer 之間的關係。</li>
<li>systemd restart policy 與 health endpoint 的責任分工。</li>
</ol>
<h2 id="觀察平台會主動改變服務生命週期">【觀察】平台會主動改變服務生命週期</h2>
<p>Go 程式不會在真空裡執行。Kubernetes、systemd、load balancer、container runtime 都會影響服務何時接新請求、何時開始收尾、何時被強制終止。這表示程式不只要「能跑」，還要能跟平台協調。</p>
<p>常見的生命週期訊號有：</p>
<ul>
<li>SIGTERM</li>
<li>readiness false</li>
<li>HTTP shutdown</li>
<li>connection draining</li>
<li>memory pressure</li>
</ul>
<h2 id="判讀health-與-readiness-有不同合約">【判讀】health 與 readiness 有不同合約</h2>
<p>health 通常表示服務自己還活著，readiness 則表示它是否適合接新流量。</p>
<ul>
<li>health 可以用來讓平台知道 process 還活著。</li>
<li>readiness 可以用來讓 load balancer 停止送新請求。</li>
</ul>
<p>如果兩者混在一起，部署時就容易出現「服務還沒收尾就被塞新流量」或「其實還能接流量卻被誤判下線」的問題。</p>
<h2 id="策略shutdown-應該是可預期流程">【策略】shutdown 應該是可預期流程</h2>
<p>典型的 shutdown 順序是：</p>
<ol>
<li>接收到停止訊號。</li>
<li>先把 readiness 關掉。</li>
<li>停止接新流量。</li>
<li>讓現有 request / worker / websocket 收尾。</li>
<li>超時後強制結束。</li>
</ol>
<p>這個順序能讓平台有時間把流量移走，也讓應用有時間清理資源。</p>
<h2 id="執行資源限制要和-runtime-觀念一起看">【執行】資源限制要和 runtime 觀念一起看</h2>
<p>container memory limit 不只是部署平台的事，也會影響 Go runtime 的行為。當可用記憶體變少時，應用更需要控制：</p>
<ul>
<li>goroutine 數量</li>
<li><a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 大小</li>
<li>cache 體積</li>
<li>in-memory <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 長度</li>
</ul>
<p>如果這些沒有限制，平台的 OOM killer 可能會比你的 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 先來。</p>
<h2 id="延伸平台合約要被測試">【延伸】平台合約要被測試</h2>
<p>部署平台合約需要在測試或預備環境驗證。至少要確認：</p>
<ul>
<li>shutdown 時 request 是否停止接入</li>
<li>worker 是否有機會收尾</li>
<li>WebSocket 是否有 close path</li>
<li>health 與 readiness 是否分工清楚</li>
</ul>
<h2 id="本章不處理">本章不處理</h2>
<p>本章不會完整教 Kubernetes 或 systemd 操作。重點是讓 Go 程式設計能清楚暴露平台需要的生命週期訊號。</p>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 Go 的 shutdown 與 runtime 限制；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go-advanced/03-runtime-profiling/gc-memory-limit/" data-link-title="3.1 GC 與 memory limit" data-link-desc="理解 debug.SetMemoryLimit 在長時間服務中的用途">Go 進階：GC 與 memory limit</a></li>
<li><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go 進階：graceful shutdown 與 signal handling</a></li>
<li><a href="/blog/go-advanced/06-production-operations/health-diagnostics/" data-link-title="6.2 健康檢查與診斷 endpoint" data-link-desc="區分服務可用性與工程診斷入口">Go 進階：健康檢查與診斷 endpoint</a></li>
</ul>
]]></content:encoded></item><item><title>模組五：部署平台與網路入口</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/</guid><description>&lt;p>部署平台模組的核心目標是說明服務如何和外部調度、網路入口與資源限制對齊。語言教材會處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a>、health / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 檢查與 signal handling；本模組負責平台設定與操作語意。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors&lt;/a> — T1 收錄 Kubernetes / Docker / systemd / nginx / Envoy / AWS ELB / Terraform / Traefik / Consul，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分類&lt;/th>
 &lt;th>內容方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container&lt;/a>&lt;/td>
 &lt;td>image build、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Kubernetes&lt;/td>
 &lt;td>deployment、pod lifecycle、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>systemd&lt;/td>
 &lt;td>service unit、restart policy、signal、journal&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">Load balancer&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">Service Registry&lt;/a>&lt;/td>
 &lt;td>實例如何註冊、更新與摘除&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">Service discovery&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint&lt;/a> discovery、DNS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config rollout&lt;/a>&lt;/td>
 &lt;td>設定如何安全下發到正在運作的服務實例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config&lt;/a>&lt;/td>
 &lt;td>environment variable、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN 與邊緣分發&lt;/td>
 &lt;td>邊緣快取、origin protection、purge 與 invalidation、stale-while-revalidate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;p>章節編號是主題分類，不是閱讀順序。建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 理解 startup / readiness / liveness / shutdown / drain 的責任分類，再按 5.1 → 5.2 → 5.3 → 5.4 進入平台實作層。5.5（威脅建模）和 5.7（boundary 分類）適合讀完 5.1-5.4 後做概念整理。5.8（實作示範）是 5.2 + 5.3 的操作化，適合最後讀。&lt;/p></description><content:encoded><![CDATA[<p>部署平台模組的核心目標是說明服務如何和外部調度、網路入口與資源限制對齊。語言教材會處理 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>、health / <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 檢查與 signal handling；本模組負責平台設定與操作語意。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors</a> — T1 收錄 Kubernetes / Docker / systemd / nginx / Envoy / AWS ELB / Terraform / Traefik / Consul，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container</a></td>
          <td>image build、<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a>、<a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a></td>
      </tr>
      <tr>
          <td>Kubernetes</td>
          <td>deployment、pod lifecycle、<a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>、<a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update</a></td>
      </tr>
      <tr>
          <td>systemd</td>
          <td>service unit、restart policy、signal、journal</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">Load balancer</a></td>
          <td><a href="/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>、<a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a>、<a href="/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">Service Registry</a></td>
          <td>實例如何註冊、更新與摘除</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">Service discovery</a></td>
          <td><a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> discovery、DNS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config rollout</a></td>
          <td>設定如何安全下發到正在運作的服務實例</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a></td>
          <td>environment variable、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、<a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag</a></td>
      </tr>
      <tr>
          <td>CDN 與邊緣分發</td>
          <td>邊緣快取、origin protection、purge 與 invalidation、stale-while-revalidate</td>
      </tr>
  </tbody>
</table>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<p>章節編號是主題分類，不是閱讀順序。建議先讀 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 理解 startup / readiness / liveness / shutdown / drain 的責任分類，再按 5.1 → 5.2 → 5.3 → 5.4 進入平台實作層。5.5（威脅建模）和 5.7（boundary 分類）適合讀完 5.1-5.4 後做概念整理。5.8（實作示範）是 5.2 + 5.3 的操作化，適合最後讀。</p>
<h2 id="選型入口">選型入口</h2>
<p>部署平台選型的核心判斷是服務如何被啟動、更新、接流量、擴容與停止。當問題集中在 container image、rolling update、health check、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a>、<a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry</a>、<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a> 或 <a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a> 時，應先評估部署平台能力。</p>
<p>Container 解決服務包裝與 runtime 依賴；Kubernetes 解決多 instance 調度、<a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>、rolling update 與 <a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">resource limit</a>；systemd 適合單機或 VM 上的 service lifecycle；<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 解決流量入口、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>、<a href="/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout</a> 與 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a>；<a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry</a> 解決實例狀態維護；<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a> 解決服務彼此如何找到 <a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a>；<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a> 解決環境差異、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 與 <a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag</a>。</p>
<p>接近真實網路服務的例子包括發版時 request 失敗、pod 尚未 ready 就接流量、長連線 shutdown 清理不完整、服務擴容後 <a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> 更新延遲。這些場景的共同問題是程式與平台合約，因此本模組會先處理生命週期、流量入口與平台訊號。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理程式內的生命週期與訊號。Backend deployment 模組處理 Kubernetes、systemd、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 與 <a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a> 平台如何觸發、解讀與限制這些訊號。</p>
<h2 id="與資安概念層的交接">與資安概念層的交接</h2>
<p>本模組承接 07 模組的概念判讀，並在服務實體層落地。交接基線如下：</p>
<ul>
<li>來自 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>：承接入口分級、管理平面分離、修補窗口節奏。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a>：承接 TLS/mTLS 與憑證佈署節奏。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>：承接 runtime secret 與機器憑證交付模型。</li>
</ul>
<p>這個交接讓部署模組聚焦實體配置與平台語意，同時保持與資安判讀一致。</p>
<h2 id="案例驅動讀法">案例驅動讀法</h2>
<p>部署平台案例的核心讀法是先確認切換單位（服務、流量、叢集），再定義可回退邊界。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>先看章節</th>
          <th>回寫目標</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift：self-managed K8s -&gt; EKS</a></td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a>、<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3</a></td>
          <td>把零停機遷移拆成分批切流策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast：平台整併</a></td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
          <td>把多叢集治理收斂成單一控制面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera：managed K8s migration</a></td>
          <td><a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1</a>、<a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4</a></td>
          <td>把平台重置與服務連續性目標綁定</td>
      </tr>
  </tbody>
</table>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>部署平台使用方式會受語言的啟動時間、process model、signal handling、thread/task lifecycle、runtime memory behavior 與 liveness 支援影響。啟動慢的 runtime 要調整 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 與 rollout 節奏；長連線或背景 worker 要支援 <a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>；使用 GC 的 runtime 要觀察 memory limit 與 pause 行為；多 process 模型要確認 signal、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 與 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 如何聚合。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1</a></td>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a> 與 runtime</td>
          <td>規劃 image、資源限制與啟動行為</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
          <td>Kubernetes 部署策略</td>
          <td>了解 deployment、<a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>、rolling update</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3</a></td>
          <td><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">Load Balancer Contract</a></td>
          <td>處理 <a href="/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 與 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4</a></td>
          <td><a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a></td>
          <td>讓服務能穩定註冊與發現彼此</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5</a></td>
          <td>平台與入口威脅建模（Threat Modeling）</td>
          <td>用隱藏入口、設定漂移與切換風險盤點交付平台</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6</a></td>
          <td>Platform Lifecycle Contract</td>
          <td>分辨 startup、readiness、liveness、shutdown 與 drain 的責任</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7</a></td>
          <td>Traffic、Config 與 Control Plane Boundary</td>
          <td>拆分流量、設定、secret、service discovery 與管理面邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8</a></td>
          <td>Deployment Rollout with Drain and Rollback 實作示範</td>
          <td>以 checkout service 示範 canary evidence、drain signal 與 rollback decision</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9</a></td>
          <td>邊緣分發與靜態資源（CDN / Origin Protection）</td>
          <td>把 CDN 視為網路入口層，理解三層快取分工、origin protection、purge 操作模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10</a></td>
          <td>Outbound Tunnel 入口與生命週期（cloudflared / Tailscale）</td>
          <td>把反向隧道視為一種入口形態、理解就緒對齊、network 層故障與認證疊法</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/" data-link-title="模組五案例正文" data-link-desc="部署平台轉換案例入口。">5.C</a></td>
          <td>轉換案例正文</td>
          <td>把平台遷移、整併與流量切換做成可回寫案例</td>
      </tr>
  </tbody>
</table>
<p>反例與規模對照入口： <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> / <a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照</a>。</p>
<p>回退判讀寫法見 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/#%e5%9b%9e%e9%80%80%e5%88%a4%e8%ae%80%e5%af%ab%e6%b3%95" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 回退判讀寫法</a>，部署案例要優先保留切流批次、draining、連線生命週期與回退時間。</p>
<h2 id="觀念網路補完方向">觀念網路補完方向</h2>
<p>部署平台章節下一輪的核心責任是把平台能力寫成服務契約。現有章節已經有 container、Kubernetes、load balancer 與 service discovery，但還需要補上 runtime contract、lifecycle contract、traffic contract、rollout contract 與 control-plane contract 的關係，讓讀者知道部署是一組流量、連線、設定、資源與回退條件的連續切換。</p>
<table>
  <thead>
      <tr>
          <th>補完方向</th>
          <th>需要回答的問題</th>
          <th>主要路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Runtime contract</td>
          <td>image、entrypoint、runtime config 與 resource limit 是否可預期</td>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a>、<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">runtime config</a></td>
      </tr>
      <tr>
          <td>Lifecycle contract</td>
          <td>startup、readiness、liveness、shutdown 與 drain 是否對齊</td>
          <td><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a></td>
      </tr>
      <tr>
          <td>Traffic contract</td>
          <td>load balancer、timeout、sticky session 與 routing 是否有明確邊界</td>
          <td><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">load balancer contract</a>、<a href="/blog/backend/knowledge-cards/request-routing/" data-link-title="Request Routing" data-link-desc="說明入口流量如何依規則被導向不同服務或處理路徑">request routing</a></td>
      </tr>
      <tr>
          <td>Rollout contract</td>
          <td>canary、rolling update、config rollout 與 rollback 是否可分批</td>
          <td><a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>Control-plane contract</td>
          <td>service discovery、secret delivery 與管理面是否被保護</td>
          <td><a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane</a>、<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3</a></td>
      </tr>
  </tbody>
</table>
<p>這些方向要用部署平台自己的服務壓力展開。短 request API、長連線服務、背景 worker、<a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> config push 與多租戶平台的生命週期不同，寫作時要分別處理它們的 rollout 與 drain 條件。</p>
<h2 id="知識卡補強方向">知識卡補強方向</h2>
<p>部署模組的 knowledge card 缺口集中在「平台契約」與「切換完成訊號」。已有 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>、<a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout</a> 與 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a> 可以作為第一批錨點。</p>
<p>下一批候選卡片包括 startup probe、drain completion、rollout batch、<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a>、config freeze、environment protection 與 deployment contract。這些卡片要讓讀者能分辨「服務已啟動」和「服務可安全接流量」分屬不同責任。</p>
<h2 id="實作探討入口">實作探討入口</h2>
<p>部署平台的第一條實作路徑是 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout with Drain and Rollback（實作示範）</a>。這篇以 checkout service rollout 為例，說明 rollout plan、canary evidence、drain signal、rollback condition 與 incident decision route 如何一起成立。</p>
<p>這條路徑的前置引用應該是 5.2 Kubernetes deployment、5.3 load balancer contract、<a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。完成後可依 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 進入下一條服務路徑。</p>
<p>部署路徑的 artifact 對齊重點是「每一批切換都能被觀測、被放行、被回退」。對 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 要交 <code>Source/Time range/Query link/Owner/Data quality</code>，並覆蓋 per-version error rate、latency、drain completion 與 reconnect 訊號；對 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 要交 <code>Gate decision/Checks/Stop condition/Rollback window/Owner</code>，呈現 canary 批次與停損規則；對 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 要交 <code>Timestamp/Decision/Context/Evidence/Owner/Expected effect/Rollback condition</code>，記錄 freeze、回退與重啟切流的決策條件與時間序列。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">infra 模組五：核心服務上 IaC</a>：ECS / EKS 的 IaC 描述（subnet 接線、IAM task role、映像版本解耦）是部署平台的地基層</li>
</ul>
]]></content:encoded></item><item><title>6.5 跨進 production 的 routing 中樞</title><link>https://tarrragon.github.io/blog/llm/06-security/routing-to-production-security/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/06-security/routing-to-production-security/</guid><description>&lt;p>模組六前五章建立了個人 dev 視角的 LLM 安全判讀（&lt;a href="https://tarrragon.github.io/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0 供應鏈&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/inference-server-binding/" data-link-title="6.1 推論伺服器的綁定與暴露範圍" data-link-desc="個人 dev 場景下 llama-server / Ollama / LM Studio 的 bind address 判讀：127.0.0.1 vs LAN vs 反代、預設安全、誤開放給內網的後果">6.1 伺服器綁定&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">6.2 tool use 權限&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3 prompt injection&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4 跨雲端資料邊界&lt;/a>）、framing 的根基是 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理&lt;/a>。當工作流從個人 dev 跨進團隊共用、再跨進 production 服務時、安全議題的 framing 跟控制機制都會升級。升級的軸對應 backend 既有卡片：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">attack-surface&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast-radius&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">trust-boundary&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant-boundary&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">iam&lt;/a> 等。本章是這兩個跨越的 routing 中樞、把每個議題在 production 場景下的對應位置（backend/07 對應卡片）整理出來、避免讀者在升級階段「不知道下一步該讀什麼」。&lt;/p>
&lt;p>讀完本章後、你應該能判讀自己當前處在三層哪一階、要跨到下一階時需要補哪些議題、對應到 backend/07 哪些卡片。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;ol>
&lt;li>區分個人 dev、團隊共用、production 三層 LLM 部署的安全議題差異。&lt;/li>
&lt;li>知道從個人 dev 跨到團隊共用時、需要補哪些控制。&lt;/li>
&lt;li>知道從團隊共用跨到 production 時、需要補哪些控制。&lt;/li>
&lt;li>認識每層演化對應的 backend/07 卡片清單。&lt;/li>
&lt;li>知道何時該停留在當前層、何時該主動升級。&lt;/li>
&lt;/ol>
&lt;h2 id="三層演化的判讀軸">三層演化的判讀軸&lt;/h2>





&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">個人 dev（本模組前五章）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓
&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>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">production 服務（對外服務 / SaaS / B2B）&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>個人 dev&lt;/th>
 &lt;th>團隊共用&lt;/th>
 &lt;th>production 服務&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>使用者數&lt;/td>
 &lt;td>1&lt;/td>
 &lt;td>5 ~ 50&lt;/td>
 &lt;td>50+ / 對外不限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>信任假設&lt;/td>
 &lt;td>自己信自己&lt;/td>
 &lt;td>同事互信、訪客不信&lt;/td>
 &lt;td>全部不信、用 IAM 控制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料邊界&lt;/td>
 &lt;td>本機 user account&lt;/td>
 &lt;td>內網&lt;/td>
 &lt;td>多租戶、明確隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失誤後果&lt;/td>
 &lt;td>自己承擔&lt;/td>
 &lt;td>影響少數同事&lt;/td>
 &lt;td>影響大量用戶 / 法律責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>控制機制需求&lt;/td>
 &lt;td>基本配置 + git track&lt;/td>
 &lt;td>+ auth + log + 政策&lt;/td>
 &lt;td>+ IAM + audit + IR + 合規&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對應的時間 / 預算&lt;/td>
 &lt;td>小時級&lt;/td>
 &lt;td>天級&lt;/td>
 &lt;td>週 / 月級、需要專人或團隊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵原則：&lt;strong>控制機制應該跟需求對齊、不該過度設計也不該不足&lt;/strong>。個人 dev 不需要 SOC 2 audit、production 不能只靠 git track。&lt;/p></description><content:encoded><![CDATA[<p>模組六前五章建立了個人 dev 視角的 LLM 安全判讀（<a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0 供應鏈</a>、<a href="/blog/llm/06-security/inference-server-binding/" data-link-title="6.1 推論伺服器的綁定與暴露範圍" data-link-desc="個人 dev 場景下 llama-server / Ollama / LM Studio 的 bind address 判讀：127.0.0.1 vs LAN vs 反代、預設安全、誤開放給內網的後果">6.1 伺服器綁定</a>、<a href="/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">6.2 tool use 權限</a>、<a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3 prompt injection</a>、<a href="/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4 跨雲端資料邊界</a>）、framing 的根基是 <a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理</a>。當工作流從個人 dev 跨進團隊共用、再跨進 production 服務時、安全議題的 framing 跟控制機制都會升級。升級的軸對應 backend 既有卡片：<a href="/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">attack-surface</a>、<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast-radius</a>、<a href="/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">trust-boundary</a>、<a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant-boundary</a>、<a href="/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">iam</a> 等。本章是這兩個跨越的 routing 中樞、把每個議題在 production 場景下的對應位置（backend/07 對應卡片）整理出來、避免讀者在升級階段「不知道下一步該讀什麼」。</p>
<p>讀完本章後、你應該能判讀自己當前處在三層哪一階、要跨到下一階時需要補哪些議題、對應到 backend/07 哪些卡片。</p>
<h2 id="本章目標">本章目標</h2>
<ol>
<li>區分個人 dev、團隊共用、production 三層 LLM 部署的安全議題差異。</li>
<li>知道從個人 dev 跨到團隊共用時、需要補哪些控制。</li>
<li>知道從團隊共用跨到 production 時、需要補哪些控制。</li>
<li>認識每層演化對應的 backend/07 卡片清單。</li>
<li>知道何時該停留在當前層、何時該主動升級。</li>
</ol>
<h2 id="三層演化的判讀軸">三層演化的判讀軸</h2>





<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">個人 dev（本模組前五章）
</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">團隊共用（家裡 / 小團隊 / 內部部署）
</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">production 服務（對外服務 / SaaS / B2B）</span></span></code></pre></div><p>三層的核心差異：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>個人 dev</th>
          <th>團隊共用</th>
          <th>production 服務</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者數</td>
          <td>1</td>
          <td>5 ~ 50</td>
          <td>50+ / 對外不限</td>
      </tr>
      <tr>
          <td>信任假設</td>
          <td>自己信自己</td>
          <td>同事互信、訪客不信</td>
          <td>全部不信、用 IAM 控制</td>
      </tr>
      <tr>
          <td>資料邊界</td>
          <td>本機 user account</td>
          <td>內網</td>
          <td>多租戶、明確隔離</td>
      </tr>
      <tr>
          <td>失誤後果</td>
          <td>自己承擔</td>
          <td>影響少數同事</td>
          <td>影響大量用戶 / 法律責任</td>
      </tr>
      <tr>
          <td>控制機制需求</td>
          <td>基本配置 + git track</td>
          <td>+ auth + log + 政策</td>
          <td>+ IAM + audit + IR + 合規</td>
      </tr>
      <tr>
          <td>對應的時間 / 預算</td>
          <td>小時級</td>
          <td>天級</td>
          <td>週 / 月級、需要專人或團隊</td>
      </tr>
  </tbody>
</table>
<p>關鍵原則：<strong>控制機制應該跟需求對齊、不該過度設計也不該不足</strong>。個人 dev 不需要 SOC 2 audit、production 不能只靠 git track。</p>
<h2 id="個人-dev--團隊共用要補什麼">個人 dev → 團隊共用：要補什麼</h2>
<p>從個人 dev 跨到團隊共用、典型的觸發場景：</p>
<ol>
<li>家裡跑模型給家人 / 室友用</li>
<li>小團隊共用一台 LLM server</li>
<li>公司內部部署、有 5 ~ 50 個工程師用</li>
</ol>
<p>需要補的控制（在前五章的基礎上）：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>從個人 dev 的什麼演化而來</th>
          <th>對應的補強</th>
          <th>backend/07 對應卡片</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>身份識別</td>
          <td>自己一人 → 多人共用</td>
          <td>加 auth、知道誰送了什麼 prompt</td>
          <td><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">identity-access-boundary</a></td>
      </tr>
      <tr>
          <td>入口治理</td>
          <td>bind 到 LAN 加 API key</td>
          <td>反代 + TLS + rate limit</td>
          <td><a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">entrypoint-and-server-protection</a></td>
      </tr>
      <tr>
          <td>傳輸信任</td>
          <td>內網 HTTP 偶爾 OK</td>
          <td>內網全程 HTTPS、TLS 憑證管理</td>
          <td><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">transport-trust-and-certificate-lifecycle</a></td>
      </tr>
      <tr>
          <td>秘密管理</td>
          <td>dotfile 環境變數</td>
          <td>集中 secret store（Vault / SSM / Doppler）</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">secrets-and-machine-credential-governance</a></td>
      </tr>
      <tr>
          <td>供應鏈</td>
          <td>自己抓 GGUF / npm package（見 <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0</a>）</td>
          <td>內部 mirror、固定 version、定期 audit</td>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">supply-chain-integrity-and-artifact-trust</a></td>
      </tr>
      <tr>
          <td>政策</td>
          <td>自己腦中的判讀</td>
          <td>寫明 acceptable use、敏感內容指引</td>
          <td>（結合各章的政策性章節）</td>
      </tr>
  </tbody>
</table>
<p>團隊共用階段的常見 anti-pattern：</p>
<ol>
<li><strong>把個人 dev 的 dotfile config 直接複製到團隊 server</strong>：API key、log 路徑、reset 機制都不對。</li>
<li><strong>依賴單一管理員口頭傳遞政策</strong>：沒寫下來、新成員不知道、人離職就失傳。</li>
<li><strong>跳過 auth 直接用「公司內網本來就安全」當理由</strong>：內網設備有訪客、有實習生、有 BYOD、有合作廠商；零信任的最低版本仍要做。</li>
</ol>
<h2 id="團隊共用--production要補什麼">團隊共用 → production：要補什麼</h2>
<p>從團隊共用跨到 production 服務、典型的觸發場景：</p>
<ol>
<li>把內部 LLM 服務開放給外部客戶（B2B）</li>
<li>做 SaaS-like LLM API 對外賣</li>
<li>把 LLM 嵌入產品給終端用戶用</li>
</ol>
<p>需要補的控制（在前面兩層的基礎上）：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>從團隊共用的什麼演化而來</th>
          <th>對應的補強</th>
          <th>backend/07 對應卡片</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多租戶隔離</td>
          <td>共用 server 跨同事 → 跨用戶</td>
          <td>KV cache / log / model 訪問權的多租戶隔離</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/" data-link-title="LLM 多租戶推論隔離" data-link-desc="production LLM 服務的多租戶隔離：KV cache 不共享、log / model artifact 隔離、跨用戶 prompt 洩漏面">llm-multi-tenant-isolation</a></td>
      </tr>
      <tr>
          <td>deployment 供應鏈</td>
          <td>內部 mirror → 對外責任</td>
          <td>模型 release 流程、簽章、回退機制</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-deployment-supply-chain/" data-link-title="LLM Deployment 供應鏈完整性" data-link-desc="把 LLM 模型權重、推論伺服器、第三方 plugin 三條 production 供應鏈納入既有 artifact trust 框架的判讀">llm-deployment-supply-chain</a></td>
      </tr>
      <tr>
          <td>agent prompt injection 後果</td>
          <td>IDE injection（<a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3</a>）→ agent 場景（<a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4</a>）</td>
          <td>tool spec 設計、限制 agent loop、人為 review checkpoint</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-prompt-injection-in-agent/" data-link-title="LLM Agent Prompt Injection 後果治理" data-link-desc="production LLM agent 場景的 prompt injection 後果：tool spec 設計、agent loop 限制、review checkpoint、跟 incident workflow 的接合">llm-prompt-injection-in-agent</a></td>
      </tr>
      <tr>
          <td>log / PII 治理</td>
          <td>簡單 access log → 完整 prompt log</td>
          <td>log 累積的 prompt 內容、PII 偵測與過濾、保留期限</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-log-and-pii-governance/" data-link-title="LLM Log 與 PII 治理" data-link-desc="production LLM 服務的 prompt log 累積、PII 偵測與過濾、保留期限與合規對齊">llm-log-and-pii-governance</a></td>
      </tr>
      <tr>
          <td>偵測訊號</td>
          <td>看 log → 主動偵測</td>
          <td>LLM agent 異常行為的訊號設計、tool use 異常模式</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-as-service-detection-coverage/" data-link-title="LLM Service 偵測訊號覆蓋" data-link-desc="production LLM 服務的 detection 訊號設計：tool call 異常模式、prompt injection 觸發徵兆、abuse 跟濫用模式、跟既有 detection-coverage 框架的接合">llm-as-service-detection-coverage</a></td>
      </tr>
      <tr>
          <td>Workload Identity</td>
          <td>server 自己持 API key → workload IAM</td>
          <td>每個 workload 一個身份、可 audit</td>
          <td><a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">workload-identity-and-federated-trust</a></td>
      </tr>
      <tr>
          <td>偵測平台</td>
          <td>手動觀察 → SIEM</td>
          <td>集中偵測、alert 系統</td>
          <td><a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">detection-coverage-and-signal-governance</a></td>
      </tr>
      <tr>
          <td>Incident response</td>
          <td>重啟解決 → IR 流程</td>
          <td>IR 演練、escalation、post-mortem</td>
          <td><a href="/blog/backend/07-security-data-protection/incident-case-to-control-workflow/" data-link-title="7.16 從公開事故到工程 Workflow：案例如何回寫控制面" data-link-desc="建立公開事故如何轉成控制面失效樣式與 workflow 回寫的大綱">incident-case-to-control-workflow</a></td>
      </tr>
      <tr>
          <td>合規</td>
          <td>不需要 → 對外服務需要</td>
          <td>GDPR / HIPAA / SOC 2 等</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">data-protection-and-masking-governance</a></td>
      </tr>
  </tbody>
</table>
<p>production 階段不是「把團隊共用放大」、是「另一個複雜度等級」。多數議題從 backend/07 既有卡片開始讀、LLM-specific 議題在 backend/07 的 LLM 相關章節（<code>llm-*.md</code>）補充。</p>
<h2 id="何時該停留在當前層">何時該停留在當前層</h2>
<p>不是所有工作流都需要升級。停留在當前層的合理判讀：</p>
<table>
  <thead>
      <tr>
          <th>當前層</th>
          <th>該停留的徵兆</th>
          <th>升級的徵兆</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>個人 dev</td>
          <td>只有自己用、不分享、沒對外暴露需求</td>
          <td>開始有人想連你的 server / 想做 demo 給朋友 / 想分享給家人</td>
      </tr>
      <tr>
          <td>團隊共用</td>
          <td>5 ~ 50 人的內部使用、不對外賣、不涉及客戶 PII</td>
          <td>客戶要連 / 對外 SLA / 要收費 / 開始涉及客戶 PII</td>
      </tr>
      <tr>
          <td>production</td>
          <td>已對外服務、有 SLA、有客戶</td>
          <td>（目標狀態）</td>
      </tr>
  </tbody>
</table>
<p>升級的兩個常見錯誤：</p>
<ol>
<li><strong>過早升級</strong>：個人 dev 階段就上 enterprise stack（IAM、Vault、SIEM）、複雜度過高、自己用不到、維護成本反而傷工作流。</li>
<li><strong>過晚升級</strong>：團隊共用階段該補的控制沒補、出事才補、可能已經有資料外洩 / 法律責任。</li>
</ol>
<p>判讀依據：<strong>控制機制對齊實際 threat model 跟 user 規模</strong>、不是「越多越好」。</p>
<h2 id="跨層升級的常見-anti-pattern">跨層升級的常見 anti-pattern</h2>
<p>從各層往上跨時、常見的意外：</p>
<ol>
<li><strong>把個人 dev 的 LLM client config 直接放上 production</strong>：autocomplete model、default model、API key 都不對；production 場景需要重新設計 model 路由。</li>
<li><strong>把個人習慣的 prompt injection 防護當 production 防護</strong>：「我 git track 工作流」對個人 dev 夠、production agent 場景下、git 不在迴路裡、要改用 tool spec + review checkpoint。</li>
<li><strong>production 場景仍然依賴使用者「看 prompt 內容」</strong>：使用者數量大、不可能每個 prompt 都人工看；production 需要自動化偵測訊號。</li>
<li><strong>production 場景沒 tenant 隔離</strong>：所有用戶的 KV cache / log / context 混在一起、A 用戶能看到 B 用戶的 cache hit。</li>
<li><strong>沒有 vendor 政策的書面化承諾</strong>：team 階段口頭講「我們不訓練客戶資料」、production 階段要寫進條款 / SLA。</li>
</ol>
<h2 id="給讀者的層級判讀清單">給讀者的層級判讀清單</h2>
<p>判斷自己當前在哪一層：</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">[ ] 只有自己用                                              → 個人 dev
</span></span><span class="line"><span class="ln">2</span><span class="cl">[ ] 1 ~ 5 個人共用一台 server                                → 個人 dev 或團隊共用初期
</span></span><span class="line"><span class="ln">3</span><span class="cl">[ ] 5 ~ 50 個人共用、內部部署                                → 團隊共用
</span></span><span class="line"><span class="ln">4</span><span class="cl">[ ] 對外提供 API 服務 / SaaS                                 → production
</span></span><span class="line"><span class="ln">5</span><span class="cl">[ ] 服務多個客戶 / 涉及客戶 PII                              → production
</span></span><span class="line"><span class="ln">6</span><span class="cl">[ ] 有 SLA / 合約承諾                                        → production</span></span></code></pre></div><p>對應的「要補的議題」：</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">個人 dev → 團隊共用：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  [ ] auth                  ← backend/07 identity-access-boundary
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  [ ] 入口治理               ← backend/07 entrypoint-and-server-protection
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  [ ] TLS                    ← backend/07 transport-trust-and-certificate-lifecycle
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  [ ] secret 集中管理        ← backend/07 secrets-and-machine-credential-governance
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  [ ] 內部 supply chain      ← backend/07 supply-chain-integrity-and-artifact-trust
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  [ ] 寫下 acceptable use 政策
</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">團隊共用 → production：
</span></span><span class="line"><span class="ln">10</span><span class="cl">  [ ] 多租戶 isolation       ← backend/07 llm-multi-tenant-isolation
</span></span><span class="line"><span class="ln">11</span><span class="cl">  [ ] deployment 供應鏈      ← backend/07 llm-deployment-supply-chain
</span></span><span class="line"><span class="ln">12</span><span class="cl">  [ ] agent prompt injection ← backend/07 llm-prompt-injection-in-agent
</span></span><span class="line"><span class="ln">13</span><span class="cl">  [ ] log / PII 治理         ← backend/07 llm-log-and-pii-governance
</span></span><span class="line"><span class="ln">14</span><span class="cl">  [ ] 偵測訊號               ← backend/07 llm-as-service-detection-coverage
</span></span><span class="line"><span class="ln">15</span><span class="cl">  [ ] workload identity      ← backend/07 workload-identity-and-federated-trust
</span></span><span class="line"><span class="ln">16</span><span class="cl">  [ ] 偵測平台               ← backend/07 detection-coverage-and-signal-governance
</span></span><span class="line"><span class="ln">17</span><span class="cl">  [ ] IR 流程                ← backend/07 incident-case-to-control-workflow
</span></span><span class="line"><span class="ln">18</span><span class="cl">  [ ] 合規                   ← backend/07 data-protection-and-masking-governance</span></span></code></pre></div><h2 id="下一步">下一步</h2>
<p>本章是模組六的最後一章。下一步可以回到 <a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六 _index</a> 看其他章節、或進入 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">Backend 模組七 資安與資料保護</a> 接 production 場景。</p>
]]></content:encoded></item><item><title>5.6 Platform Lifecycle Contract</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/</guid><description>&lt;p>Platform lifecycle contract 的核心責任是讓服務和部署平台對同一組生命週期訊號有共同解讀。進入 Kubernetes、systemd、Docker、ELB 或 Envoy 前，讀者需要先理解「服務啟動」和「服務可接流量」是不同狀態。&lt;/p>
&lt;h2 id="lifecycle-contract">Lifecycle Contract&lt;/h2>
&lt;p>Lifecycle contract 定義平台如何啟動、檢查、接流量、停止與回收服務實例。它包含 runtime、startup、readiness、liveness、shutdown 與 drain。&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>runtime&lt;/td>
 &lt;td>固定 image、entrypoint、config 與 resource&lt;/td>
 &lt;td>提供可預期執行環境&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>startup&lt;/td>
 &lt;td>初始化依賴與內部狀態&lt;/td>
 &lt;td>避免過早重啟慢啟動服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>readiness&lt;/td>
 &lt;td>宣告可安全接流量&lt;/td>
 &lt;td>只把流量導向 ready instance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>liveness&lt;/td>
 &lt;td>宣告基本運作能力&lt;/td>
 &lt;td>在不可恢復時重建 instance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>shutdown&lt;/td>
 &lt;td>停接新工作並釋放資源&lt;/td>
 &lt;td>給予 termination window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>drain&lt;/td>
 &lt;td>完成在途請求或連線退場&lt;/td>
 &lt;td>從路由集合摘除 instance&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些狀態分開後，部署事故才能定位是啟動、接流量、退場還是平台判讀問題。&lt;/p>
&lt;p>runtime 與 startup 決定服務能否形成可運行實例。readiness 與 liveness 決定平台何時導入流量與何時重建實例。shutdown 與 drain 決定版本退場時是否能保護在途工作。這些狀態都屬於生命週期合約，卻對應不同的事故處理路徑。&lt;/p>
&lt;h2 id="startup-與-readiness">Startup 與 Readiness&lt;/h2>
&lt;p>startup 的責任是確認服務初始化完成。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 的責任是確認服務可承接實際流量。啟動完成不代表依賴已就緒，也不代表背景任務、config、secret 或 connection pool 都可用。&lt;/p>
&lt;p>慢啟動服務需要 startup gate，避免 liveness 在初始化期間反覆重啟。依賴敏感服務需要 readiness gate，避免尚未連上資料庫、cache 或 queue 時就接收請求。&lt;/p>
&lt;h3 id="啟動時間的組成與壓縮">啟動時間的組成與壓縮&lt;/h3>
&lt;p>服務啟動時間的長短決定 rollout 節奏的下限。啟動時間由四段組成，每段有不同壓縮策略：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>runtime 初始化&lt;/strong>：語言 VM、GC 初始化、class loading（JVM warmup 可達 10-30 秒）。壓縮手段是 ahead-of-time compilation（GraalVM native image、Go 靜態編譯啟動速度快）或 CDS（Class Data Sharing）。&lt;/li>
&lt;li>&lt;strong>依賴建立&lt;/strong>：資料庫連線池、cache 連線、queue consumer 註冊。壓縮手段是 lazy initialization（按需建立）或 connection pool pre-warming（啟動時建好但不阻擋 readiness）。&lt;/li>
&lt;li>&lt;strong>資料預載&lt;/strong>：config 同步、feature flag 初始拉取、本地快取預熱。壓縮手段是區分必要載入與非必要載入——必要的阻擋 readiness，非必要的平行載入。&lt;/li>
&lt;li>&lt;strong>就緒驗證&lt;/strong>：自我健康檢查、依賴可達性驗證。壓縮手段是平行驗證多個依賴，避免串行等待。&lt;/li>
&lt;/ol>
&lt;p>啟動時間超過平台預設 startup timeout 時，先拆成這四段分析瓶頸，再決定調大 timeout 還是壓縮啟動流程。盲目調大 timeout 會掩蓋啟動退化問題，讓單次 rollout 的最短觀察窗拉長。&lt;/p>
&lt;h3 id="readiness-設計的核心取捨">Readiness 設計的核心取捨&lt;/h3>
&lt;p>readiness 太鬆（只檢查 HTTP port 是否可達）會讓尚未就緒的實例接到流量。readiness 太緊（檢查所有下游可達性）會讓非自身問題的下游故障觸發連鎖 not-ready，放大故障面。&lt;/p>
&lt;p>取捨的判讀框架是「這個依賴不可用時，服務是否仍能提供有意義的回應」：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>必要依賴&lt;/strong>：資料庫、auth service——不可用時服務完全無法處理請求。這類依賴的可達性應納入 readiness 條件。&lt;/li>
&lt;li>&lt;strong>可降級依賴&lt;/strong>：推薦引擎、非關鍵 cache——不可用時服務可回傳降級結果。這類依賴不應納入 readiness，改用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">circuit breaker&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a> 處理。&lt;/li>
&lt;li>&lt;strong>觀測依賴&lt;/strong>：metrics collector、log shipper——不可用不影響業務流量。這類依賴進 readiness 是常見誤判，會讓觀測基礎設施故障擊倒整個服務。&lt;/li>
&lt;/ul>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration&lt;/a>：揭露「跨平台遷移本質是能力遷移、部署 / 觀測 / 恢復與團隊流程都需要同步重建」。遷移到新平台時，舊平台的 readiness 條件不能直接搬——新平台的依賴可達路徑、DNS 解析速度、secret 注入方式可能改變，readiness 條件要重新驗證。&lt;/p>
&lt;h2 id="liveness-與-restart">Liveness 與 Restart&lt;/h2>
&lt;p>liveness 的責任是偵測無法自我恢復的狀態。短暫下游故障適合交給 readiness、circuit breaker 或 fallback 處理，否則平台會用重啟放大故障。&lt;/p>
&lt;p>liveness 太敏感會造成 restart loop；liveness 太寬鬆會讓壞實例長期留在線上。設計時要先定義哪些錯誤可由服務內部恢復，哪些才需要平台重建。&lt;/p>
&lt;h3 id="liveness-適合偵測的失敗模式">Liveness 適合偵測的失敗模式&lt;/h3>
&lt;p>liveness 的工程價值在於捕捉服務自己無法修復的狀態。把 liveness 當成通用健康檢查是過度使用，會讓正常的瞬態故障觸發不必要的重建。&lt;/p></description><content:encoded><![CDATA[<p>Platform lifecycle contract 的核心責任是讓服務和部署平台對同一組生命週期訊號有共同解讀。進入 Kubernetes、systemd、Docker、ELB 或 Envoy 前，讀者需要先理解「服務啟動」和「服務可接流量」是不同狀態。</p>
<h2 id="lifecycle-contract">Lifecycle Contract</h2>
<p>Lifecycle contract 定義平台如何啟動、檢查、接流量、停止與回收服務實例。它包含 runtime、startup、readiness、liveness、shutdown 與 drain。</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>服務責任</th>
          <th>平台責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>runtime</td>
          <td>固定 image、entrypoint、config 與 resource</td>
          <td>提供可預期執行環境</td>
      </tr>
      <tr>
          <td>startup</td>
          <td>初始化依賴與內部狀態</td>
          <td>避免過早重啟慢啟動服務</td>
      </tr>
      <tr>
          <td>readiness</td>
          <td>宣告可安全接流量</td>
          <td>只把流量導向 ready instance</td>
      </tr>
      <tr>
          <td>liveness</td>
          <td>宣告基本運作能力</td>
          <td>在不可恢復時重建 instance</td>
      </tr>
      <tr>
          <td>shutdown</td>
          <td>停接新工作並釋放資源</td>
          <td>給予 termination window</td>
      </tr>
      <tr>
          <td>drain</td>
          <td>完成在途請求或連線退場</td>
          <td>從路由集合摘除 instance</td>
      </tr>
  </tbody>
</table>
<p>這些狀態分開後，部署事故才能定位是啟動、接流量、退場還是平台判讀問題。</p>
<p>runtime 與 startup 決定服務能否形成可運行實例。readiness 與 liveness 決定平台何時導入流量與何時重建實例。shutdown 與 drain 決定版本退場時是否能保護在途工作。這些狀態都屬於生命週期合約，卻對應不同的事故處理路徑。</p>
<h2 id="startup-與-readiness">Startup 與 Readiness</h2>
<p>startup 的責任是確認服務初始化完成。<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 的責任是確認服務可承接實際流量。啟動完成不代表依賴已就緒，也不代表背景任務、config、secret 或 connection pool 都可用。</p>
<p>慢啟動服務需要 startup gate，避免 liveness 在初始化期間反覆重啟。依賴敏感服務需要 readiness gate，避免尚未連上資料庫、cache 或 queue 時就接收請求。</p>
<h3 id="啟動時間的組成與壓縮">啟動時間的組成與壓縮</h3>
<p>服務啟動時間的長短決定 rollout 節奏的下限。啟動時間由四段組成，每段有不同壓縮策略：</p>
<ol>
<li><strong>runtime 初始化</strong>：語言 VM、GC 初始化、class loading（JVM warmup 可達 10-30 秒）。壓縮手段是 ahead-of-time compilation（GraalVM native image、Go 靜態編譯啟動速度快）或 CDS（Class Data Sharing）。</li>
<li><strong>依賴建立</strong>：資料庫連線池、cache 連線、queue consumer 註冊。壓縮手段是 lazy initialization（按需建立）或 connection pool pre-warming（啟動時建好但不阻擋 readiness）。</li>
<li><strong>資料預載</strong>：config 同步、feature flag 初始拉取、本地快取預熱。壓縮手段是區分必要載入與非必要載入——必要的阻擋 readiness，非必要的平行載入。</li>
<li><strong>就緒驗證</strong>：自我健康檢查、依賴可達性驗證。壓縮手段是平行驗證多個依賴，避免串行等待。</li>
</ol>
<p>啟動時間超過平台預設 startup timeout 時，先拆成這四段分析瓶頸，再決定調大 timeout 還是壓縮啟動流程。盲目調大 timeout 會掩蓋啟動退化問題，讓單次 rollout 的最短觀察窗拉長。</p>
<h3 id="readiness-設計的核心取捨">Readiness 設計的核心取捨</h3>
<p>readiness 太鬆（只檢查 HTTP port 是否可達）會讓尚未就緒的實例接到流量。readiness 太緊（檢查所有下游可達性）會讓非自身問題的下游故障觸發連鎖 not-ready，放大故障面。</p>
<p>取捨的判讀框架是「這個依賴不可用時，服務是否仍能提供有意義的回應」：</p>
<ul>
<li><strong>必要依賴</strong>：資料庫、auth service——不可用時服務完全無法處理請求。這類依賴的可達性應納入 readiness 條件。</li>
<li><strong>可降級依賴</strong>：推薦引擎、非關鍵 cache——不可用時服務可回傳降級結果。這類依賴不應納入 readiness，改用 <a href="/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">circuit breaker</a> 或 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 處理。</li>
<li><strong>觀測依賴</strong>：metrics collector、log shipper——不可用不影響業務流量。這類依賴進 readiness 是常見誤判，會讓觀測基礎設施故障擊倒整個服務。</li>
</ul>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration</a>：揭露「跨平台遷移本質是能力遷移、部署 / 觀測 / 恢復與團隊流程都需要同步重建」。遷移到新平台時，舊平台的 readiness 條件不能直接搬——新平台的依賴可達路徑、DNS 解析速度、secret 注入方式可能改變，readiness 條件要重新驗證。</p>
<h2 id="liveness-與-restart">Liveness 與 Restart</h2>
<p>liveness 的責任是偵測無法自我恢復的狀態。短暫下游故障適合交給 readiness、circuit breaker 或 fallback 處理，否則平台會用重啟放大故障。</p>
<p>liveness 太敏感會造成 restart loop；liveness 太寬鬆會讓壞實例長期留在線上。設計時要先定義哪些錯誤可由服務內部恢復，哪些才需要平台重建。</p>
<h3 id="liveness-適合偵測的失敗模式">Liveness 適合偵測的失敗模式</h3>
<p>liveness 的工程價值在於捕捉服務自己無法修復的狀態。把 liveness 當成通用健康檢查是過度使用，會讓正常的瞬態故障觸發不必要的重建。</p>
<p>適合 liveness 偵測的狀態：</p>
<ul>
<li><strong>deadlock</strong>：所有 worker thread 被卡住，無法處理新請求也無法回傳錯誤。liveness endpoint 設在獨立 goroutine / thread 上，如果 worker pool 卡住但 liveness goroutine 能回應，問題在業務邏輯而非 deadlock。</li>
<li><strong>memory leak 導致的 OOM 前兆</strong>：記憶體使用率持續上升不回落，GC 已無法回收。此時主動回報 unhealthy 讓平台在 OOM kill 前重建，比被動等 OOM 更可控——OOM kill 不走 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>，在途請求直接中斷。</li>
<li><strong>essential background task 永久停止</strong>：必要的定期任務（如 license renewal、session cleanup）超過預期間隔仍未執行。這類失敗靜默發生，只有 liveness 主動偵測能發現。</li>
</ul>
<p>不適合 liveness 偵測的狀態：下游資料庫短暫不可用、外部 API timeout、cache miss 率升高。這些由 readiness 或 circuit breaker 處理——用 liveness 重建不會修好下游，只會用重啟放大問題。</p>
<h3 id="restart-的代價量化">Restart 的代價量化</h3>
<p>每次 liveness 觸發的重啟會產生四類代價：</p>
<ol>
<li><strong>在途請求中斷</strong>：被重啟的實例正在處理的請求直接失敗。</li>
<li><strong>連線重建成本</strong>：資料庫連線池、cache 連線、queue consumer 重新建立。</li>
<li><strong>啟動期間的容量缺口</strong>：重啟到 readiness 通過之間，整體服務容量降低。</li>
<li><strong>thundering herd 風險</strong>：多實例同時被 liveness 判定失敗並重啟時，同時重建連線、同時搶資源、下游壓力瞬間放大。</li>
</ol>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio 升級治理</a>：揭露「基礎平台元件升級若缺乏分批治理、會形成全域風險放大器」。以下基於通用工程知識展開：Istio 等 service mesh 升級期間的 sidecar 重啟可觸發大量服務的 liveness 暫時失敗，若 liveness 太敏感會放大成全域 restart storm。升級期的 liveness 閾值應比穩態更寬鬆，或在升級批次中暫時加大 liveness failure threshold。</p>
<h2 id="shutdown-與-drain">Shutdown 與 Drain</h2>
<p>shutdown 的責任是讓服務停止接新工作並完成資源釋放。<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 的責任是讓平台在移除實例前，讓 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request、長連線或背景工作有時間收束。</p>
<p>短 request API、長連線服務與 background worker 的 drain 條件不同。短 API 主要看在途請求歸零；長連線看 reconnect 節奏；worker 看已領取工作能否完成或重新排隊。tunnel 入口的 startup / readiness / drain 對齊見 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a>。</p>
<h3 id="三種-workload-的-drain-差異">三種 Workload 的 Drain 差異</h3>
<p>不同 workload 類型的 drain 完成條件與時間尺度完全不同，用同一套 drain 設定覆蓋所有 workload 會在至少一類服務上出事。</p>
<p><strong>短 request API</strong>（HTTP REST、gRPC unary）：drain 窗口通常在 5-30 秒。核心條件是在途請求數歸零。風險點是 load balancer 的 deregistration delay——LB 可能在服務已標記 not-ready 後仍送幾秒流量（取決於 health check interval 與 deregistration delay），所以服務端 drain 窗口要覆蓋這段延遲。endpoint 摘除的傳播窗口與 preStop 等待策略見 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 摘除節奏與 Drain 的配合</a>。</p>
<p><strong>長連線服務</strong>（WebSocket、gRPC streaming、SSE）：drain 窗口通常在 30 秒到數分鐘。核心條件是現有連線收斂且 reconnect 波形穩定。風險點是客戶端 reconnect 策略——服務端 drain 完成不代表客戶端已連上新實例。若客戶端沒有 backoff 或 reconnect 目標選擇邏輯，會形成 reconnect storm。drain 設計要跟客戶端 reconnect 策略一起規劃。</p>
<p><strong>Background worker</strong>（queue consumer、定時任務、batch job）：drain 窗口取決於單一工作的最長執行時間。核心條件是已領取的工作完成處理或安全重新排隊。風險點是不可中斷工作——某些 job 做到一半無法重試（例如外部 API 呼叫已發出但回應尚未確認），drain 時序要覆蓋這類 job 的最長完成時間，否則 job 被中斷後產生不一致狀態。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：平台切流未先 Draining</a>：揭露「切流失敗常在 connection lifecycle 管理」「drain / idle timeout / health check / client retry 沒有同一節奏」。反例中的事故擴大機制正是不同 workload 類型的 drain 條件被忽略——短 API 的 drain 完成了，長連線的 reconnect 仍在震盪，worker 的 job 被中斷重試造成重複處理。</p>
<h3 id="shutdown-信號的傳遞路徑">Shutdown 信號的傳遞路徑</h3>
<p>platform 到 application 的 shutdown 信號傳遞有多個可能斷點。信號從平台送到容器 PID 1、PID 1 轉發到應用進程——PID 1 的信號處理語意與常見陷阱見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 PID 1 與信號處理</a>。本段聚焦 lifecycle 層的時序問題：</p>
<ul>
<li><strong>preStop hook 與 SIGTERM 時序</strong>：Kubernetes 先執行 preStop hook、再送 SIGTERM。preStop hook 可用來等 LB 摘流量（sleep 幾秒讓 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">endpoint 從可用集合移除</a>），讓 SIGTERM 到達時在途流量已經減少。</li>
<li><strong>terminationGracePeriodSeconds</strong>：平台等待的最長時間。超過後 SIGKILL 強制結束，不走 graceful shutdown。這個值要覆蓋 preStop + drain + 資源釋放的總時間。</li>
</ul>
<p>shutdown 信號傳遞的驗證方式是在 staging 環境觸發 pod delete，觀察應用 log 中是否出現 shutdown handler 的紀錄。沒看到 shutdown log 代表信號沒傳到、要先修傳遞路徑再談 drain 設計。</p>
<h2 id="不同-workload-的-lifecycle-特性對照">不同 Workload 的 Lifecycle 特性對照</h2>
<p>生命週期合約的參數設定要依 workload 類型調整。以下是三類常見 workload 的特性差異。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>短 request API</th>
          <th>長連線服務</th>
          <th>Background worker</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>startup 關注點</td>
          <td>依賴連線池建立</td>
          <td>依賴連線池 + 監聽埠就緒</td>
          <td>queue consumer 註冊完成</td>
      </tr>
      <tr>
          <td>readiness 條件</td>
          <td>必要依賴可達 + 連線池滿</td>
          <td>必要依賴可達 + 可接受新連線</td>
          <td>consumer 已註冊 + 可拉取新工作</td>
      </tr>
      <tr>
          <td>liveness 偵測</td>
          <td>deadlock、OOM 前兆</td>
          <td>連線管理 thread 存活</td>
          <td>worker loop 存活、queue 輪詢正常</td>
      </tr>
      <tr>
          <td>drain 完成條件</td>
          <td>在途請求數歸零</td>
          <td>現有連線收斂、reconnect 穩</td>
          <td>已領取工作完成或重新排隊</td>
      </tr>
      <tr>
          <td>drain 窗口</td>
          <td>5-30 秒</td>
          <td>30 秒 - 數分鐘</td>
          <td>取決於最長 job 執行時間</td>
      </tr>
      <tr>
          <td>shutdown 風險</td>
          <td>LB 延遲仍送流量</td>
          <td>reconnect storm</td>
          <td>不可中斷 job 被強制結束</td>
      </tr>
      <tr>
          <td>rollout 節奏建議</td>
          <td>可激進（秒級觀察窗）</td>
          <td>保守（分鐘級、等 reconnect）</td>
          <td>依 job 粒度（完成當前批次再切）</td>
      </tr>
  </tbody>
</table>
<p>這張表是選型前判準的操作化：先確認服務屬於哪類 workload，再套用對應的 lifecycle 參數基線。混合 workload（例如同時提供 HTTP API 和 WebSocket）要取各層的嚴格值——drain 窗口取最長的、readiness 取最嚴格的。</p>
<h2 id="平台如何表達-lifecycle-差異">平台如何表達 Lifecycle 差異</h2>
<p>不同部署平台表達生命週期合約的能力不同。選型時要問的是「這個平台能不能分別設定 startup、readiness、liveness 與 drain」。</p>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>startup gate</th>
          <th>readiness 與 liveness 分離</th>
          <th>drain 能力</th>
          <th>termination 窗口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kubernetes</td>
          <td>startupProbe</td>
          <td>readinessProbe / livenessProbe 獨立</td>
          <td>preStop hook + endpoint 摘除</td>
          <td>terminationGracePeriodSeconds</td>
      </tr>
      <tr>
          <td>systemd</td>
          <td>無原生 startup probe</td>
          <td>靠 sd_notify(READY=1)</td>
          <td>ExecStop + KillSignal</td>
          <td>TimeoutStopSec</td>
      </tr>
      <tr>
          <td>Docker</td>
          <td>HEALTHCHECK（不分離）</td>
          <td>單一 HEALTHCHECK</td>
          <td>stop_grace_period</td>
          <td>stop_grace_period</td>
      </tr>
      <tr>
          <td>ECS</td>
          <td>startupHealthCheck</td>
          <td>health check（不分離）</td>
          <td>deregistration delay</td>
          <td>stopTimeout</td>
      </tr>
  </tbody>
</table>
<p>Kubernetes 在 lifecycle 表達力上最完整，但參數最多也最容易配錯。systemd 靠 sd_notify 協議明確宣告 readiness，在單機部署場景下反而比 K8s 的 probe 直接。Docker 和 ECS 不分離 readiness 與 liveness，需要在應用層自行實作降級邏輯。</p>
<p>選平台不只看功能清單，要看它表達 lifecycle 差異的粒度是否覆蓋服務需求。若服務需要分離 startup 和 readiness 但平台只有一個 health check，這個差距要在應用層補——代價是複雜度從平台設定轉移到程式碼。</p>
<h2 id="遷移期的-lifecycle-重新驗證">遷移期的 Lifecycle 重新驗證</h2>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6 Airbnb Kubernetes 叢集擴縮演進</a>：揭露「擴縮策略版本化與可回放」「不同 workload 區分擴縮政策」。以下基於通用工程知識展開：叢集演進過程中，lifecycle 參數的假設會改變——workload 從穩態變成高波動、從單一類型變成混合類型、從小規模變成大規模。lifecycle contract 的參數不是設一次就好，要隨叢集演進重新驗證。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照：規模差異下的平台遷移</a>：揭露「小型組織最容易漏掉回退腳本化」「中型組織依賴錯位、服務切過去但資料面 / 認證面 / 觀測面沒同步」。lifecycle contract 在遷移後的完整性驗證不只看 probe 設定——secret 注入時序、資料庫連線池的 endpoint 是否切到新叢集、observability pipeline 的 readiness 是否對齊，都是 lifecycle 合約的一部分。</p>
<p>遷移後的 lifecycle 驗證清單：</p>
<ol>
<li><strong>startup 時序重測</strong>：新平台的 image pull 時間、secret mount 時間、DNS 解析路徑可能不同，原本的 startup timeout 可能不夠。</li>
<li><strong>readiness 依賴路徑檢查</strong>：readiness 檢查的依賴是否仍可達（新叢集到舊資料庫的 latency 是否增加、跨叢集 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">service discovery</a> 是否對齊、DNS TTL 與快取行為是否改變）。</li>
<li><strong>drain 行為驗證</strong>：在新平台觸發 pod delete、觀察 drain 完成時間與在途請求處理是否符合預期。</li>
<li><strong>信號傳遞驗證</strong>：在新平台觸發 shutdown、確認 SIGTERM 到達應用進程並觸發 graceful shutdown handler。</li>
</ol>
<h2 id="選型前判準">選型前判準</h2>
<p>部署平台選型前要先回答：</p>
<ol>
<li>服務啟動需要多久，哪些依賴是 readiness 條件。</li>
<li>服務失敗時應由自己恢復，還是由平台重建。</li>
<li>服務停止時有哪些 in-flight request、connection 或 job。</li>
<li>平台是否能表達 startup、readiness、liveness 與 drain 的差異。</li>
</ol>
<p>這些問題決定後續要比較 Kubernetes probe、systemd restart policy、load balancer health check 或 service mesh drain 能力。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rollout 期間新版本反覆重啟</td>
          <td>startup timeout 小於實際啟動時間</td>
          <td>拆分啟動四段分析瓶頸、調整 startup gate</td>
      </tr>
      <tr>
          <td>新版本 readiness 通過但首批請求錯誤率高</td>
          <td>readiness 條件太鬆、依賴未就緒就接流量</td>
          <td>加入必要依賴檢查、分離可降級依賴</td>
      </tr>
      <tr>
          <td>下游故障時大量實例被 liveness 重啟</td>
          <td>liveness 檢查了不該檢查的下游依賴</td>
          <td>把下游可達性移到 readiness、liveness 只看自身</td>
      </tr>
      <tr>
          <td>shutdown 後仍有請求中斷</td>
          <td>SIGTERM 未正確傳達或 drain 窗口不足</td>
          <td>驗證信號傳遞路徑、調整 terminationGracePeriod</td>
      </tr>
      <tr>
          <td>長連線服務切版後 reconnect storm</td>
          <td>drain 設計未考慮客戶端 reconnect 策略</td>
          <td>拉長 drain、分批切流、搭配 reconnect backoff</td>
      </tr>
      <tr>
          <td>worker 切版後出現重複處理</td>
          <td>job 被中斷後重試、但前次已產生副作用</td>
          <td>drain 窗口覆蓋最長 job、或 job 支援冪等</td>
      </tr>
      <tr>
          <td>遷移新平台後啟動時間變長</td>
          <td>新平台 image pull / secret mount 路徑不同</td>
          <td>重測啟動四段、調整新平台的 startup timeout</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把所有 probe 設成同一個 <code>/health</code> endpoint，會讓 startup、readiness 與 liveness 的語意混在一起。三種 probe 回答不同問題：startup 問「初始化完了嗎」、readiness 問「可以接流量嗎」、liveness 問「還活著嗎」。同一個 endpoint 無法同時回答三個問題，因為初始化完成不代表依賴就緒，依賴暫時不可達不代表服務本身壞了。</p>
<p>把 drain 窗口設成固定值不分 workload 類型，會在某一類服務上出事。5 秒對短 API 足夠、對長連線不夠、對 batch job 遠遠不夠。drain 窗口要依服務實際 workload 設定，不是用平台預設值。</p>
<p>把 liveness 失敗當成「服務壞了」而不問代價，會忽略重啟本身的連鎖效應。每次重啟都有在途請求中斷、連線重建、容量缺口的代價——特別是多實例同時被判定 liveness 失敗時，代價會被放大。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>lifecycle contract 的完整性可用多個案例交叉驗證。<a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration</a> 揭露遷移後 readiness 依賴路徑改變的風險。<a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 揭露不同 workload 的 drain 條件被忽略造成的事故擴大。<a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio 升級治理</a> 揭露基礎平台元件升級缺乏分批治理會形成全域風險放大器。<a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照</a> 揭露不同規模下 lifecycle 驗證的缺口模式。</p>
<p>這些案例共同支撐的判讀是「lifecycle contract 的每個狀態都有不同的失敗模式，混在一起處理會在事故時無法定位」。流量切換或連線生命週期問題路由到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。runtime 產物穩定性問題路由到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container 與 runtime</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>lifecycle contract 是部署模組的概念基底，後續章節都會引用本篇的狀態分類。</p>
<ol>
<li>與 5.1 的交接：runtime 與 entrypoint 定義 startup 行為回到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">container 與 runtime</a>。</li>
<li>與 5.2 的交接：probe 設定與 rollout 節奏回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes 部署策略</a>。</li>
<li>與 5.3 的交接：drain 與流量退場回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.10 的交接：tunnel 入口的 readiness 與 drain 對齊回到 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">Outbound Tunnel 入口</a>。</li>
<li>與 4.20 的交接：lifecycle 事件的證據收集回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.8 的交接：lifecycle 狀態作為 release gate 判定條件回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看 Kubernetes 如何承接這組生命週期，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a>。要看流量退場如何和 LB 對齊，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看不同平台的 lifecycle 表達力比較，接著讀 <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors/</a>。</p>
]]></content:encoded></item><item><title>5.C6 Airbnb：Kubernetes 叢集擴縮演進</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/</guid><description>&lt;p>這個案例的核心責任是說明部署平台演進常來自容量治理需求。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 的叢集擴縮經歷了多個演進階段。早期是手動調整 node 數量——工程師根據流量預測或事故壓力臨時加 node、事後忘記縮回。中期引入 Cluster Autoscaler，讓 node 數量跟 pending pod 連動。後期隨工作負載類型分化（stateless API、長連線服務、batch job、ML 訓練），單一 autoscaler policy 無法覆蓋所有場景，開始分群治理。&lt;/p>
&lt;p>這個演進路徑的共同主題是「每當流量型態或 workload 組成改變，原本的擴縮策略就會在某個量級開始失效」。擴縮策略的有效期跟服務演進速度成反比。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>叢集擴縮若停留在人工流程，面對高波動流量會放大成本與可用性風險。人工擴縮的問題有兩面：反應太慢（流量已衝高但 node 還沒加上來）和撤退太慢（流量已回落但多餘 node 繼續燒錢）。自動化解決反應速度，但引入新的判讀問題——autoscaler 的參數設定本身需要治理。&lt;/p>
&lt;p>HPA 觸發閾值設太低會造成 pod 數量頻繁抖動；Cluster Autoscaler 的 scale-down delay 設太短會在流量波動時反覆 add/remove node，增加 pod eviction 頻率。這些參數的調校要依 workload 類型分群——API 服務的擴縮節奏跟 batch job 完全不同。&lt;/p>
&lt;p>另一個判讀是擴縮策略跟事故指標要綁定。autoscaler 的動作（scale-up trigger、scale-down execution、node provision latency）如果不在事故 timeline 上可見，事故團隊無法分辨「是 autoscaler 來不及」還是「是應用本身有問題」。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>擴縮策略版本化與可回放&lt;/strong>：HPA / VPA / Cluster Autoscaler / Karpenter 的配置進 git，變更走 release flow。每次調參都有 commit 紀錄，事故後可以追溯「這次 scale-down 過快是因為哪次參數變更」。版本化的另一個價值是可回放——新的擴縮配置在 staging 環境用歷史流量 replay 驗證後，再推到 production。&lt;/li>
&lt;li>&lt;strong>workload 分群擴縮&lt;/strong>：stateless API 用 CPU / RPS-based HPA、batch job 用 queue depth-based HPA、長連線服務用 connection count-based 自訂 metric。不同 workload 類型放在不同 namespace，各自有獨立的 autoscaler policy。避免一套 HPA 規則套全部 workload。&lt;/li>
&lt;li>&lt;strong>容量治理與事故指標綁定&lt;/strong>：HPA 觸發事件、Cluster Autoscaler 的 scale-up / scale-down 事件、node provision latency 都送進事故 timeline（可用 Kubernetes event exporter 或 custom metric）。事故 timeline 上看到「HPA 觸發後 3 分鐘 node 才 ready」就能直接判斷「容量補充太慢」而非「應用有 bug」。&lt;/li>
&lt;/ol>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>擴縮策略變更的回退比應用版本回退簡單——改 HPA / autoscaler 的 config 就好。風險在於回退後的舊策略可能已經跟當前 workload 型態不匹配（workload 成長了、流量特性變了）。穩定做法是回退後立刻進入觀察窗口，確認舊策略在當前流量下仍然有效。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment&lt;/a> 看 autoscaling 與部署策略協同。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 platform lifecycle contract&lt;/a> 看不同 workload 的 lifecycle 差異如何影響擴縮設計。回 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity &amp;amp; cost&lt;/a> 看容量規劃的完整框架。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://airbnb.tech/infrastructure/dynamic-kubernetes-cluster-scaling-at-airbnb/">Dynamic Kubernetes Cluster Scaling at Airbnb&lt;/a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明部署平台演進常來自容量治理需求。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 的叢集擴縮經歷了多個演進階段。早期是手動調整 node 數量——工程師根據流量預測或事故壓力臨時加 node、事後忘記縮回。中期引入 Cluster Autoscaler，讓 node 數量跟 pending pod 連動。後期隨工作負載類型分化（stateless API、長連線服務、batch job、ML 訓練），單一 autoscaler policy 無法覆蓋所有場景，開始分群治理。</p>
<p>這個演進路徑的共同主題是「每當流量型態或 workload 組成改變，原本的擴縮策略就會在某個量級開始失效」。擴縮策略的有效期跟服務演進速度成反比。</p>
<h2 id="判讀">判讀</h2>
<p>叢集擴縮若停留在人工流程，面對高波動流量會放大成本與可用性風險。人工擴縮的問題有兩面：反應太慢（流量已衝高但 node 還沒加上來）和撤退太慢（流量已回落但多餘 node 繼續燒錢）。自動化解決反應速度，但引入新的判讀問題——autoscaler 的參數設定本身需要治理。</p>
<p>HPA 觸發閾值設太低會造成 pod 數量頻繁抖動；Cluster Autoscaler 的 scale-down delay 設太短會在流量波動時反覆 add/remove node，增加 pod eviction 頻率。這些參數的調校要依 workload 類型分群——API 服務的擴縮節奏跟 batch job 完全不同。</p>
<p>另一個判讀是擴縮策略跟事故指標要綁定。autoscaler 的動作（scale-up trigger、scale-down execution、node provision latency）如果不在事故 timeline 上可見，事故團隊無法分辨「是 autoscaler 來不及」還是「是應用本身有問題」。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>擴縮策略版本化與可回放</strong>：HPA / VPA / Cluster Autoscaler / Karpenter 的配置進 git，變更走 release flow。每次調參都有 commit 紀錄，事故後可以追溯「這次 scale-down 過快是因為哪次參數變更」。版本化的另一個價值是可回放——新的擴縮配置在 staging 環境用歷史流量 replay 驗證後，再推到 production。</li>
<li><strong>workload 分群擴縮</strong>：stateless API 用 CPU / RPS-based HPA、batch job 用 queue depth-based HPA、長連線服務用 connection count-based 自訂 metric。不同 workload 類型放在不同 namespace，各自有獨立的 autoscaler policy。避免一套 HPA 規則套全部 workload。</li>
<li><strong>容量治理與事故指標綁定</strong>：HPA 觸發事件、Cluster Autoscaler 的 scale-up / scale-down 事件、node provision latency 都送進事故 timeline（可用 Kubernetes event exporter 或 custom metric）。事故 timeline 上看到「HPA 觸發後 3 分鐘 node 才 ready」就能直接判斷「容量補充太慢」而非「應用有 bug」。</li>
</ol>
<h2 id="回退判讀">回退判讀</h2>
<p>擴縮策略變更的回退比應用版本回退簡單——改 HPA / autoscaler 的 config 就好。風險在於回退後的舊策略可能已經跟當前 workload 型態不匹配（workload 成長了、流量特性變了）。穩定做法是回退後立刻進入觀察窗口，確認舊策略在當前流量下仍然有效。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment</a> 看 autoscaling 與部署策略協同。回 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 platform lifecycle contract</a> 看不同 workload 的 lifecycle 差異如何影響擴縮設計。回 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity &amp; cost</a> 看容量規劃的完整框架。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://airbnb.tech/infrastructure/dynamic-kubernetes-cluster-scaling-at-airbnb/">Dynamic Kubernetes Cluster Scaling at Airbnb</a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）</li>
</ul>
]]></content:encoded></item><item><title>AWS ELB（ALB / NLB / CLB）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/aws-elb/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/aws-elb/</guid><description>&lt;p>AWS ELB 是 AWS managed load balancer 系列、承擔三個責任：流量入口（HTTP/HTTPS for ALB、TCP/UDP for NLB）、health check + draining、跟 AWS 生態整合（ACM TLS / Target Group / WAF / Lambda）。包含 ALB（L7、HTTP/HTTPS）、NLB（L4、極低延遲）、CLB（legacy、不要選）。設計取捨偏向「managed + AWS-native + integrate with ECS/EKS/Lambda」、跨雲 / 進階 traffic management 是限制。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>建立 ALB / NLB、配置 listener + target group&lt;/li>
&lt;li>設計 health check + connection draining&lt;/li>
&lt;li>用 ACM 自動憑證 + SNI&lt;/li>
&lt;li>用 ALB Ingress Controller / AWS Load Balancer Controller for K8s&lt;/li>
&lt;li>評估 ALB vs NLB vs CloudFront vs API Gateway&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-aws-elb-跑起來">最短路徑：5 分鐘把 AWS ELB 跑起來&lt;/h2>





&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"># 1. 建 ALB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">aws elbv2 create-load-balancer &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> --name demo-alb &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> --subnets subnet-aaa subnet-bbb &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> --security-groups sg-xxx &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> --scheme internet-facing &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> --type application
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 建 target group + register targets&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">aws elbv2 create-target-group &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --name demo-tg &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --protocol HTTP --port &lt;span class="m">8080&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --vpc-id vpc-xxx &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --target-type instance &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --health-check-path /health &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --health-check-interval-seconds &lt;span class="m">15&lt;/span>
&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">aws elbv2 register-targets &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/demo-tg/... &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --targets &lt;span class="nv">Id&lt;/span>&lt;span class="o">=&lt;/span>i-0abc123 &lt;span class="nv">Id&lt;/span>&lt;span class="o">=&lt;/span>i-0def456
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 建 listener + 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">aws elbv2 create-listener &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/demo-alb/... &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --protocol HTTP --port &lt;span class="m">80&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --default-actions &lt;span class="nv">Type&lt;/span>&lt;span class="o">=&lt;/span>forward,TargetGroupArn&lt;span class="o">=&lt;/span>arn:aws:...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="nv">ALB_DNS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>aws elbv2 describe-load-balancers --names demo-alb &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;LoadBalancers[0].DNSName&amp;#39;&lt;/span> --output text&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">curl &lt;span class="s2">&amp;#34;http://&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">ALB_DNS&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="alb-vs-nlb-vs-clb">ALB vs NLB vs CLB&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>ALB：L7、path/host routing、WebSocket、gRPC、Lambda target&lt;/li>
&lt;li>NLB：L4、static IP、preserve client IP、極低延遲、TCP/UDP&lt;/li>
&lt;li>CLB：legacy、不要新用&lt;/li>
&lt;li>選擇判讀：HTTP/HTTPS → ALB；TCP/UDP / 高吞吐 → NLB&lt;/li>
&lt;/ul>
&lt;h3 id="target-group--listener-rule">Target group / listener rule&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>AWS ELB 是 AWS managed load balancer 系列、承擔三個責任：流量入口（HTTP/HTTPS for ALB、TCP/UDP for NLB）、health check + draining、跟 AWS 生態整合（ACM TLS / Target Group / WAF / Lambda）。包含 ALB（L7、HTTP/HTTPS）、NLB（L4、極低延遲）、CLB（legacy、不要選）。設計取捨偏向「managed + AWS-native + integrate with ECS/EKS/Lambda」、跨雲 / 進階 traffic management 是限制。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>建立 ALB / NLB、配置 listener + target group</li>
<li>設計 health check + connection draining</li>
<li>用 ACM 自動憑證 + SNI</li>
<li>用 ALB Ingress Controller / AWS Load Balancer Controller for K8s</li>
<li>評估 ALB vs NLB vs CloudFront vs API Gateway</li>
</ol>
<h2 id="最短路徑5-分鐘把-aws-elb-跑起來">最短路徑：5 分鐘把 AWS ELB 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 建 ALB</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws elbv2 create-load-balancer <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --name demo-alb <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --subnets subnet-aaa subnet-bbb <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --security-groups sg-xxx <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --scheme internet-facing <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --type application
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 2. 建 target group + register targets</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">aws elbv2 create-target-group <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --name demo-tg <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --protocol HTTP --port <span class="m">8080</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --vpc-id vpc-xxx <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --target-type instance <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --health-check-path /health <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --health-check-interval-seconds <span class="m">15</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">aws elbv2 register-targets <span class="se">\
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="se"></span>  --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/demo-tg/... <span class="se">\
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="se"></span>  --targets <span class="nv">Id</span><span class="o">=</span>i-0abc123 <span class="nv">Id</span><span class="o">=</span>i-0def456
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"># 3. 建 listener + 驗證</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">aws elbv2 create-listener <span class="se">\
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="se"></span>  --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/demo-alb/... <span class="se">\
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="se"></span>  --protocol HTTP --port <span class="m">80</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="se"></span>  --default-actions <span class="nv">Type</span><span class="o">=</span>forward,TargetGroupArn<span class="o">=</span>arn:aws:...
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="nv">ALB_DNS</span><span class="o">=</span><span class="k">$(</span>aws elbv2 describe-load-balancers --names demo-alb <span class="se">\
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;LoadBalancers[0].DNSName&#39;</span> --output text<span class="k">)</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">curl <span class="s2">&#34;http://</span><span class="si">${</span><span class="nv">ALB_DNS</span><span class="si">}</span><span class="s2">&#34;</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="alb-vs-nlb-vs-clb">ALB vs NLB vs CLB</h3>
<p>子議題：</p>
<ul>
<li>ALB：L7、path/host routing、WebSocket、gRPC、Lambda target</li>
<li>NLB：L4、static IP、preserve client IP、極低延遲、TCP/UDP</li>
<li>CLB：legacy、不要新用</li>
<li>選擇判讀：HTTP/HTTPS → ALB；TCP/UDP / 高吞吐 → NLB</li>
</ul>
<h3 id="target-group--listener-rule">Target group / listener rule</h3>
<p>子議題：</p>
<ul>
<li>Target type：instance / IP / Lambda</li>
<li>Listener rule：path-based / host-based / header-based routing</li>
<li>Priority 排序</li>
<li>對應指令：<code>aws elbv2 modify-rule</code></li>
</ul>
<h3 id="health-check-與-draining">Health check 與 draining</h3>
<p>子議題：</p>
<ul>
<li>Health check：HTTP path / interval / threshold</li>
<li>Connection draining（deregistration delay）：deregister 後等到 in-flight requests 完成</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例 cutover without drain</a></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="tls-termination--sni">TLS termination + SNI</h3>
<p>子議題：</p>
<ul>
<li>ACM 自動憑證 + 續期</li>
<li>SNI：單 ALB 多 domain（最多 25 certificates）</li>
<li>TLS policy（min TLS version）</li>
<li>Mutual TLS（ALB 2023+）</li>
</ul>
<h3 id="alb-ingress-controller--aws-load-balancer-controller">ALB Ingress Controller / AWS Load Balancer Controller</h3>
<p>子議題：</p>
<ul>
<li>在 EKS 內配置 ALB / NLB（Ingress / Service of type LoadBalancer）</li>
<li>IngressClass / annotations</li>
<li>Pod readiness gate（pod 到 ALB target group healthy 才接流量）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes vendor 頁</a></li>
</ul>
<h3 id="cross-zone-load-balancing">Cross-zone load balancing</h3>
<p>子議題：</p>
<ul>
<li>ALB default enabled、NLB default disabled</li>
<li>Cross-zone 跨 AZ data transfer cost</li>
<li>跟 AZ failover 對應</li>
</ul>
<h3 id="waf-integration">WAF integration</h3>
<p>子議題：</p>
<ul>
<li>AWS WAF on ALB</li>
<li>Rate-based rule / managed rule group</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security WAF</a></li>
</ul>
<h3 id="idle-timeout">Idle timeout</h3>
<p>子議題：</p>
<ul>
<li>ALB default 60s、可調 1-4000s</li>
<li>跟 keep-alive / WebSocket 長連線對應</li>
<li>跟 backend（K8s pod / EC2）的 timeout 對齊</li>
</ul>
<h3 id="cost-模型">Cost 模型</h3>
<p>子議題：</p>
<ul>
<li>LB-hour（per ALB / NLB）</li>
<li>LCU（Load Balancer Capacity Unit）— 多維度計算</li>
<li>Data processing charge</li>
<li>跨 AZ data transfer</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="target-unhealthy">Target unhealthy</h3>
<p>操作原則：health check path 不對 / security group 沒開 / backend 反應慢。</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 elbv2 describe-target-health <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/demo-tg/...
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># HealthState: unhealthy → 查 Reason（Target.Timeout / Elb.InternalError / Target.ResponseCodeMismatch）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 常見根因：security group 沒開 health check port、health check path 回 404、backend 回應超過 timeout</span></span></span></code></pre></div><h3 id="504-gateway-timeout">504 Gateway Timeout</h3>
<p>操作原則：backend 超 ALB idle timeout / 60s。判讀：backend log + ALB access log。</p>
<h3 id="cross-zone-imbalance">Cross-zone imbalance</h3>
<p>操作原則：cross-zone disabled、流量集中單 AZ。修法：enable cross-zone（注意 cost）。</p>
<h3 id="draining-卡住">Draining 卡住</h3>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>。判讀：deregistration delay 太短 / connection 未結束就被斷。</p>
<h3 id="acm-cert-renew-失敗">ACM cert renew 失敗</h3>
<p>操作原則：DNS validation 失敗 / domain ownership 變動。判讀：ACM console 看 cert state。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨雲 / 自管</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a> / <a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a> + Istio</td>
      </tr>
      <tr>
          <td>Cloud-native auto-discovery</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></td>
      </tr>
      <tr>
          <td>CDN / edge</td>
          <td>CloudFront / Cloudflare / Fastly</td>
      </tr>
      <tr>
          <td>API Gateway</td>
          <td>AWS API Gateway / Kong</td>
      </tr>
      <tr>
          <td>極低成本</td>
          <td>自管 <a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a> on EC2</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>AWS WAF rule 完整 reference</li>
<li>Network Firewall 配置</li>
<li>各 AWS region 限制差異</li>
<li>ELB classic（CLB）細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>遷 EKS 時 ALB / NLB 是入口、切流批次跟 target group 權重連動</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast EKS</a></td>
          <td>多集群整併 EKS、AWS Load Balancer Controller 統一 ingress 入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4 Mobileye EKS</a></td>
          <td>大規模 workload 遷 EKS、ALB target group health check 是切流驗證點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro EKS</a></td>
          <td>Managed EKS 後 ALB / NLB 治理回到平台團隊</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 AWS ELB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>ALB deregistration delay / NLB connection draining 是切流的關鍵回退面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>AWS 生態小型 ALB + EC2 / 中型 ALB + EKS / 大型 NLB + 多 region + WAF</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 AWS ELB 案例</strong>：大規模 AWS Load Balancer Controller 客戶案例、NLB static IP 場景、AWS WAF + ALB 安全整合。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a>、<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security WAF</a>、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6 reliability release gate</a></li>
</ul>
]]></content:encoded></item><item><title>模組六：生產操作</title><link>https://tarrragon.github.io/blog/go-advanced/06-production-operations/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/06-production-operations/</guid><description>&lt;p>生產操作的核心目標是讓 Go 服務可停止、可觀測、可診斷、可漸進啟用功能。服務能在本機跑起來只是第一步；長時間運行後，真正重要的是 shutdown 是否可預期、監控訊號是否清楚、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 是否可查詢、功能開關是否有降級策略。&lt;/p>
&lt;p>本模組承接前面的並發、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a>、runtime 與測試：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a> 需要 context 和 goroutine lifecycle，health endpoint 需要區分可用性與診斷，structured &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 需要能追 event flow，feature gate 需要能安全控制新能力。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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;a href="https://tarrragon.github.io/blog/go-advanced/06-production-operations/graceful-shutdown/" data-link-title="6.1 graceful shutdown 與 signal handling" data-link-desc="用 signal 與 context 傳遞停止訊號">6.1&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a> 與 signal handling&lt;/td>
 &lt;td>用 signal、context、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 與 owner cleanup 停止服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/06-production-operations/health-diagnostics/" data-link-title="6.2 健康檢查與診斷 endpoint" data-link-desc="區分服務可用性與工程診斷入口">6.2&lt;/a>&lt;/td>
 &lt;td>健康檢查與診斷 endpoint&lt;/td>
 &lt;td>區分 health、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a>、diagnostics 與 status code 合約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/06-production-operations/log-fields/" data-link-title="6.3 結構化日誌欄位設計" data-link-desc="讓 log 可 grep、可聚合、可追蹤">6.3&lt;/a>&lt;/td>
 &lt;td>結構化日誌欄位設計&lt;/td>
 &lt;td>用穩定欄位讓 log 可 grep、可聚合、可追蹤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/06-production-operations/feature-gate/" data-link-title="6.4 版本偵測與 feature gate" data-link-desc="依版本與環境能力啟用功能">6.4&lt;/a>&lt;/td>
 &lt;td>版本偵測與 feature gate&lt;/td>
 &lt;td>用功能開關、能力偵測與降級策略控制行為&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="本模組使用的範例主題">本模組使用的範例主題&lt;/h2>
&lt;p>本模組使用虛構的即時通知服務作為範例。範例包含 HTTP server、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> hub、background worker、runtime diagnostics、structured log 與 feature gate。&lt;/p>
&lt;p>範例只用來展示 Go 生產操作設計，不假設讀者正在維護任何特定專案。&lt;/p>
&lt;h2 id="本模組的-go-核心概念">本模組的 Go 核心概念&lt;/h2>
&lt;ul>
&lt;li>用 &lt;code>signal.NotifyContext&lt;/code> 或 signal channel 建立 root context。&lt;/li>
&lt;li>用 &lt;code>http.Server.Shutdown&lt;/code> 停止接受新 request。&lt;/li>
&lt;li>用 context 傳遞停止訊號給 worker、hub、WebSocket pump。&lt;/li>
&lt;li>用 &lt;code>/health&lt;/code>、&lt;code>/ready&lt;/code>、&lt;code>/debug/...&lt;/code> 分開不同操作訊號。&lt;/li>
&lt;li>用 &lt;code>log/slog&lt;/code> 建立穩定 structured fields。&lt;/li>
&lt;li>用 config struct 載入 feature gate，而不是到處讀環境變數。&lt;/li>
&lt;/ul>
&lt;h2 id="學習重點">學習重點&lt;/h2>
&lt;p>學完本模組後，你應該能判斷：&lt;/p>
&lt;ol>
&lt;li>服務收到停止訊號後，哪些元件要先停止接流量&lt;/li>
&lt;li>health、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a>、diagnostics 各自回答什麼問題&lt;/li>
&lt;li>structured log 欄位如何支援查詢與聚合&lt;/li>
&lt;li>哪些資料不應進入 log&lt;/li>
&lt;li>feature gate 關閉時應降級、回錯、隱藏還是排程稍後處理&lt;/li>
&lt;/ol>
&lt;h2 id="本模組不處理">本模組不處理&lt;/h2>
&lt;p>本模組不討論 Kubernetes、systemd、雲端平台或完整 SRE 流程的所有細節。這些環境會影響操作策略，但本模組先建立 Go 服務本身應具備的操作邊界；後續可接 &lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/deployment-contracts/" data-link-title="7.5 Kubernetes、systemd 與 load balancer 合約" data-link-desc="理解部署平台如何影響 Go 服務的 shutdown、health 與資源限制">Kubernetes、systemd 與 load balancer 合約&lt;/a> 以及 &lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/observability-pipeline/" data-link-title="7.4 Observability pipeline、metrics 與 tracing" data-link-desc="把 structured log、metric、trace 與 profile 組成可操作的診斷系統">Observability pipeline、metrics 與 tracing&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>生產操作的核心目標是讓 Go 服務可停止、可觀測、可診斷、可漸進啟用功能。服務能在本機跑起來只是第一步；長時間運行後，真正重要的是 shutdown 是否可預期、監控訊號是否清楚、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 是否可查詢、功能開關是否有降級策略。</p>
<p>本模組承接前面的並發、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a>、runtime 與測試：<a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 需要 context 和 goroutine lifecycle，health endpoint 需要區分可用性與診斷，structured <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 需要能追 event flow，feature gate 需要能安全控制新能力。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/go-advanced/06-production-operations/graceful-shutdown/" data-link-title="6.1 graceful shutdown 與 signal handling" data-link-desc="用 signal 與 context 傳遞停止訊號">6.1</a></td>
          <td><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 與 signal handling</td>
          <td>用 signal、context、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 與 owner cleanup 停止服務</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/06-production-operations/health-diagnostics/" data-link-title="6.2 健康檢查與診斷 endpoint" data-link-desc="區分服務可用性與工程診斷入口">6.2</a></td>
          <td>健康檢查與診斷 endpoint</td>
          <td>區分 health、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、diagnostics 與 status code 合約</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/06-production-operations/log-fields/" data-link-title="6.3 結構化日誌欄位設計" data-link-desc="讓 log 可 grep、可聚合、可追蹤">6.3</a></td>
          <td>結構化日誌欄位設計</td>
          <td>用穩定欄位讓 log 可 grep、可聚合、可追蹤</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/06-production-operations/feature-gate/" data-link-title="6.4 版本偵測與 feature gate" data-link-desc="依版本與環境能力啟用功能">6.4</a></td>
          <td>版本偵測與 feature gate</td>
          <td>用功能開關、能力偵測與降級策略控制行為</td>
      </tr>
  </tbody>
</table>
<h2 id="本模組使用的範例主題">本模組使用的範例主題</h2>
<p>本模組使用虛構的即時通知服務作為範例。範例包含 HTTP server、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> hub、background worker、runtime diagnostics、structured log 與 feature gate。</p>
<p>範例只用來展示 Go 生產操作設計，不假設讀者正在維護任何特定專案。</p>
<h2 id="本模組的-go-核心概念">本模組的 Go 核心概念</h2>
<ul>
<li>用 <code>signal.NotifyContext</code> 或 signal channel 建立 root context。</li>
<li>用 <code>http.Server.Shutdown</code> 停止接受新 request。</li>
<li>用 context 傳遞停止訊號給 worker、hub、WebSocket pump。</li>
<li>用 <code>/health</code>、<code>/ready</code>、<code>/debug/...</code> 分開不同操作訊號。</li>
<li>用 <code>log/slog</code> 建立穩定 structured fields。</li>
<li>用 config struct 載入 feature gate，而不是到處讀環境變數。</li>
</ul>
<h2 id="學習重點">學習重點</h2>
<p>學完本模組後，你應該能判斷：</p>
<ol>
<li>服務收到停止訊號後，哪些元件要先停止接流量</li>
<li>health、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、diagnostics 各自回答什麼問題</li>
<li>structured log 欄位如何支援查詢與聚合</li>
<li>哪些資料不應進入 log</li>
<li>feature gate 關閉時應降級、回錯、隱藏還是排程稍後處理</li>
</ol>
<h2 id="本模組不處理">本模組不處理</h2>
<p>本模組不討論 Kubernetes、systemd、雲端平台或完整 SRE 流程的所有細節。這些環境會影響操作策略，但本模組先建立 Go 服務本身應具備的操作邊界；後續可接 <a href="/blog/go-advanced/07-distributed-operations/deployment-contracts/" data-link-title="7.5 Kubernetes、systemd 與 load balancer 合約" data-link-desc="理解部署平台如何影響 Go 服務的 shutdown、health 與資源限制">Kubernetes、systemd 與 load balancer 合約</a> 以及 <a href="/blog/go-advanced/07-distributed-operations/observability-pipeline/" data-link-title="7.4 Observability pipeline、metrics 與 tracing" data-link-desc="把 structured log、metric、trace 與 profile 組成可操作的診斷系統">Observability pipeline、metrics 與 tracing</a>。</p>
<h2 id="學習時間">學習時間</h2>
<p>預計 3-4 小時</p>
]]></content:encoded></item><item><title>5.7 Traffic、Config 與 Control Plane Boundary</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/</guid><description>&lt;p>Traffic、config 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane&lt;/a> boundary 的核心責任是把平台切換中的資料面與控制面分開。進入 Kubernetes、ELB、Envoy、Consul 或 Terraform 前，讀者需要先知道流量、設定、secret、service discovery 與管理面各自有不同風險與回退方式。&lt;/p>
&lt;h2 id="traffic-boundary">Traffic Boundary&lt;/h2>
&lt;p>Traffic boundary 的責任是決定 request 如何進入服務、如何分流、如何回退。它包含 load balancer、routing rule、health check、sticky session、timeout 與 drain。&lt;/p>
&lt;p>流量切換要能回答三個問題：哪一批 request 會到新版本、失敗時如何停止擴批、舊版本是否仍能承接回退流量。這三個答案明確後，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary&lt;/a> 才能從比例設定變成可回退策略。&lt;/p>
&lt;p>Traffic boundary 的判讀重點是 customer impact 如何被分批限制。小比例 canary、區域切流、tenant 切流與 route rule 都是不同切換單位；切換單位越清楚，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window&lt;/a> 越容易被驗證。&lt;/p>
&lt;h3 id="切換單位的選擇">切換單位的選擇&lt;/h3>
&lt;p>切換單位決定故障的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 與回退的精準度。常見切換單位各有不同操作特性：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>切換單位&lt;/th>
 &lt;th>blast radius&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;/td>
 &lt;td>粗（全域）&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>通用 canary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>區域 / AZ&lt;/td>
 &lt;td>限定地理範圍&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>跨區部署的服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>租戶 / 組織&lt;/td>
 &lt;td>限定特定客戶&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>多租戶 SaaS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>路由規則&lt;/td>
 &lt;td>限定特定路徑&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>API 版本切換、功能漸進上線&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>比例切換最簡單但 blast radius 不可控——5% 的流量中可能包含大客戶的關鍵路徑。租戶切換精準度最高但操作複雜度也最高——需要在 routing 層維護租戶到版本的映射。穩定做法是從比例切換開始，遇到需要精準控制 impact 時再升級到租戶或路由規則切換。&lt;/p>
&lt;h2 id="config-boundary">Config Boundary&lt;/h2>
&lt;p>設定如何下發、如何生效、如何回退——Config boundary 回答這三個問題。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout&lt;/a> 和應用版本不一定同步，因此要保留相容窗口。&lt;/p>
&lt;p>高風險設定包含 payment provider endpoint、feature flag、rate limit、routing rule、timeout 與 fallback policy。這些設定變更可能不需要新 image，卻能改變 production 行為，因此要進 release gate。&lt;/p>
&lt;h3 id="config-變更的風險分級">Config 變更的風險分級&lt;/h3>
&lt;p>設定變更的風險不一致——有些設定改了只影響 log level，有些設定改了直接影響付款路徑。分級後才能對不同風險的設定套用對應的 review 與 rollout 強度。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>風險等級&lt;/th>
 &lt;th>設定類型&lt;/th>
 &lt;th>review 與 rollout 要求&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>payment endpoint、auth provider URL、encryption key&lt;/td>
 &lt;td>等同 code review + staged rollout + rollback 驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中&lt;/td>
 &lt;td>rate limit、timeout、feature flag、CORS 設定&lt;/td>
 &lt;td>變更 review + 觀測窗口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>log level、debug flag、非關鍵 UI 文案&lt;/td>
 &lt;td>變更紀錄即可&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>風險分級的判讀依據是「這個設定改錯時、使用者會看到什麼」。改錯 payment endpoint 會讓付款打到錯誤目標；改錯 rate limit 可能讓合法流量被擋；改錯 log level 最多是 log 太吵或太安靜。設定的注入方式與版本追蹤見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Traffic、config 與 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> boundary 的核心責任是把平台切換中的資料面與控制面分開。進入 Kubernetes、ELB、Envoy、Consul 或 Terraform 前，讀者需要先知道流量、設定、secret、service discovery 與管理面各自有不同風險與回退方式。</p>
<h2 id="traffic-boundary">Traffic Boundary</h2>
<p>Traffic boundary 的責任是決定 request 如何進入服務、如何分流、如何回退。它包含 load balancer、routing rule、health check、sticky session、timeout 與 drain。</p>
<p>流量切換要能回答三個問題：哪一批 request 會到新版本、失敗時如何停止擴批、舊版本是否仍能承接回退流量。這三個答案明確後，<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a> 才能從比例設定變成可回退策略。</p>
<p>Traffic boundary 的判讀重點是 customer impact 如何被分批限制。小比例 canary、區域切流、tenant 切流與 route rule 都是不同切換單位；切換單位越清楚，<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 越容易被驗證。</p>
<h3 id="切換單位的選擇">切換單位的選擇</h3>
<p>切換單位決定故障的 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 與回退的精準度。常見切換單位各有不同操作特性：</p>
<table>
  <thead>
      <tr>
          <th>切換單位</th>
          <th>blast radius</th>
          <th>回退精準度</th>
          <th>操作複雜度</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>比例（%）</td>
          <td>按流量比例</td>
          <td>粗（全域）</td>
          <td>低</td>
          <td>通用 canary</td>
      </tr>
      <tr>
          <td>區域 / AZ</td>
          <td>限定地理範圍</td>
          <td>中</td>
          <td>中</td>
          <td>跨區部署的服務</td>
      </tr>
      <tr>
          <td>租戶 / 組織</td>
          <td>限定特定客戶</td>
          <td>高</td>
          <td>高</td>
          <td>多租戶 SaaS</td>
      </tr>
      <tr>
          <td>路由規則</td>
          <td>限定特定路徑</td>
          <td>高</td>
          <td>高</td>
          <td>API 版本切換、功能漸進上線</td>
      </tr>
  </tbody>
</table>
<p>比例切換最簡單但 blast radius 不可控——5% 的流量中可能包含大客戶的關鍵路徑。租戶切換精準度最高但操作複雜度也最高——需要在 routing 層維護租戶到版本的映射。穩定做法是從比例切換開始，遇到需要精準控制 impact 時再升級到租戶或路由規則切換。</p>
<h2 id="config-boundary">Config Boundary</h2>
<p>設定如何下發、如何生效、如何回退——Config boundary 回答這三個問題。<a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout</a> 和應用版本不一定同步，因此要保留相容窗口。</p>
<p>高風險設定包含 payment provider endpoint、feature flag、rate limit、routing rule、timeout 與 fallback policy。這些設定變更可能不需要新 image，卻能改變 production 行為，因此要進 release gate。</p>
<h3 id="config-變更的風險分級">Config 變更的風險分級</h3>
<p>設定變更的風險不一致——有些設定改了只影響 log level，有些設定改了直接影響付款路徑。分級後才能對不同風險的設定套用對應的 review 與 rollout 強度。</p>
<table>
  <thead>
      <tr>
          <th>風險等級</th>
          <th>設定類型</th>
          <th>review 與 rollout 要求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高</td>
          <td>payment endpoint、auth provider URL、encryption key</td>
          <td>等同 code review + staged rollout + rollback 驗證</td>
      </tr>
      <tr>
          <td>中</td>
          <td>rate limit、timeout、feature flag、CORS 設定</td>
          <td>變更 review + 觀測窗口</td>
      </tr>
      <tr>
          <td>低</td>
          <td>log level、debug flag、非關鍵 UI 文案</td>
          <td>變更紀錄即可</td>
      </tr>
  </tbody>
</table>
<p>風險分級的判讀依據是「這個設定改錯時、使用者會看到什麼」。改錯 payment endpoint 會讓付款打到錯誤目標；改錯 rate limit 可能讓合法流量被擋；改錯 log level 最多是 log 太吵或太安靜。設定的注入方式與版本追蹤見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨</a>。</p>
<h2 id="secret-boundary">Secret Boundary</h2>
<p>Credential、token、certificate 與 machine identity 需要可輪替、可稽核、可回退——Secret boundary 管理這組生命週期。Secret 變更同時影響平台、應用與外部依賴，應使用比普通 config 更嚴格的 evidence 與 rollback window。</p>
<p>Secret rollout 要回答版本相容、雙軌驗證、舊 secret 撤除時間與失敗回退。這裡要接到 <a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.27 Credential Rotation with Scoped Evidence</a>。</p>
<h3 id="secret-rollout-的雙軌驗證">Secret Rollout 的雙軌驗證</h3>
<p>Secret 輪替跟應用版本部署有本質差異：rollback secret 不是「換回舊版本」那麼單純——舊 secret 可能已經被撤銷、過期、或在外部系統中標記為失效。Secret rollout 的安全做法是雙軌驗證：</p>
<ol>
<li><strong>新 secret 先加入、舊 secret 暫不移除</strong>：應用先驗證能用新 secret 正常運作。</li>
<li><strong>觀測窗口確認新 secret 穩定</strong>：auth 成功率、API 呼叫成功率、certificate handshake 成功率都在 baseline 內。</li>
<li><strong>確認後移除舊 secret</strong>：舊 secret 的撤除要有明確時間點，而且要在撤除前確認沒有服務還在用舊 secret。</li>
</ol>
<p>這個流程的風險點是第 3 步：撤除舊 secret 後發現某個遺漏的服務或 job 還在用、導致該服務認證失敗。盤點覆蓋率的做法是在觀測窗口內搜尋 audit log，確認所有 secret 使用都已切到新版本。</p>
<h2 id="service-discovery-boundary">Service Discovery Boundary</h2>
<p>Service discovery 的責任是維持可用 endpoint 集合。它回答服務應該連到哪些實例；業務設定與版本正確性則分別交給 config boundary 與 rollout gate。Discovery 的 DNS / registry 運作模式與註冊時序見 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 Service Discovery</a>。</p>
<p>Discovery 失準常見於 rollout、擴縮容與區域故障。判讀時要拆成註冊時序、健康判斷、DNS/registry 新鮮度與 fallback 存活時間。</p>
<h2 id="control-plane-boundary">Control Plane Boundary</h2>
<p>設定、策略、部署與路由規則的管理落在 <a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane</a>。Control plane 變更會影響大量服務，因此需要更嚴格的 evidence、gate 與 decision log。</p>
<p>Control plane 事故常見於規則推送、routing 誤配、secret 下發失敗與 registry 異常。這類事故要先保留 decision timeline，避免事後只看到資料面錯誤率。</p>
<h3 id="control-plane-變更的-blast-radius-控制">Control Plane 變更的 Blast Radius 控制</h3>
<p>Control plane 變更的 blast radius 跟 data plane 變更不同——一條 routing rule 推送錯誤可能同時影響所有服務的流量。控制 blast radius 的做法：</p>
<ol>
<li><strong>分批推送</strong>：規則變更先推到 staging / canary namespace、驗證後再推到 production。推送結果的觀測應包含受影響服務的 error rate 與 latency。</li>
<li><strong>approval gate</strong>：高影響變更（network policy、admission webhook、RBAC binding）需要多人 review。變更的 blast radius 估算（影響多少 namespace / service）應在 review 時可見。</li>
<li><strong>decision log</strong>：所有 control plane 變更記入 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log</a>，包含時間、操作者、受影響範圍、預期效果與回退條件。事故時對照 decision log 跟 data plane 症狀的時間序列，可以快速判斷因果。</li>
</ol>
<h2 id="平台元件升級的可重播流程">平台元件升級的可重播流程</h2>
<p>平台基礎元件升級是 control plane 風險最高的場景。Service mesh、ingress controller、CNI、API server 這類元件影響面廣、單次升級可能形成全域風險放大器。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio 升級治理</a>：揭露 1 個判讀（基礎平台元件升級缺乏分批治理會形成全域風險放大器）+ 3 條策略（分批升級 + 回退窗口、升級驗證標準固定化、升級事件接入 incident command 節奏）。以下基於通用工程知識展開、「升級事件進 timeline」是從 case「接入 incident command」策略進一步推到具體操作。</p>
<p>可重複套用的升級流程：</p>
<ol>
<li><strong>分批升級單位</strong>：先在開發 / staging 叢集驗證、再選低流量 production 叢集 / namespace 作為先導、之後分批擴大。分批單位可以是叢集、namespace、region、tenant，依風險面選擇。</li>
<li><strong>回退窗口跟驗證標準同時設</strong>：每批升級前定義「驗證通過」的具體訊號（SLI 維持、特定 metric 不偏移、無新告警），跟「回退窗口」（多久內可以回退）。沒有驗證標準的分批等於連續高風險動作。</li>
<li><strong>升級流程紀錄到 incident-style 文件</strong>：升級期間的決策、觀察、停止點都用 incident decision log 格式紀錄。下次升級可重播、不依賴執行者個人經驗。</li>
<li><strong>升級事件進 timeline</strong>：升級本身產生的短暫錯誤、reconnect、配置同步延遲，要在事故 timeline 上可見、避免被誤判成事故。</li>
</ol>
<p>平台元件升級的核心治理價值是把「一次性高風險作業」變成「可重複的低風險作業」。第一次升級用流程，第二次升級用同樣流程，第三次升級流程已經穩定到可以委派、不再需要資深工程師親自執行。</p>
<h2 id="managed-平台跟團隊職責邊界">Managed 平台跟團隊職責邊界</h2>
<p>平台託管化（self-managed → managed）改變維運責任跟團隊精力的分配。本段聚焦團隊職責邊界；流量跟依賴的分段切換流程見 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移</a>、紅隊視角的攻擊面變動見 <a href="/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/#%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb%e6%9c%9f%e7%9a%84%e6%94%bb%e6%93%8a%e9%9d%a2%e8%ae%8a%e5%8b%95" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5 平台遷移期的攻擊面變動</a>、三者組合才完整。</p>
<p>Platform team 從「維持 Kubernetes 跑起來」轉向「定義 release flow、observability convention、cost governance」。managed 平台採用後第一個治理動作是顯式重新定義職責邊界、讓 platform team 從 cluster ops 轉到 release flow / observability convention / cost governance。重新定義缺位、組織轉型紅利容易被誤判為純技術升級。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro Managed EKS 遷移</a>：揭露 1 個判讀（平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略）+ 3 條策略（先定義遷移後的平台責任邊界、自動化流程取代手動平台操作、incident 跟 release policy 接回平台治理）。對應 <a href="/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33 Maersk + Bosch Azure AKS</a>：揭露 Maersk 工程訴求引語「focus on things that makes the most business impact」、傳統產業 K8s 動機是治理一致性 + 釋放工程資源到業務功能（後者屬作者判讀）。以下基於通用工程知識展開。</p>
<p>managed 平台採用後的職責邊界重訂可以分四層：</p>
<ol>
<li><strong>Cluster 層</strong>：control plane 上游接管（API server、etcd、scheduler、controller-manager）、platform team 從 cluster ops 退到 cluster policy。CIS benchmark、network policy、admission controller 配置仍是 platform 責任。</li>
<li><strong>Cluster-internal 層</strong>：CNI、ingress controller、service mesh、cluster DNS、storage CSI 通常仍由 platform team own。這層是 managed 服務沒覆蓋的 grey zone、需要明確 ownership。</li>
<li><strong>Application 層</strong>：deployment、service、HPA、PDB 由 service team own、platform 提供 convention 跟 review process。</li>
<li><strong>跨層議題</strong>：cost governance、observability convention、release flow、incident response 是 platform / service / SRE / finance 跨層協作、需要 operating model 明確化。</li>
</ol>
<p>managed 採用後 day-1 治理項目有兩件事：明確界定 grey zone ownership（避免「以為 managed 服務什麼都管了」的心智模型）、把 platform team 心力從 cluster ops 轉到組織轉型紅利（release flow、observability convention、cost governance）。把重新定義職責當 day-2 議題、會錯失組織轉型紅利。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>平台選型前要先回答：</p>
<ol>
<li>哪些變更屬於 traffic，哪些屬於 config，哪些屬於 secret。</li>
<li>每種變更是否能分批、暫停與回退。</li>
<li>Discovery 失準時是否有可控 fallback。</li>
<li>Control plane 變更是否有 audit、owner 與 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 限制。</li>
<li>基礎元件升級是否有可重播流程跟回退窗口。</li>
<li>Managed 平台採用後團隊職責邊界是否重新定義。</li>
</ol>
<p>這些答案決定後續要比較 load balancer、service mesh、secret manager、service registry 或 deployment controller 的能力。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體平台文章要承接本篇的 traffic、config 與 control plane boundary。ELB、nginx、Envoy、service mesh、Consul、Kubernetes controller、secret manager 或 Terraform 的比較，要先分清它們是在資料面接流量、在控制面改規則，還是在設定面下發狀態。</p>
<p>若主問題是流量切換，後續文章要比較 routing rule、weight、health check、drain 與 rollback。若主問題是設定與 secret，後續文章要比較 rollout、audit、rotation 與相容窗口。若主問題是 control plane 風險，後續文章要比較 blast radius、approval、observability 與 incident decision log。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要把流量邊界接到實際 LB 合約，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要把 control plane 決策寫入事故流程，接著讀 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log and Write-back</a>。</p>
]]></content:encoded></item><item><title>5.C7 Airbnb：Istio 升級治理</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/</guid><description>&lt;p>這個案例的核心責任是把平台元件升級從一次性作業轉成可重播流程。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 在數十個 Kubernetes 叢集、數萬個 pod、數千個 VM 的規模下持續升級 Istio service mesh，峰值流量達數千萬 QPS。團隊累計完成 14 次成功的 Istio 升級。&lt;/p>
&lt;p>升級的核心挑戰是規模帶來的協同成本：無法逐一通知每個 workload team 進行升級配合，也無法同時監控所有 workload 的升級狀態。升級策略必須對 workload team 透明——workload 不需要改程式碼或調配置就能完成 proxy 版本切換。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>基礎平台元件升級若缺乏分批治理，會形成全域風險放大器。Istio 升級的影響面覆蓋所有跑 sidecar 的服務——一次壞的升級可以讓整個叢集的服務間通訊中斷。這個風險決定了升級策略必須是 canary 模式（小比例先行），而且 canary 的粒度要夠細（namespace 或 workload 級別），才能在問題擴大前攔截。&lt;/p>
&lt;p>另一個判讀是升級流程本身要版本化。第一次升級靠資深工程師手動操作可以成功，但這個知識留在個人經驗裡。第二次升級換了人就可能踩到不同的坑。把升級流程固定成可重播的 spec（升級計畫 → 執行 → 驗證 → 確認/回退），讓升級從「英雄行為」變成「例行操作」。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Canary upgrade model（兩版本並存）&lt;/strong>：採用 Istio 的 canary upgrade 機制，同時跑兩個版本的 Istiod。新版本的 sidecar proxy 跟對應版本的 control plane 配置一起原子部署，避免跨版本相容性問題。透過 revision label 決定每個 namespace 使用哪個版本的 Istiod。&lt;/li>
&lt;li>&lt;strong>自建工具解耦基礎設施更新與 workload 部署&lt;/strong>：團隊開發了 Krispr（mutation framework），在 CI 階段注入 Istio revision label，並在 admission 階段對超過兩週未部署的 pod 重新注入最新 label。這讓 workload 在正常部署流程中自動完成 proxy 升級，不需要額外操作。&lt;/li>
&lt;li>&lt;strong>rollouts.yml 定義升級批次與比例&lt;/strong>：用 spec 檔定義每個環境（staging / production）、每個 namespace pattern 的版本分佈（例如 staging 75% 舊版 / 25% 新版）。比例可以逐步調整——先 5% → 25% → 50% → 100%。每個批次有明確的觀測窗口與停損條件。&lt;/li>
&lt;li>&lt;strong>VM 升級用 mxrc controller&lt;/strong>：Kubernetes 外的 VM workload 用 mxrc controller 根據 rollouts.yml 更新 tag，遵守健康狀態檢查與可用性門檻。VM 的升級通常在兩週內透過自然輪替完成。&lt;/li>
&lt;li>&lt;strong>升級事件進 incident timeline&lt;/strong>：升級期間的短暫錯誤（proxy 重連、配置同步延遲）在事故 timeline 上標記為升級事件，避免被誤判成獨立事故。升級的決策紀錄用 incident decision log 格式，讓下次升級可以回溯上次的判斷依據。&lt;/li>
&lt;/ol>
&lt;h2 id="升級節奏的收斂">升級節奏的收斂&lt;/h2>
&lt;p>14 次升級的經驗讓升級流程逐步收斂。多數 workload 在正常 deployment 時自動完成 proxy 升級（因為 Krispr 在 admission 階段注入最新 revision）。沒有 regular deployment 的 workload 在四週內透過自然 pod cycling（node 維護、HPA 調整）完成升級。這個四週窗口是可接受的——超過四週未部署的 workload 通常也是低變動、低風險的。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>Istio 升級的回退是把 revision label 切回舊版本、讓 pod 在下次 restart 時重新注入舊版 sidecar。回退的風險在於回退期間新舊 proxy 混跑，traffic policy 可能不完全一致。穩定做法是先在小範圍驗證回退行為（一個 namespace），確認 traffic policy 一致性後再擴大回退範圍。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment&lt;/a> 看 rollout 節奏與 probe 設計。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程&lt;/a> 看通用升級框架。回 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&amp;#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.6 IC handoff&lt;/a> 看升級期事故的指揮交接。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是把平台元件升級從一次性作業轉成可重播流程。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 在數十個 Kubernetes 叢集、數萬個 pod、數千個 VM 的規模下持續升級 Istio service mesh，峰值流量達數千萬 QPS。團隊累計完成 14 次成功的 Istio 升級。</p>
<p>升級的核心挑戰是規模帶來的協同成本：無法逐一通知每個 workload team 進行升級配合，也無法同時監控所有 workload 的升級狀態。升級策略必須對 workload team 透明——workload 不需要改程式碼或調配置就能完成 proxy 版本切換。</p>
<h2 id="判讀">判讀</h2>
<p>基礎平台元件升級若缺乏分批治理，會形成全域風險放大器。Istio 升級的影響面覆蓋所有跑 sidecar 的服務——一次壞的升級可以讓整個叢集的服務間通訊中斷。這個風險決定了升級策略必須是 canary 模式（小比例先行），而且 canary 的粒度要夠細（namespace 或 workload 級別），才能在問題擴大前攔截。</p>
<p>另一個判讀是升級流程本身要版本化。第一次升級靠資深工程師手動操作可以成功，但這個知識留在個人經驗裡。第二次升級換了人就可能踩到不同的坑。把升級流程固定成可重播的 spec（升級計畫 → 執行 → 驗證 → 確認/回退），讓升級從「英雄行為」變成「例行操作」。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>Canary upgrade model（兩版本並存）</strong>：採用 Istio 的 canary upgrade 機制，同時跑兩個版本的 Istiod。新版本的 sidecar proxy 跟對應版本的 control plane 配置一起原子部署，避免跨版本相容性問題。透過 revision label 決定每個 namespace 使用哪個版本的 Istiod。</li>
<li><strong>自建工具解耦基礎設施更新與 workload 部署</strong>：團隊開發了 Krispr（mutation framework），在 CI 階段注入 Istio revision label，並在 admission 階段對超過兩週未部署的 pod 重新注入最新 label。這讓 workload 在正常部署流程中自動完成 proxy 升級，不需要額外操作。</li>
<li><strong>rollouts.yml 定義升級批次與比例</strong>：用 spec 檔定義每個環境（staging / production）、每個 namespace pattern 的版本分佈（例如 staging 75% 舊版 / 25% 新版）。比例可以逐步調整——先 5% → 25% → 50% → 100%。每個批次有明確的觀測窗口與停損條件。</li>
<li><strong>VM 升級用 mxrc controller</strong>：Kubernetes 外的 VM workload 用 mxrc controller 根據 rollouts.yml 更新 tag，遵守健康狀態檢查與可用性門檻。VM 的升級通常在兩週內透過自然輪替完成。</li>
<li><strong>升級事件進 incident timeline</strong>：升級期間的短暫錯誤（proxy 重連、配置同步延遲）在事故 timeline 上標記為升級事件，避免被誤判成獨立事故。升級的決策紀錄用 incident decision log 格式，讓下次升級可以回溯上次的判斷依據。</li>
</ol>
<h2 id="升級節奏的收斂">升級節奏的收斂</h2>
<p>14 次升級的經驗讓升級流程逐步收斂。多數 workload 在正常 deployment 時自動完成 proxy 升級（因為 Krispr 在 admission 階段注入最新 revision）。沒有 regular deployment 的 workload 在四週內透過自然 pod cycling（node 維護、HPA 調整）完成升級。這個四週窗口是可接受的——超過四週未部署的 workload 通常也是低變動、低風險的。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>Istio 升級的回退是把 revision label 切回舊版本、讓 pod 在下次 restart 時重新注入舊版 sidecar。回退的風險在於回退期間新舊 proxy 混跑，traffic policy 可能不完全一致。穩定做法是先在小範圍驗證回退行為（一個 namespace），確認 traffic policy 一致性後再擴大回退範圍。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment</a> 看 rollout 節奏與 probe 設計。回 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程</a> 看通用升級框架。回 <a href="/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.6 IC handoff</a> 看升級期事故的指揮交接。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://airbnb.tech/infrastructure/seamless-istio-upgrades-at-scale/">Seamless Istio Upgrades at Scale</a></li>
</ul>
]]></content:encoded></item><item><title>Terraform / OpenTofu</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/</guid><description>&lt;p>Terraform 是 HashiCorp 出品的 IaC 工具、承擔三個責任：declarative infrastructure 配置（HCL）、state-based reconciliation（plan → apply）、跨 provider 抽象（AWS / GCP / Azure / K8s / SaaS）。設計取捨偏向「state-driven + declarative + multi-cloud」、provider 生態最廣。2023 改 BSL 授權、社群 fork OpenTofu（Linux Foundation 託管、MPL 2.0）。&lt;/p>
&lt;p>對「跨雲基礎設施管理、團隊協作 IaC、需要 state + plan workflow」這條路徑、Terraform / OpenTofu 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 HCL config（resource / variable / output / module）&lt;/li>
&lt;li>設定 remote state（S3 + DynamoDB lock / Terraform Cloud）&lt;/li>
&lt;li>設計 module + workspace 結構&lt;/li>
&lt;li>跑 plan / apply / destroy 工作流 + GitOps&lt;/li>
&lt;li>評估 Terraform vs OpenTofu vs Pulumi vs Crossplane&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-terraform-跑起來">最短路徑：5 分鐘把 Terraform 跑起來&lt;/h2>





&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"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">brew install hashicorp/tap/terraform &lt;span class="c1"># 或 brew install opentofu&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 寫 main.tf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">terraform&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">required_providers&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n"> aws&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> { source&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> &amp;#34;hashicorp/aws&amp;#34;, version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;~&amp;gt; 5.0&amp;#34;&lt;/span> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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="n">provider &amp;#34;aws&amp;#34; { region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;us-east-1&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">resource &amp;#34;aws_s3_bucket&amp;#34; &amp;#34;demo&amp;#34; { bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;my-tf-demo-bucket&amp;#34;&lt;/span> }&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. init + plan + apply&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">terraform init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">terraform plan -out&lt;span class="o">=&lt;/span>plan.tfplan
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">terraform apply plan.tfplan&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="hcl-config-結構">HCL config 結構&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>provider / resource / data source / variable / output / locals&lt;/li>
&lt;li>terraform block（required_version / required_providers / backend）&lt;/li>
&lt;li>Module（reusable group of resources）&lt;/li>
&lt;li>對應指令：&lt;code>terraform fmt&lt;/code>、&lt;code>terraform validate&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="state-管理">State 管理&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Local state（terraform.tfstate）：dev / 學習用&lt;/li>
&lt;li>Remote state（S3 + DynamoDB lock / GCS / Terraform Cloud / Spacelift）&lt;/li>
&lt;li>State migration（terraform state mv / rm / import）&lt;/li>
&lt;li>State sensitive data 不入 git&lt;/li>
&lt;/ul>
&lt;h3 id="plan--apply-workflow">Plan / apply workflow&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Terraform 是 HashiCorp 出品的 IaC 工具、承擔三個責任：declarative infrastructure 配置（HCL）、state-based reconciliation（plan → apply）、跨 provider 抽象（AWS / GCP / Azure / K8s / SaaS）。設計取捨偏向「state-driven + declarative + multi-cloud」、provider 生態最廣。2023 改 BSL 授權、社群 fork OpenTofu（Linux Foundation 託管、MPL 2.0）。</p>
<p>對「跨雲基礎設施管理、團隊協作 IaC、需要 state + plan workflow」這條路徑、Terraform / OpenTofu 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 HCL config（resource / variable / output / module）</li>
<li>設定 remote state（S3 + DynamoDB lock / Terraform Cloud）</li>
<li>設計 module + workspace 結構</li>
<li>跑 plan / apply / destroy 工作流 + GitOps</li>
<li>評估 Terraform vs OpenTofu vs Pulumi vs Crossplane</li>
</ol>
<h2 id="最短路徑5-分鐘把-terraform-跑起來">最短路徑：5 分鐘把 Terraform 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew install hashicorp/tap/terraform   <span class="c1"># 或 brew install opentofu</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 2. 寫 main.tf
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">terraform</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">required_providers</span> {
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">    aws</span> <span class="o">=</span><span class="n"> { source</span> <span class="o">=</span><span class="n"> &#34;hashicorp/aws&#34;, version</span> <span class="o">=</span> <span class="s2">&#34;~&gt; 5.0&#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="n">provider &#34;aws&#34; { region</span> <span class="o">=</span> <span class="s2">&#34;us-east-1&#34;</span> }
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">resource &#34;aws_s3_bucket&#34; &#34;demo&#34; { bucket</span> <span class="o">=</span> <span class="s2">&#34;my-tf-demo-bucket&#34;</span> }</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 3. init + plan + apply</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform init
</span></span><span class="line"><span class="ln">3</span><span class="cl">terraform plan -out<span class="o">=</span>plan.tfplan
</span></span><span class="line"><span class="ln">4</span><span class="cl">terraform apply plan.tfplan</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="hcl-config-結構">HCL config 結構</h3>
<p>子議題：</p>
<ul>
<li>provider / resource / data source / variable / output / locals</li>
<li>terraform block（required_version / required_providers / backend）</li>
<li>Module（reusable group of resources）</li>
<li>對應指令：<code>terraform fmt</code>、<code>terraform validate</code></li>
</ul>
<h3 id="state-管理">State 管理</h3>
<p>子議題：</p>
<ul>
<li>Local state（terraform.tfstate）：dev / 學習用</li>
<li>Remote state（S3 + DynamoDB lock / GCS / Terraform Cloud / Spacelift）</li>
<li>State migration（terraform state mv / rm / import）</li>
<li>State sensitive data 不入 git</li>
</ul>
<h3 id="plan--apply-workflow">Plan / apply workflow</h3>
<p>子議題：</p>
<ul>
<li>terraform plan -out=plan.tfplan（凍結結果）</li>
<li>terraform apply plan.tfplan</li>
<li>Auto-approve（CI / CD）vs manual approve（critical）</li>
<li>對應 GitOps：Atlantis / Terraform Cloud / Spacelift</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="module-設計">Module 設計</h3>
<p>子議題：</p>
<ul>
<li>Module input / output</li>
<li>Module composition（root module → child module）</li>
<li>Public module registry（Terraform Registry / OpenTofu Registry）</li>
<li>Version pinning</li>
<li>對應 Terraform best practice</li>
</ul>
<h3 id="workspaces-vs-directory-layout">Workspaces vs directory layout</h3>
<p>子議題：</p>
<ul>
<li>Workspaces：同 module 多 instance（dev / staging / prod）</li>
<li>Directory：每 env 一個 directory</li>
<li>Workspaces 的局限（state 同 backend、env 共享 config）</li>
<li>選擇判讀：強隔離 → directory；快切換 → workspace</li>
</ul>
<h3 id="drift-detection">Drift detection</h3>
<p>子議題：</p>
<ul>
<li>Drift = 實際 infra ≠ Terraform state</li>
<li>偵測：<code>terraform plan</code> 跑出來有 diff</li>
<li>修法：Manual import / state pull / 修改 cloud directly + plan refresh</li>
<li>對應 自動化 drift detection（Atlantis / Driftctl）</li>
</ul>
<h3 id="terraform-vs-opentofu">Terraform vs OpenTofu</h3>
<p>子議題：</p>
<ul>
<li>2023 Terraform 改 BSL：Linux Foundation fork OpenTofu</li>
<li>OpenTofu 跟 Terraform 1.5 API 相容</li>
<li>之後分歧：OpenTofu 加 state encryption、provider iteration</li>
<li>遷移路徑：替換 binary、import 既有 state</li>
</ul>
<h3 id="provider-生態">Provider 生態</h3>
<p>子議題：</p>
<ul>
<li>AWS / Azure / GCP（cloud provider）</li>
<li>Kubernetes / Helm（K8s provider）</li>
<li>SaaS：Datadog / Pagerduty / Cloudflare / GitHub</li>
<li>Community provider vs official provider 品質差距</li>
</ul>
<h3 id="跟-crossplane--pulumi-對比">跟 Crossplane / Pulumi 對比</h3>
<p>子議題：</p>
<ul>
<li>Crossplane：K8s-native IaC（用 K8s CRD 管 cloud resource）</li>
<li>Pulumi：用通用語言（TS / Python / Go / C#）寫 IaC</li>
<li>選擇判讀：純 cloud infra → Terraform / OpenTofu；K8s-heavy → Crossplane；developer-first → Pulumi</li>
</ul>
<h3 id="terraform-cloud--spacelift--atlantis">Terraform Cloud / Spacelift / Atlantis</h3>
<p>子議題：</p>
<ul>
<li>Terraform Cloud（HashiCorp managed）：remote state + run + policy</li>
<li>Spacelift / env0：商業替代</li>
<li>Atlantis：OSS Pull Request automation</li>
<li>對應 GitOps for IaC</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="state-lock-stuck">State lock stuck</h3>
<p>操作原則：DynamoDB lock 沒釋放（process killed）。判讀 + 修法：<code>terraform force-unlock &lt;lock-id&gt;</code>（小心）。</p>
<h3 id="plan-diff-過大">Plan diff 過大</h3>
<p>操作原則：drift 累積 / provider 升級 / config 改太多。判讀：先看 plan output、再決定要不要 apply。</p>
<h3 id="provider-auth-fail">Provider auth fail</h3>
<p>操作原則：AWS / GCP credentials 沒設、過期、權限不夠。判讀：<code>AWS_PROFILE</code> / IAM role / GCP ADC 配置。</p>
<h3 id="module-version-衝突">Module version 衝突</h3>
<p>操作原則：root module 跟 child module 用不同 provider version。判讀：<code>terraform providers</code> 看 version constraint。</p>
<h3 id="apply-partial-failure">Apply partial failure</h3>
<p>操作原則：apply 中某 resource 失敗、state 一致性問題。判讀：state pull 看當前、可能要 import / state rm 修。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OSI-licensed Terraform</td>
          <td>OpenTofu（同模組）</td>
      </tr>
      <tr>
          <td>Imperative API</td>
          <td>Pulumi</td>
      </tr>
      <tr>
          <td>Cloud-specific（單一 cloud）</td>
          <td>CloudFormation / Azure Bicep / GCP Deployment Manager</td>
      </tr>
      <tr>
          <td>K8s-native IaC</td>
          <td>Crossplane</td>
      </tr>
      <tr>
          <td>Application config（不是 infra）</td>
          <td>Helm / Kustomize / cdk8s</td>
      </tr>
      <tr>
          <td>極小場景</td>
          <td>CLI / Cloud Shell（不用 IaC）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 HCL syntax reference</li>
<li>各 provider 完整 resource list</li>
<li>Terraform Cloud / Spacelift 商業 feature</li>
<li>Drift detection 工具細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Terraform 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>平台遷移期間舊 / 新叢集共通配置基線靠 IaC 表達、批次切流時 module 版本要凍結</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast EKS</a></td>
          <td>多團隊異質集群盤點後、用 module + workspace 把平台基線變成統一可審計的 IaC</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro EKS</a></td>
          <td>Managed EKS 後平台團隊把手動操作改成 IaC + GitOps、自動化取代手動操作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小型 CLI / 中型單 workspace / 大型 multi-workspace + Atlantis / Spacelift 治理</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Terraform 案例</strong>：HashiCorp Cloud 大客戶案例、OpenTofu fork 後企業遷移案例、Drift detection 治理案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 deployment platform</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a>（K8s provider）</li>
<li>下游能力：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 reliability</a>（IaC GitOps + release gate）</li>
</ul>
]]></content:encoded></item><item><title>模組七：跨節點與平台整合</title><link>https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/</guid><description>&lt;p>跨節點與平台整合的核心目標是把「單一 Go process 內的正確邊界」延伸到外部基礎設施。前六個模組先建立 goroutine lifecycle、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> 連線、runtime 診斷、事件邊界、測試與操作語意；本模組處理服務進入多節點、多資料來源、多觀測工具與部署平台後會出現的新責任。&lt;/p>
&lt;p>本模組已開始補成正文。章節先定義問題邊界與前置脈絡，再逐步補上 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a>、outbox、跨節點 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a>、observability、部署與可靠性驗證的實作語意；後續仍可依實戰需求繼續擴寫。&lt;/p>
&lt;h2 id="與-backend-教材的分工">與 Backend 教材的分工&lt;/h2>
&lt;p>本模組保留在 Go 進階篇，因為它要回答的是「Go 服務跨出單一 process 前，程式內部需要準備哪些 port、訊號、錯誤語意與測試合約」。具體資料庫、Redis、RabbitMQ、observability、Kubernetes 或 CI 平台操作，會放在跨語言的 &lt;a href="https://tarrragon.github.io/blog/backend/" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 服務實務指南&lt;/a>。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>承接問題&lt;/th>
 &lt;th>Backend 實作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/database-transactions/" data-link-title="7.1 資料庫 transaction 與 schema migration" data-link-desc="把 repository 邊界延伸到資料庫交易、migration 與一致性語意">7.1&lt;/a>&lt;/td>
 &lt;td>資料庫 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 與 schema &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a>&lt;/td>
 &lt;td>狀態邊界進入持久化層後如何維持一致&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">資料庫與持久化&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/outbox-idempotency/" data-link-title="7.2 Durable queue、outbox 與 idempotency" data-link-desc="設計跨 process 事件傳遞的可靠性與去重邊界">7.2&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">Durable queue&lt;/a>、outbox 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a>&lt;/td>
 &lt;td>事件跨 process 後如何避免遺失、重複與半成功&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">訊息佇列與事件傳遞&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/cross-node-websocket/" data-link-title="7.3 跨節點 WebSocket、presence 與重連協定" data-link-desc="把單一 server 的 WebSocket hub 擴展到多節點推送與連線狀態">7.3&lt;/a>&lt;/td>
 &lt;td>跨節點 WebSocket、presence 與重連協定&lt;/td>
 &lt;td>多台 server 如何管理訂閱、推送與連線狀態&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">快取與 Redis&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">訊息佇列&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/observability-pipeline/" data-link-title="7.4 Observability pipeline、metrics 與 tracing" data-link-desc="把 structured log、metric、trace 與 profile 組成可操作的診斷系統">7.4&lt;/a>&lt;/td>
 &lt;td>Observability pipeline、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 與 tracing&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、metric、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 如何組成可操作的診斷系統&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/deployment-contracts/" data-link-title="7.5 Kubernetes、systemd 與 load balancer 合約" data-link-desc="理解部署平台如何影響 Go 服務的 shutdown、health 與資源限制">7.5&lt;/a>&lt;/td>
 &lt;td>Kubernetes、systemd 與 load balancer 合約&lt;/td>
 &lt;td>部署平台如何影響 shutdown、health 與資源限制&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台與網路入口&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/reliability-pipeline/" data-link-title="7.6 CI、fuzz、load test 與 chaos testing" data-link-desc="把單元測試與整合測試擴展成服務可靠性驗證流程">7.6&lt;/a>&lt;/td>
 &lt;td>CI、fuzz、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test&lt;/a> 與 chaos testing&lt;/td>
 &lt;td>測試如何從單一行為擴展到系統可靠性&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="本模組和前面章節的關係">本模組和前面章節的關係&lt;/h2>
&lt;p>本模組適合在你已經理解單一 Go 服務的內部邊界後閱讀，用來補足生產環境常見的外部系統責任。&lt;/p></description><content:encoded><![CDATA[<p>跨節點與平台整合的核心目標是把「單一 Go process 內的正確邊界」延伸到外部基礎設施。前六個模組先建立 goroutine lifecycle、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 連線、runtime 診斷、事件邊界、測試與操作語意；本模組處理服務進入多節點、多資料來源、多觀測工具與部署平台後會出現的新責任。</p>
<p>本模組已開始補成正文。章節先定義問題邊界與前置脈絡，再逐步補上 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a>、outbox、跨節點 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a>、observability、部署與可靠性驗證的實作語意；後續仍可依實戰需求繼續擴寫。</p>
<h2 id="與-backend-教材的分工">與 Backend 教材的分工</h2>
<p>本模組保留在 Go 進階篇，因為它要回答的是「Go 服務跨出單一 process 前，程式內部需要準備哪些 port、訊號、錯誤語意與測試合約」。具體資料庫、Redis、RabbitMQ、observability、Kubernetes 或 CI 平台操作，會放在跨語言的 <a href="/blog/backend/" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 服務實務指南</a>。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>承接問題</th>
          <th>Backend 實作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/go-advanced/07-distributed-operations/database-transactions/" data-link-title="7.1 資料庫 transaction 與 schema migration" data-link-desc="把 repository 邊界延伸到資料庫交易、migration 與一致性語意">7.1</a></td>
          <td>資料庫 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 與 schema <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a></td>
          <td>狀態邊界進入持久化層後如何維持一致</td>
          <td><a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">資料庫與持久化</a></td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/07-distributed-operations/outbox-idempotency/" data-link-title="7.2 Durable queue、outbox 與 idempotency" data-link-desc="設計跨 process 事件傳遞的可靠性與去重邊界">7.2</a></td>
          <td><a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">Durable queue</a>、outbox 與 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a></td>
          <td>事件跨 process 後如何避免遺失、重複與半成功</td>
          <td><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">訊息佇列與事件傳遞</a></td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/07-distributed-operations/cross-node-websocket/" data-link-title="7.3 跨節點 WebSocket、presence 與重連協定" data-link-desc="把單一 server 的 WebSocket hub 擴展到多節點推送與連線狀態">7.3</a></td>
          <td>跨節點 WebSocket、presence 與重連協定</td>
          <td>多台 server 如何管理訂閱、推送與連線狀態</td>
          <td><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">快取與 Redis</a>、<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">訊息佇列</a></td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/07-distributed-operations/observability-pipeline/" data-link-title="7.4 Observability pipeline、metrics 與 tracing" data-link-desc="把 structured log、metric、trace 與 profile 組成可操作的診斷系統">7.4</a></td>
          <td>Observability pipeline、<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 與 tracing</td>
          <td><a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、metric、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 如何組成可操作的診斷系統</td>
          <td><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a></td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/07-distributed-operations/deployment-contracts/" data-link-title="7.5 Kubernetes、systemd 與 load balancer 合約" data-link-desc="理解部署平台如何影響 Go 服務的 shutdown、health 與資源限制">7.5</a></td>
          <td>Kubernetes、systemd 與 load balancer 合約</td>
          <td>部署平台如何影響 shutdown、health 與資源限制</td>
          <td><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台與網路入口</a></td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/07-distributed-operations/reliability-pipeline/" data-link-title="7.6 CI、fuzz、load test 與 chaos testing" data-link-desc="把單元測試與整合測試擴展成服務可靠性驗證流程">7.6</a></td>
          <td>CI、fuzz、<a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a> 與 chaos testing</td>
          <td>測試如何從單一行為擴展到系統可靠性</td>
          <td><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></td>
      </tr>
  </tbody>
</table>
<h2 id="本模組和前面章節的關係">本模組和前面章節的關係</h2>
<p>本模組適合在你已經理解單一 Go 服務的內部邊界後閱讀，用來補足生產環境常見的外部系統責任。</p>
<ul>
<li>事件與狀態邊界先讀 <a href="/blog/go-advanced/04-architecture-boundaries/" data-link-title="模組四：架構邊界與事件系統" data-link-desc="用事件驅動架構拆解事件來源、處理流程、狀態邊界與即時推送">模組四：架構邊界與事件系統</a>。</li>
<li>WebSocket lifecycle 先讀 <a href="/blog/go-advanced/02-networking-websocket/" data-link-title="模組二：WebSocket 服務架構" data-link-desc="WebSocket client lifecycle、heartbeat、訂閱路由與慢客戶端管理">模組二：WebSocket 服務架構</a>。</li>
<li>測試可靠性先讀 <a href="/blog/go-advanced/05-testing-reliability/" data-link-title="模組五：測試與可靠性" data-link-desc="時間控制、WebSocket integration test、race check 與 table-driven test">模組五：測試與可靠性</a>。</li>
<li>操作語意先讀 <a href="/blog/go-advanced/06-production-operations/" data-link-title="模組六：生產操作" data-link-desc="graceful shutdown、健康檢查、結構化日誌與 feature gate">模組六：生產操作</a>。</li>
</ul>
<h2 id="學習時間">學習時間</h2>
<p>目前已可作為第一輪正文閱讀，完整學習時間可隨後續擴寫再調整。</p>
]]></content:encoded></item><item><title>5.8 Deployment Rollout with Drain and Rollback（實作示範）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/</guid><description>&lt;p>Deployment rollout with drain and rollback 的核心責任是把版本、流量、連線、設定與回退條件拆成可驗證批次。這篇以 checkout service 為例，示範平台切換如何從 preflight、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary&lt;/a>、drain 到事故回退都保留一致證據。&lt;/p>
&lt;p>本篇以 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約&lt;/a> 為前置知識——rollout 批次、probe 對齊、drain contract 等概念在該兩篇定義，本篇直接操作化。lifecycle 狀態的完整定義見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a>。&lt;/p>
&lt;h2 id="服務路徑與切換責任">服務路徑與切換責任&lt;/h2>
&lt;p>這條路徑是 &lt;code>client -&amp;gt; load balancer -&amp;gt; checkout-api -&amp;gt; payment provider/order db/order event&lt;/code>。部署期間新舊版本會同時承接流量，核心風險在流量生命週期是否可收斂，image 替換本身反而是最可預測的部分。&lt;/p>
&lt;p>切換責任分三層：&lt;/p>
&lt;ol>
&lt;li>版本可啟動：container/runtime/config 可用。&lt;/li>
&lt;li>版本可接流量：readiness 與依賴狀態對齊。&lt;/li>
&lt;li>版本可退場：drain 與在途請求可收束。&lt;/li>
&lt;/ol>
&lt;h2 id="preflight先驗證可服務基線">Preflight：先驗證可服務基線&lt;/h2>
&lt;p>Preflight 的責任是把「可啟動」與「可服務」拆開驗證。最小檢查包含：&lt;/p>
&lt;ol>
&lt;li>image 與 runtime config 版本對齊。&lt;/li>
&lt;li>secret 已注入且權限正確。&lt;/li>
&lt;li>startup/readiness probe 能反映真實依賴狀態。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">load balancer contract&lt;/a> 參數與服務期望一致。&lt;/li>
&lt;li>service discovery 註冊與摘除路徑可用。&lt;/li>
&lt;/ol>
&lt;p>Preflight 失敗時不進 canary。先把失敗收斂在控制面，避免切流後才發現版本不可服務。&lt;/p>
&lt;h3 id="preflight-自動化">Preflight 自動化&lt;/h3>
&lt;p>手動 preflight 在低頻部署時可行，部署頻率上升後會成為瓶頸或被跳過。穩定做法是把 preflight 檢查嵌入 CI/CD pipeline 的 pre-deploy stage：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>image 與 config 版本對齊檢查&lt;/strong>：pipeline 比對即將部署的 image tag 與 ConfigMap / Secret 版本是否在相容矩陣內。版本矩陣可維護在 git（如 &lt;code>deploy/compat-matrix.yaml&lt;/code>），CI 自動比對。&lt;/li>
&lt;li>&lt;strong>infra drift detection&lt;/strong>：部署前用 IaC 工具（Terraform plan、Crossplane drift check）掃描目標環境的實際狀態是否跟宣告狀態一致。drift 存在時暫停部署——在已漂移的環境上部署新版本，會把漂移與版本變更的影響混在一起，事故時無法分辨根因。&lt;/li>
&lt;li>&lt;strong>probe 語意驗證&lt;/strong>：在 staging 環境對新版本觸發 startup → readiness → liveness 全流程，確認 probe 回應與依賴就緒條件吻合。這步抓的是 probe 設定退化（如 readiness endpoint 被改成永遠回 200）。&lt;/li>
&lt;li>&lt;strong>rollback 可行性驗證&lt;/strong>：確認舊版本 image 仍在 registry 且可拉取、舊版本 config 仍相容。rollback 能力在 preflight 階段驗證，比事故時才發現「舊版拉不到」代價低得多。&lt;/li>
&lt;/ol>
&lt;p>Preflight 自動化的產出是一份 go/no-go 報告，進入 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a> 作為放行依據。pipeline 中的 preflight stage 失敗應阻擋部署而非產生警告——可忽略的 preflight 等於沒有 preflight。&lt;/p>
&lt;h2 id="canary-batch-與-stop-condition">Canary Batch 與 Stop Condition&lt;/h2>
&lt;p>小流量先驗證新版本行為，再決定是否擴批——Canary 回答的是「這個版本值不值得擴大」。&lt;/p></description><content:encoded><![CDATA[<p>Deployment rollout with drain and rollback 的核心責任是把版本、流量、連線、設定與回退條件拆成可驗證批次。這篇以 checkout service 為例，示範平台切換如何從 preflight、<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a>、drain 到事故回退都保留一致證據。</p>
<p>本篇以 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a> 與 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a> 為前置知識——rollout 批次、probe 對齊、drain contract 等概念在該兩篇定義，本篇直接操作化。lifecycle 狀態的完整定義見 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
<h2 id="服務路徑與切換責任">服務路徑與切換責任</h2>
<p>這條路徑是 <code>client -&gt; load balancer -&gt; checkout-api -&gt; payment provider/order db/order event</code>。部署期間新舊版本會同時承接流量，核心風險在流量生命週期是否可收斂，image 替換本身反而是最可預測的部分。</p>
<p>切換責任分三層：</p>
<ol>
<li>版本可啟動：container/runtime/config 可用。</li>
<li>版本可接流量：readiness 與依賴狀態對齊。</li>
<li>版本可退場：drain 與在途請求可收束。</li>
</ol>
<h2 id="preflight先驗證可服務基線">Preflight：先驗證可服務基線</h2>
<p>Preflight 的責任是把「可啟動」與「可服務」拆開驗證。最小檢查包含：</p>
<ol>
<li>image 與 runtime config 版本對齊。</li>
<li>secret 已注入且權限正確。</li>
<li>startup/readiness probe 能反映真實依賴狀態。</li>
<li><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">load balancer contract</a> 參數與服務期望一致。</li>
<li>service discovery 註冊與摘除路徑可用。</li>
</ol>
<p>Preflight 失敗時不進 canary。先把失敗收斂在控制面，避免切流後才發現版本不可服務。</p>
<h3 id="preflight-自動化">Preflight 自動化</h3>
<p>手動 preflight 在低頻部署時可行，部署頻率上升後會成為瓶頸或被跳過。穩定做法是把 preflight 檢查嵌入 CI/CD pipeline 的 pre-deploy stage：</p>
<ol>
<li><strong>image 與 config 版本對齊檢查</strong>：pipeline 比對即將部署的 image tag 與 ConfigMap / Secret 版本是否在相容矩陣內。版本矩陣可維護在 git（如 <code>deploy/compat-matrix.yaml</code>），CI 自動比對。</li>
<li><strong>infra drift detection</strong>：部署前用 IaC 工具（Terraform plan、Crossplane drift check）掃描目標環境的實際狀態是否跟宣告狀態一致。drift 存在時暫停部署——在已漂移的環境上部署新版本，會把漂移與版本變更的影響混在一起，事故時無法分辨根因。</li>
<li><strong>probe 語意驗證</strong>：在 staging 環境對新版本觸發 startup → readiness → liveness 全流程，確認 probe 回應與依賴就緒條件吻合。這步抓的是 probe 設定退化（如 readiness endpoint 被改成永遠回 200）。</li>
<li><strong>rollback 可行性驗證</strong>：確認舊版本 image 仍在 registry 且可拉取、舊版本 config 仍相容。rollback 能力在 preflight 階段驗證，比事故時才發現「舊版拉不到」代價低得多。</li>
</ol>
<p>Preflight 自動化的產出是一份 go/no-go 報告，進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 作為放行依據。pipeline 中的 preflight stage 失敗應阻擋部署而非產生警告——可忽略的 preflight 等於沒有 preflight。</p>
<h2 id="canary-batch-與-stop-condition">Canary Batch 與 Stop Condition</h2>
<p>小流量先驗證新版本行為，再決定是否擴批——Canary 回答的是「這個版本值不值得擴大」。</p>
<table>
  <thead>
      <tr>
          <th>批次階段</th>
          <th>判讀重點</th>
          <th>停損條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1-5%</td>
          <td>per-version error rate、p95/p99 latency</td>
          <td>錯誤率高於基線、延遲持續惡化</td>
      </tr>
      <tr>
          <td>10-25%</td>
          <td>payment dependency timeout、fallback 比例</td>
          <td>依賴 timeout 連續超門檻</td>
      </tr>
      <tr>
          <td>50%</td>
          <td>drain 成功率、reconnect 波形、下游事件完整性</td>
          <td>drain 未完成或 reconnect storm</td>
      </tr>
      <tr>
          <td>100% 前</td>
          <td>新舊版本差異是否收斂、rollback 可行性</td>
          <td>仍需依賴舊版本特殊路徑</td>
      </tr>
  </tbody>
</table>
<p>canary 判讀要維持 per-version 視角。只看整體服務平均值會掩蓋新版本局部退化。</p>
<h2 id="traffic--drain把退場變成可驗證流程">Traffic / Drain：把退場變成可驗證流程</h2>
<p>Drain 的責任是讓舊版本在下線前完成在途請求，不讓 rollout 把短暫切換放大成用戶錯誤。</p>
<p>退場順序：</p>
<ol>
<li>舊實例 readiness 先轉 <code>not-ready</code> 停接新流量。</li>
<li>保留 drain 窗口完成 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request。</li>
<li>確認連線數下降到門檻後再終止進程。</li>
<li>驗證無異常 reconnect 尖峰再進下一批。</li>
</ol>
<p>Drain 條件的完整 workload 分類回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>，本段以 checkout service 為例：短 API 的 <a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 窗口可短，長輪詢與 webhook callback 要更保守。</p>
<h2 id="rollback-compatibility">Rollback Compatibility</h2>
<p>舊版本回來時仍可運作，是 rollback 能成立的前提——回退如果變成第二次故障，就失去了回退的工程價值。</p>
<p>要先驗證四個相容面：</p>
<ol>
<li>config 相容：新設定不會讓舊版啟動失敗。</li>
<li>schema 相容：資料結構仍可被舊版讀取。</li>
<li>cache key 相容：舊版可讀新快取或有 fallback。</li>
<li>event schema 相容：舊版 consumer 不會因新事件欄位崩潰。</li>
</ol>
<p>若這四項未完成，所謂 rollback 只會停在「版本回切」，無法恢復服務正確性。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>每一批切換要可被判讀、可被追責、可被回放——部署 evidence 支撐這三個條件。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>deployment logs、LB metrics、service metrics、dependency logs</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>每批 rollout/drain 觀察窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>per-version error、latency、5xx、timeout、drain completion</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>platform owner、checkout owner、SRE on-call</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>指標延遲、分區覆蓋、log 掉點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>confirmed / suspected / needs follow-up</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>尚未覆蓋長連線場景、低流量區域樣本不足</td>
      </tr>
  </tbody>
</table>
<p>這份 evidence 要對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Release gate 的責任是決定下一批切換與是否凍結 rollout，不是報告「目前看起來正常」。</p>
<table>
  <thead>
      <tr>
          <th>Gate 欄位</th>
          <th>最小內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a></td>
          <td>放行下一批、維持 canary、freeze rollout、rollback version</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td>per-version SLI、dependency timeout、drain completion</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>error <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、reconnect storm、drain 逾時</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>可回切時間、舊版可服務窗口、config 回退窗口</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>release owner、platform on-call</td>
      </tr>
  </tbody>
</table>
<p>這組欄位要對齊 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>freeze rollout、rollback version、隔離 region、延長 drain 都屬事故決策，需寫入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。涉及流量規則 / <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> 設定推送的決策、見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7</a> 跟 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log</a>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T15:06:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;freeze rollout at 25% and rollback one region&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;new version timeout to payment provider increased in ap-northeast&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">checkout_error_rate_by_version_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">payment_timeout_ratio_by_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">release-incident-commander</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;contain customer impact and restore baseline success rate&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;timeout ratio does not recover after rollback batch completes&#34;</span></span></span></code></pre></div><h2 id="case-write-back-與邊界">Case Write-back 與邊界</h2>
<p>這篇回寫對齊 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>、<a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift</a> 與 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera</a>：前者看切換失序，後兩者看遷移路徑與回退策略。preflight / canary / drain 各階段的生命週期定義回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
<p>這篇不處理 schema migration 本身、cache stampede 或 queue replay。若核心風險在資料正式狀態、快取回源或事件恢復，路由到 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>、<a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a> 或 <a href="/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/" data-link-title="3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）" data-link-desc="以 order_created consumer 示範 queue 路徑如何交付 idempotency evidence、DLQ handling、replay runbook 與 incident decision log。">3.8 Queue Consumer Retry 與 Replay Handoff</a>。</p>
]]></content:encoded></item><item><title>Traefik</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/traefik/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/traefik/</guid><description>&lt;p>Traefik 是 cloud-native reverse proxy / ingress、承擔三個責任：auto-discovery（從 Docker / K8s / Consul / file 自動發現 backend）、dynamic config（不 reload、即時更新）、ACME 自動 TLS（Let&amp;rsquo;s Encrypt 整合）。設計取捨偏向「cloud-native 簡潔 + auto-discovery 為核心 + middleware chain extensibility」、適合 Docker / K8s 中小規模、大規模 / 複雜 traffic management 跟 nginx / envoy 比相對弱。&lt;/p>
&lt;p>對「Docker / K8s ingress、需要 auto-discovery、ACME 自動 TLS、配置簡潔」這條路徑、Traefik 是 cloud-native first 選擇。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Traefik 到 Docker / K8s&lt;/li>
&lt;li>配置 dynamic provider（labels / annotations / CRD / file）&lt;/li>
&lt;li>配置 ACME 自動 TLS&lt;/li>
&lt;li>設計 middleware chain（auth / rate limit / circuit breaker）&lt;/li>
&lt;li>評估 Traefik vs nginx vs Envoy 的選用&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-traefik-跑起來">最短路徑：5 分鐘把 Traefik 跑起來&lt;/h2>





&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"># 1. Docker 跑 Traefik + dashboard&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d -p 80:80 -p 8080:8080 &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> -v /var/run/docker.sock:/var/run/docker.sock &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> traefik:v3 --api.insecure&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> --providers.docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 用 docker label 配置 routing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">docker run -d --label &lt;span class="s2">&amp;#34;traefik.http.routers.demo.rule=Host(\`demo.local\`)&amp;#34;&lt;/span> nginx
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 訪 dashboard 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">curl -s http://localhost:8080/api/http/routers &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.[].rule&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="provider-auto-discovery">Provider auto-discovery&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Docker provider：從 container labels 讀 config&lt;/li>
&lt;li>Kubernetes Ingress provider：從 Ingress resource&lt;/li>
&lt;li>Kubernetes CRD provider：Traefik IngressRoute CRD&lt;/li>
&lt;li>Consul / Etcd provider：從 KV store&lt;/li>
&lt;li>File provider：YAML / TOML 靜態 file&lt;/li>
&lt;/ul>
&lt;h3 id="ingressroutek8s-crd">IngressRoute（K8s CRD）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Traefik CRD：IngressRoute / Middleware / TLSOption / ServersTransport&lt;/li>
&lt;li>比 Ingress 表達力強（middleware chain / TLS option / multi-protocol）&lt;/li>
&lt;li>跟 Gateway API 對比&lt;/li>
&lt;/ul>
&lt;h3 id="middleware-chain">Middleware chain&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Traefik 是 cloud-native reverse proxy / ingress、承擔三個責任：auto-discovery（從 Docker / K8s / Consul / file 自動發現 backend）、dynamic config（不 reload、即時更新）、ACME 自動 TLS（Let&rsquo;s Encrypt 整合）。設計取捨偏向「cloud-native 簡潔 + auto-discovery 為核心 + middleware chain extensibility」、適合 Docker / K8s 中小規模、大規模 / 複雜 traffic management 跟 nginx / envoy 比相對弱。</p>
<p>對「Docker / K8s ingress、需要 auto-discovery、ACME 自動 TLS、配置簡潔」這條路徑、Traefik 是 cloud-native first 選擇。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Traefik 到 Docker / K8s</li>
<li>配置 dynamic provider（labels / annotations / CRD / file）</li>
<li>配置 ACME 自動 TLS</li>
<li>設計 middleware chain（auth / rate limit / circuit breaker）</li>
<li>評估 Traefik vs nginx vs Envoy 的選用</li>
</ol>
<h2 id="最短路徑5-分鐘把-traefik-跑起來">最短路徑：5 分鐘把 Traefik 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. Docker 跑 Traefik + dashboard</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d -p 80:80 -p 8080:8080 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v /var/run/docker.sock:/var/run/docker.sock <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  traefik:v3 --api.insecure<span class="o">=</span><span class="nb">true</span> --providers.docker
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 2. 用 docker label 配置 routing</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">docker run -d --label <span class="s2">&#34;traefik.http.routers.demo.rule=Host(\`demo.local\`)&#34;</span> nginx
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 3. 訪 dashboard 驗證</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">curl -s http://localhost:8080/api/http/routers <span class="p">|</span> jq <span class="s1">&#39;.[].rule&#39;</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="provider-auto-discovery">Provider auto-discovery</h3>
<p>子議題：</p>
<ul>
<li>Docker provider：從 container labels 讀 config</li>
<li>Kubernetes Ingress provider：從 Ingress resource</li>
<li>Kubernetes CRD provider：Traefik IngressRoute CRD</li>
<li>Consul / Etcd provider：從 KV store</li>
<li>File provider：YAML / TOML 靜態 file</li>
</ul>
<h3 id="ingressroutek8s-crd">IngressRoute（K8s CRD）</h3>
<p>子議題：</p>
<ul>
<li>Traefik CRD：IngressRoute / Middleware / TLSOption / ServersTransport</li>
<li>比 Ingress 表達力強（middleware chain / TLS option / multi-protocol）</li>
<li>跟 Gateway API 對比</li>
</ul>
<h3 id="middleware-chain">Middleware chain</h3>
<p>子議題：</p>
<ul>
<li>內建 middleware：headers / rate limit / basic auth / forward auth / retry / circuit breaker / compress / IP whitelist</li>
<li>自訂 middleware：plugin（Yaegi-based）</li>
<li>順序：定義 middleware → 在 router 引用</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="acme-自動-tls">ACME 自動 TLS</h3>
<p>子議題：</p>
<ul>
<li>Let&rsquo;s Encrypt 整合（自動憑證 + 續期）</li>
<li>DNS challenge（適合 wildcard）vs HTTP challenge（適合單 domain）</li>
<li>多 resolver 配置（staging / production / 不同 CA）</li>
<li>對應 ACME storage（local / KV / Traefik Hub）</li>
</ul>
<h3 id="provider-weight--priority">Provider weight / priority</h3>
<p>子議題：</p>
<ul>
<li>多 provider 同時跑、config 來源衝突處理</li>
<li>Provider 優先順序</li>
<li>對應 dynamic config debug</li>
</ul>
<h3 id="traefik-hubmanaged">Traefik Hub（managed）</h3>
<p>子議題：</p>
<ul>
<li>Traefik Hub：商業 managed control plane</li>
<li>適合：跨 cluster 統一管理 / API Gateway portal</li>
<li>跟 self-host Traefik 對比</li>
</ul>
<h3 id="跟-nginx--envoy-對比">跟 nginx / Envoy 對比</h3>
<p>子議題：</p>
<ul>
<li>Traefik 強：cloud-native auto-discovery、配置簡潔</li>
<li>nginx 強：穩定 + 配置控制力 + 大量 community recipe</li>
<li>Envoy 強：xDS dynamic config、advanced traffic management</li>
<li>選型判讀：Docker / K8s 小中規模 → Traefik；複雜 traffic → Envoy；標準 HTTP → nginx</li>
</ul>
<h3 id="plugin-機制yaegi">Plugin 機制（Yaegi）</h3>
<p>子議題：</p>
<ul>
<li>Traefik plugins 用 Yaegi（Go interpreter）跑、不需 recompile</li>
<li>Plugin catalog（社群 + 官方）</li>
<li>適合：客戶 auth / metric / transformation 小邏輯</li>
<li>對應 Envoy WASM extension 對比</li>
</ul>
<h3 id="multi-protocol">Multi-protocol</h3>
<p>子議題：</p>
<ul>
<li>HTTP / HTTPS / TCP / UDP</li>
<li>gRPC（HTTP/2）原生支援</li>
<li>WebSocket sticky session</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="service-沒被發現">Service 沒被發現</h3>
<p>操作原則：先看 provider 是否啟用、再看 label / annotation / CRD 配置。</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">curl -s http://localhost:8080/api/http/services <span class="p">|</span> jq <span class="s1">&#39;.[].name&#39;</span></span></span></code></pre></div><h3 id="route-衝突">Route 衝突</h3>
<p>操作原則：兩個 router 同 rule，看 priority 排序。判讀：dashboard 看 router list。</p>
<h3 id="acme-rate-limit">ACME rate limit</h3>
<p>操作原則：Let&rsquo;s Encrypt 有 rate limit、staging environment 先測再切 production。</p>
<h3 id="middleware-chain-順序錯">Middleware chain 順序錯</h3>
<p>操作原則：middleware 順序影響行為（auth before rate limit vs after）。判讀：dashboard 看 middleware order。</p>
<h3 id="dashboard-連不上">Dashboard 連不上</h3>
<p>操作原則：dashboard 預設 8080、需要 entrypoint 配置。判讀：traefik.yml + entrypoints 設定。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>配置控制力 / 大量 community 模板</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a></td>
      </tr>
      <tr>
          <td>Advanced traffic / xDS</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a></td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>Istio / Linkerd / Consul Connect</td>
      </tr>
      <tr>
          <td>Gateway API standard</td>
          <td>Envoy Gateway / Contour</td>
      </tr>
      <tr>
          <td>純 dev / local</td>
          <td>Docker Compose + direct port mapping</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Traefik plugin 開發</li>
<li>Yaegi Go interpreter 細節</li>
<li>Traefik Hub 商業細節</li>
<li>各 cloud provider 整合差異</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Traefik 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>Traefik auto-discovery 在 service 下線時、要靠 health check + grace period 等價 drain</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>Docker / K8s 中小規模選 Traefik 簡潔、大規模通常升階到 Envoy / ingress-nginx 或 mesh</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Traefik 案例</strong>：Traefik Labs customer story、IngressRoute CRD 大規模採用、Traefik Hub 早期 adopter。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a>、<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></li>
<li>下游能力：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes vendor 頁</a></li>
</ul>
]]></content:encoded></item><item><title>5.9 邊緣分發與靜態資源（CDN / Origin Protection）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/</guid><description>&lt;p>邊緣分發的核心責任是把靜態與半靜態內容放到離使用者最近的網路節點，讓 origin 不必為每一筆讀取請求承擔流量與延遲。CDN 屬於部署平台的網路入口層，跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 模組的應用層快取&lt;/a> 是不同責任：CDN 解決「請求是否需要進到應用程式」，應用層快取解決「應用程式如何降低資料層讀寫成本」。這個邊界清楚後，origin 保護策略與快取一致性設計才能各自展開。&lt;/p>
&lt;h2 id="三層快取的責任分工">三層快取的責任分工&lt;/h2>
&lt;p>CDN、應用層快取與資料層快取串成一條快取分層。每一層各有自己的 freshness 模型、失效路徑與失敗代價，需要各自設計策略。&lt;/p>
&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;/td>
 &lt;td>CDN edge node、browser cache&lt;/td>
 &lt;td>降低跨網延遲、保護 origin 流量&lt;/td>
 &lt;td>全球節點 purge&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>應用層&lt;/td>
 &lt;td>Redis、in-memory cache、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside&lt;/a>&lt;/td>
 &lt;td>降低資料層查詢成本&lt;/td>
 &lt;td>區域 cluster purge&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料層快取&lt;/td>
 &lt;td>DB buffer pool、query cache&lt;/td>
 &lt;td>降低硬碟 I/O&lt;/td>
 &lt;td>內部自動管理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>讀者實作時要先判斷需求屬於哪一層。把使用者頭像、商品圖片、活動 banner 放邊緣層；把熱門商品價格、會員等級放應用層；DB 自身的 buffer pool 留給資料庫引擎管理。混用會造成失效路徑互相覆蓋，事故時難以判斷快取漂移來自哪一層。&lt;/p>
&lt;h2 id="origin-protection-的設計責任">Origin Protection 的設計責任&lt;/h2>
&lt;p>CDN 在規模成長路徑上承擔 origin protection。當 KOL 引流或熱門活動同秒帶入大量請求時，沒有邊緣層遮蔽，origin 的應用伺服器、API gateway 與資料庫會被同步擊穿。邊緣層的責任是讓 origin 流量曲線跟使用者請求曲線解耦。&lt;/p>
&lt;p>origin protection 的核心策略包含三個方向：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>cache hit ratio 優化&lt;/strong>：把高頻、可共用的內容做成可快取資源（含正確的 cache-control header、ETag 跟 vary 設計）。命中率每提升 10 個百分點，origin 流量幾乎等比例下降。&lt;/li>
&lt;li>&lt;strong>回源行為控制&lt;/strong>：edge 沒命中時用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">Cache Stampede&lt;/a> 保護機制（origin shield 是 CDN 內部多一層中央節點集中回源、coalescing / request collapsing 把同時打進來的 N 個請求合併成一次 origin 呼叫）、避免擊穿。&lt;/li>
&lt;li>&lt;strong>failure fallback&lt;/strong>：origin 不健康時、edge 可以回傳舊版本（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">stale-while-revalidate&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">stale-if-error&lt;/a>）、避免使用者直接看到 5xx。代價是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">Stale Data&lt;/a> 風險暫時提高、需要在 freshness budget 內。&lt;/li>
&lt;/ol>
&lt;p>Origin shield 跟 request coalescing 常被混為一談，兩者解決的問題不同。Origin shield 在 CDN 內部插入一層中央節點——全球 edge POP 的 cache miss 先集中到 shield 節點，shield 再向 origin 回源；它解決的是「N 個 edge POP 同時 miss 變成 N 次 origin 請求」的扇出放大。Request coalescing（也叫 request collapsing）在單一節點內把同時到達的多個相同請求合併成一次 origin 呼叫；它解決的是「同一個 edge POP 在同一毫秒收到 1000 個相同請求」的並發放大。兩者是不同層級的保護——shield 跨節點收斂、coalescing 單節點收斂——可以同時啟用形成兩層防線。&lt;/p>
&lt;p>這三項決定了「能不能撐住高峰」。三項做齊才能形成保護網；缺項時邊緣層僅能發揮降低延遲的效果。&lt;/p>
&lt;h2 id="cacheable-vs-non-cacheable-的判讀">Cacheable vs Non-Cacheable 的判讀&lt;/h2>
&lt;p>CDN 適合承接的資源有明確判讀條件：對所有使用者一致、且可容忍短暫舊版。符合這兩個條件的資源放邊緣層收益最高，不符合的留在應用層或 origin 處理。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資源類型&lt;/th>
 &lt;th>適合放 CDN？&lt;/th>
 &lt;th>判讀理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>靜態 asset（JS/CSS）&lt;/td>
 &lt;td>適合&lt;/td>
 &lt;td>內容與使用者無關，hash 命名後可長期快取&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>圖片、影片&lt;/td>
 &lt;td>適合&lt;/td>
 &lt;td>公開資源，跨使用者共用，命中率高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>商品頁、活動頁&lt;/td>
 &lt;td>條件適合&lt;/td>
 &lt;td>對未登入者一致；對登入者需要分版本或退到應用層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訂單頁、會員中心&lt;/td>
 &lt;td>不適合&lt;/td>
 &lt;td>跟特定使用者綁定，邊緣層無法共用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>個人化推薦&lt;/td>
 &lt;td>不適合&lt;/td>
 &lt;td>每個請求結果不同，命中率近於零&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入 API&lt;/td>
 &lt;td>不適合&lt;/td>
 &lt;td>邊緣層不該攔截狀態改變&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表覆蓋傳統靜態 / 動態二分情境。邊緣層演化出來的中間態超出表格範圍 — 包含 API responses with short TTL（GET、idempotent）、SSR / SSG 混合頁、signed URL / per-user 私有 asset（CloudFront / Cloudflare 可帶簽章對特定 user 快取）、i18n / 地理變體用 Vary header 處理跨 locale 共用、以及 edge personalization / edge compute（Cloudflare Workers、Lambda@Edge、Akamai EdgeWorkers）。進入這層要評估 edge compute 成本與 cache key 設計複雜度、不是簡單套表決定。&lt;/p></description><content:encoded><![CDATA[<p>邊緣分發的核心責任是把靜態與半靜態內容放到離使用者最近的網路節點，讓 origin 不必為每一筆讀取請求承擔流量與延遲。CDN 屬於部署平台的網路入口層，跟 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 模組的應用層快取</a> 是不同責任：CDN 解決「請求是否需要進到應用程式」，應用層快取解決「應用程式如何降低資料層讀寫成本」。這個邊界清楚後，origin 保護策略與快取一致性設計才能各自展開。</p>
<h2 id="三層快取的責任分工">三層快取的責任分工</h2>
<p>CDN、應用層快取與資料層快取串成一條快取分層。每一層各有自己的 freshness 模型、失效路徑與失敗代價，需要各自設計策略。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>主要載體</th>
          <th>主要責任</th>
          <th>失效成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>邊緣層</td>
          <td>CDN edge node、browser cache</td>
          <td>降低跨網延遲、保護 origin 流量</td>
          <td>全球節點 purge</td>
      </tr>
      <tr>
          <td>應用層</td>
          <td>Redis、in-memory cache、<a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside</a></td>
          <td>降低資料層查詢成本</td>
          <td>區域 cluster purge</td>
      </tr>
      <tr>
          <td>資料層快取</td>
          <td>DB buffer pool、query cache</td>
          <td>降低硬碟 I/O</td>
          <td>內部自動管理</td>
      </tr>
  </tbody>
</table>
<p>讀者實作時要先判斷需求屬於哪一層。把使用者頭像、商品圖片、活動 banner 放邊緣層；把熱門商品價格、會員等級放應用層；DB 自身的 buffer pool 留給資料庫引擎管理。混用會造成失效路徑互相覆蓋，事故時難以判斷快取漂移來自哪一層。</p>
<h2 id="origin-protection-的設計責任">Origin Protection 的設計責任</h2>
<p>CDN 在規模成長路徑上承擔 origin protection。當 KOL 引流或熱門活動同秒帶入大量請求時，沒有邊緣層遮蔽，origin 的應用伺服器、API gateway 與資料庫會被同步擊穿。邊緣層的責任是讓 origin 流量曲線跟使用者請求曲線解耦。</p>
<p>origin protection 的核心策略包含三個方向：</p>
<ol>
<li><strong>cache hit ratio 優化</strong>：把高頻、可共用的內容做成可快取資源（含正確的 cache-control header、ETag 跟 vary 設計）。命中率每提升 10 個百分點，origin 流量幾乎等比例下降。</li>
<li><strong>回源行為控制</strong>：edge 沒命中時用 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">Cache Stampede</a> 保護機制（origin shield 是 CDN 內部多一層中央節點集中回源、coalescing / request collapsing 把同時打進來的 N 個請求合併成一次 origin 呼叫）、避免擊穿。</li>
<li><strong>failure fallback</strong>：origin 不健康時、edge 可以回傳舊版本（<a href="/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">stale-while-revalidate</a> / <a href="/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">stale-if-error</a>）、避免使用者直接看到 5xx。代價是 <a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">Stale Data</a> 風險暫時提高、需要在 freshness budget 內。</li>
</ol>
<p>Origin shield 跟 request coalescing 常被混為一談，兩者解決的問題不同。Origin shield 在 CDN 內部插入一層中央節點——全球 edge POP 的 cache miss 先集中到 shield 節點，shield 再向 origin 回源；它解決的是「N 個 edge POP 同時 miss 變成 N 次 origin 請求」的扇出放大。Request coalescing（也叫 request collapsing）在單一節點內把同時到達的多個相同請求合併成一次 origin 呼叫；它解決的是「同一個 edge POP 在同一毫秒收到 1000 個相同請求」的並發放大。兩者是不同層級的保護——shield 跨節點收斂、coalescing 單節點收斂——可以同時啟用形成兩層防線。</p>
<p>這三項決定了「能不能撐住高峰」。三項做齊才能形成保護網；缺項時邊緣層僅能發揮降低延遲的效果。</p>
<h2 id="cacheable-vs-non-cacheable-的判讀">Cacheable vs Non-Cacheable 的判讀</h2>
<p>CDN 適合承接的資源有明確判讀條件：對所有使用者一致、且可容忍短暫舊版。符合這兩個條件的資源放邊緣層收益最高，不符合的留在應用層或 origin 處理。</p>
<table>
  <thead>
      <tr>
          <th>資源類型</th>
          <th>適合放 CDN？</th>
          <th>判讀理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>靜態 asset（JS/CSS）</td>
          <td>適合</td>
          <td>內容與使用者無關，hash 命名後可長期快取</td>
      </tr>
      <tr>
          <td>圖片、影片</td>
          <td>適合</td>
          <td>公開資源，跨使用者共用，命中率高</td>
      </tr>
      <tr>
          <td>商品頁、活動頁</td>
          <td>條件適合</td>
          <td>對未登入者一致；對登入者需要分版本或退到應用層</td>
      </tr>
      <tr>
          <td>訂單頁、會員中心</td>
          <td>不適合</td>
          <td>跟特定使用者綁定，邊緣層無法共用</td>
      </tr>
      <tr>
          <td>個人化推薦</td>
          <td>不適合</td>
          <td>每個請求結果不同，命中率近於零</td>
      </tr>
      <tr>
          <td>寫入 API</td>
          <td>不適合</td>
          <td>邊緣層不該攔截狀態改變</td>
      </tr>
  </tbody>
</table>
<p>這張表覆蓋傳統靜態 / 動態二分情境。邊緣層演化出來的中間態超出表格範圍 — 包含 API responses with short TTL（GET、idempotent）、SSR / SSG 混合頁、signed URL / per-user 私有 asset（CloudFront / Cloudflare 可帶簽章對特定 user 快取）、i18n / 地理變體用 Vary header 處理跨 locale 共用、以及 edge personalization / edge compute（Cloudflare Workers、Lambda@Edge、Akamai EdgeWorkers）。進入這層要評估 edge compute 成本與 cache key 設計複雜度、不是簡單套表決定。</p>
<p>判讀後仍要再對齊 freshness：商品價格在限時活動期間每 5 分鐘改一次，10 分鐘 TTL 就會出現超賣或顯示差價。這類情境要把價格放應用層快取、頁面結構放 CDN，整頁邊緣化會超出 freshness budget。</p>
<h2 id="purge-與-invalidation-的操作模型">Purge 與 Invalidation 的操作模型</h2>
<p>CDN 的 <a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">Cache Invalidation</a> 跟應用層的失效路徑不一樣：應用層 purge 在自家 cluster 內可控，CDN purge 要等全球節點同步。傳統 origin-pull CDN 的全球 purge 需要數秒到數十秒；現代 push-based CDN（Cloudflare、Fastly 等）的 instant purge 在 150ms 級別、語意接近同步、但這條能力依 vendor 而異、要事前驗證。</p>
<p>操作上的三種策略各有適用場景：</p>
<ul>
<li><strong>TTL 自然過期</strong>：適合內容變動慢、不需要立即生效的資源。優點是不依賴 purge API，缺點是無法應對緊急下架。搭配 stale-while-revalidate 後可以兼顧低 origin 壓力與最終新鮮度、是現代 default 而非「弱版本」。</li>
<li><strong>顯式 purge</strong>：適合內容變動時要立刻生效的場景（價格更新、文章下架、合規移除）。要把 purge 列入發布流程，事故期能在分鐘內收回錯誤內容。</li>
<li><strong>版本化路徑</strong>：適合 JS/CSS 等可永久快取的資源。檔名含 hash（<code>app.a3f1b2.js</code>），新版本上線時直接換路徑、舊版本自然失效。這是命中率最高的策略，因為可以設定 <code>max-age=31536000, immutable</code>。</li>
</ul>
<p>這三種策略以 origin pull 模型為主、是基底但不窮盡。現代 CDN 還有兩種重要策略需要展開。</p>
<h3 id="tag-based-purge-的操作模型">Tag-based Purge 的操作模型</h3>
<p><a href="/blog/backend/knowledge-cards/cache-tag-purge/" data-link-title="Cache Tag Purge" data-link-desc="CDN / cache 用 tag / surrogate key 批量失效多個關聯資源">Tag-based / surrogate-key purge</a>（Fastly surrogate key、Cloudflare cache tag、Akamai cache tag）是大型內容系統的事實標準。它解決的核心問題是「一個業務事件需要同時失效多個 URL」——商品下架要同時 purge 商品頁、商品圖、搜尋結果頁中含該商品的快取。</p>
<p>操作流程分三步：</p>
<ol>
<li><strong>打 tag</strong>：origin 在 response header 中標記 tag（如 <code>Surrogate-Key: product-123 category-electronics</code>）。CDN 存快取時同時建立 tag → URL 的反向索引。</li>
<li><strong>按 tag purge</strong>：業務系統發出 <code>PURGE tag=product-123</code> API 呼叫，CDN 用反向索引找出所有帶這個 tag 的快取項目並失效。一次 API 呼叫可能失效數百個 URL。</li>
<li><strong>回源補快取</strong>：被 purge 的 URL 下一次被請求時回源、重新快取。搭配 stale-while-revalidate 可以讓第一個回源請求不阻塞使用者。</li>
</ol>
<p>Tag-based purge 跟顯式 purge（按 URL purge）的本質差異在於「失效單位是業務實體、不是 URL」。按 URL purge 要在業務端維護「一個商品對應哪些 URL」的映射，tag purge 把這個映射交給 CDN 的反向索引。代價是 tag 設計要跟業務模型對齊——tag 太粗（一個 tag 覆蓋太多資源）會過度 purge，tag 太細會退化成按 URL purge。</p>
<p><strong>Push-based instant purge</strong>（Cloudflare、Fastly 規格 &lt;150ms 全球同步）讓全球 purge 從「分鐘級」變成「準同步」。選擇策略時要按 vendor 能力跟資源更新模式組合。</p>
<p>選錯策略的代價會在事故時放大。把限時優惠的價格用「TTL 自然過期」策略佈在 CDN、活動結束後仍有客人看到舊價格繼續下單、客服與退款成本會壓回業務端。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>origin 流量隨使用者線性成長</td>
          <td>cache hit ratio 偏低，邊緣層沒發揮 origin protection</td>
          <td>檢查 cache-control header、命中率分布、coalescing 設定</td>
      </tr>
      <tr>
          <td>edge 命中率忽然下降</td>
          <td>purge 設定誤觸全網、或 cache key 設計過細</td>
          <td>檢查近期 purge 操作、vary 與 query string 設計</td>
      </tr>
      <tr>
          <td>purge 後仍看到舊內容</td>
          <td>全球節點同步延遲、或 CDN 與應用層快取沒對齊</td>
          <td>確認 CDN purge 完成訊號、再追應用層快取狀態</td>
      </tr>
      <tr>
          <td>高峰時 origin 出現 5xx 尖峰</td>
          <td>edge 沒做 stale-if-error，origin 過載直接打回使用者</td>
          <td>啟用 stale-while-revalidate、檢查 origin shield 設定</td>
      </tr>
      <tr>
          <td>部分區域延遲偏高</td>
          <td>區域節點覆蓋不足、或回源走錯區域</td>
          <td>檢查路由策略、加開 edge POP、考慮多 CDN 策略</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>CDN 跟「加速工具」的混淆，會讓 origin protection 跟一致性責任被忽略。多數團隊上線後第一次撞牆，是 KOL 引流或活動高峰把 origin 直接打掛，事後才發現 CDN 只覆蓋了靜態 asset、HTML 與 API 都直接打回 origin。</p>
<p>把 purge 當成同步操作也容易出事。緊急下架觸發 purge 後立刻通知公關「已下線」，但全球節點還沒收斂，仍有區域看到原內容。這類風險要把「purge 已完成」當成可觀測訊號處理，不是 API 回 200 就視為完成。</p>
<p>把 CDN 當成應用層快取替代品則是另一個極端。商品價格、會員等級這類「跟使用者狀態相關」的資料放邊緣層，會在用戶切帳號、優惠變更時暴露其他人的資料或舊狀態，是 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read</a> 的擴大版。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>CDN 專注「靜態與半靜態內容的網路層分發」。當問題進入動態 API 的延遲、跨服務一致性、寫入路徑保護，責任分別交給 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">02 cache aside</a> 與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a> 模組。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">07 入口治理</a> 的交接：CDN 同時是公網入口，需要承接 WAF、bot mitigation、TLS termination 等資安責任。邊緣層的安全設定不可遺漏，否則 origin 被繞過直接攻擊。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>邊緣分發策略可用以下案例回寫：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar：1800 萬同時觀眾的 IPL 直播</a> — 極端峰值靠多 CDN + origin shield 把 origin 流量壓在容量範圍內。Hotstar 的具體做法是把 hot content（live stream segment）跟 warm content（VOD）分配到不同 CDN provider、利用「edge cache miss 時不是同時打 origin」這條 cache stampede 防禦機制讓 origin 流量曲線跟使用者請求曲線解耦。對照本章「origin protection」段三大策略落地。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom：COVID 30 倍突發</a> — 30 倍突發中，登入頁、會議連結頁這類靜態資源由邊緣層吸收絕大部分讀取流量，API 叢集只面對真實的會議建立 / 結束請求。對照本章「Cacheable vs Non-Cacheable 判讀」段：登入頁屬未登入者一致、適合邊緣化；會議內互動屬寫入 API、保持在 origin。</li>
<li><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve 與 Tiered Storage</a> — Cloudflare 在 CDN 內部再分一層 Cache Reserve（持久層）、把 warm 內容從 origin 卸下、避免 edge LRU 淘汰後又回到 origin。對照本章「三層快取」段：邊緣層內部本身也能有 hot / warm 分層、是同一概念的遞迴應用。</li>
</ul>
<p>三個案例依規模從外向內展開：Hotstar 是極端峰值下 origin protection 防禦的天花板測試、Zoom 是把非交易流量（登入 / 連結頁）分流降低 API 叢集壓力的標準應用、Cloudflare Cache Reserve 則展示 CDN vendor 自身把 hot / warm 內容再分層的內部架構。讀者可串著讀理解規模光譜、也可以挑一條深入。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">02 cache aside</a> 的交接：應用層快取與邊緣層的失效路徑要對齊，避免兩層 stale 同時發生。</li>
<li>與 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a> 的交接：edge miss 後流量進到 origin LB，超時與重試設定要協調。</li>
<li>與 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理</a> 的交接：CDN 是公網入口，WAF、TLS 與 bot mitigation 在邊緣層落地。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃</a> 的交接：cache hit ratio 是 origin 容量規劃的核心輸入，命中率假設失準會直接撞牆。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a></strong>：邊緣層擋住讀流量後、寫流量與事務鏈的下一塊是非同步化。</p>
<p>其他延伸方向：</p>
<ul>
<li>邊緣失效跟應用層失效串成 invalidation pipeline → <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a></li>
<li>高峰活動把 CDN 跟排隊機制組合成保護網 → <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
<li>Origin 端的入口流量合約 → <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a></li>
</ul>
]]></content:encoded></item><item><title>4.9 Production 部署的資源評估原理</title><link>https://tarrragon.github.io/blog/llm/04-applications/production-resource-planning/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/production-resource-planning/</guid><description>&lt;p>LLM 應用從本地實驗跨到 production 是個 phase transition、不是線性放大。本地 single-user 場景的「跑得起來」變 production 場景就要回答全新一組問題：100 個 user 同時打進來怎麼辦、每個 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/token/" data-link-title="Token" data-link-desc="LLM 處理文字時的最小單位：介於字元與單字之間">token&lt;/a> 要多少錢、p99 latency 怎麼控、model service down 了怎麼處理。&lt;/p>
&lt;p>本章寫的是「&lt;strong>從本地實驗 → production 該想清楚的維度&lt;/strong>」、focus 在跨工具世代不變的原理。具體 framework（vLLM、TGI、Triton、SGLang）跟雲端服務（OpenAI / Anthropic / Bedrock）的選型不展開——這些半年一個世代、寫了會過時。本章建立的是「無論用哪套工具、都該回答」的設計取捨清單。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &amp;#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">4.3 Tool use&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent&lt;/a> 對應「應用怎麼設計」、本章對應「應用怎麼跑」。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後你能：&lt;/p>
&lt;ol>
&lt;li>列出 production LLM 部署該評估的 6 個 dimension。&lt;/li>
&lt;li>解釋 single-user benchmark 為什麼不能直接 extrapolate 到 multi-user 場景。&lt;/li>
&lt;li>區分 latency-sensitive 跟 throughput-sensitive 應用的設計差別。&lt;/li>
&lt;li>對成本模型（$/request、$/token、$/month）做合理估算。&lt;/li>
&lt;/ol>
&lt;h2 id="從本地到-production-的-phase-transition">從本地到 production 的 phase transition&lt;/h2>
&lt;p>本地 LLM 跑 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP&lt;/a> 的 baseline（&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-mcp-resources/" data-link-title="Hands-on：RAG / MCP 的資源 footprint" data-link-desc="RAG ingest / query / MCP server 三階段的 RAM / 磁碟 / process 實測、多模型並存的 RAM 衝突、本地 LLM 跑 RAG 跟單純 chat 的差異">hands-on 章節&lt;/a>）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>本地（single-user）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>並發 user&lt;/td>
 &lt;td>1&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Latency 要求&lt;/td>
 &lt;td>秒級 OK&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index 大小&lt;/td>
 &lt;td>&amp;lt; 100 MB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost&lt;/td>
 &lt;td>一次性硬體&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Uptime&lt;/td>
 &lt;td>自己重啟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>觀測&lt;/td>
 &lt;td>&lt;code>tail log&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Production 場景每個維度都跳一個量級：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Production（multi-tenant）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>並發 user&lt;/td>
 &lt;td>10 - 10000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Latency 要求&lt;/td>
 &lt;td>p50 &amp;lt; 500 ms、p99 &amp;lt; 2 s&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index 大小&lt;/td>
 &lt;td>GB - TB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost&lt;/td>
 &lt;td>$ / request、$ / token、$ / month&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Uptime&lt;/td>
 &lt;td>99.9% SLA&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>觀測&lt;/td>
 &lt;td>metrics、traces、dashboards&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個維度跳一個量級的 implication 不是「資源 × 10」、是「全新的失敗模式 + 新的設計取捨」。&lt;/p></description><content:encoded><![CDATA[<p>LLM 應用從本地實驗跨到 production 是個 phase transition、不是線性放大。本地 single-user 場景的「跑得起來」變 production 場景就要回答全新一組問題：100 個 user 同時打進來怎麼辦、每個 <a href="/blog/llm/knowledge-cards/token/" data-link-title="Token" data-link-desc="LLM 處理文字時的最小單位：介於字元與單字之間">token</a> 要多少錢、p99 latency 怎麼控、model service down 了怎麼處理。</p>
<p>本章寫的是「<strong>從本地實驗 → production 該想清楚的維度</strong>」、focus 在跨工具世代不變的原理。具體 framework（vLLM、TGI、Triton、SGLang）跟雲端服務（OpenAI / Anthropic / Bedrock）的選型不展開——這些半年一個世代、寫了會過時。本章建立的是「無論用哪套工具、都該回答」的設計取捨清單。</p>
<p>跟 <a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG</a> / <a href="/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">4.3 Tool use</a> / <a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent</a> 對應「應用怎麼設計」、本章對應「應用怎麼跑」。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後你能：</p>
<ol>
<li>列出 production LLM 部署該評估的 6 個 dimension。</li>
<li>解釋 single-user benchmark 為什麼不能直接 extrapolate 到 multi-user 場景。</li>
<li>區分 latency-sensitive 跟 throughput-sensitive 應用的設計差別。</li>
<li>對成本模型（$/request、$/token、$/month）做合理估算。</li>
</ol>
<h2 id="從本地到-production-的-phase-transition">從本地到 production 的 phase transition</h2>
<p>本地 LLM 跑 <a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a> / <a href="/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP</a> 的 baseline（<a href="/blog/llm/01-local-llm-services/hands-on/rag-mcp-resources/" data-link-title="Hands-on：RAG / MCP 的資源 footprint" data-link-desc="RAG ingest / query / MCP server 三階段的 RAM / 磁碟 / process 實測、多模型並存的 RAM 衝突、本地 LLM 跑 RAG 跟單純 chat 的差異">hands-on 章節</a>）：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>本地（single-user）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>並發 user</td>
          <td>1</td>
      </tr>
      <tr>
          <td>Latency 要求</td>
          <td>秒級 OK</td>
      </tr>
      <tr>
          <td>Index 大小</td>
          <td>&lt; 100 MB</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>一次性硬體</td>
      </tr>
      <tr>
          <td>Uptime</td>
          <td>自己重啟</td>
      </tr>
      <tr>
          <td>觀測</td>
          <td><code>tail log</code></td>
      </tr>
  </tbody>
</table>
<p>Production 場景每個維度都跳一個量級：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Production（multi-tenant）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>並發 user</td>
          <td>10 - 10000</td>
      </tr>
      <tr>
          <td>Latency 要求</td>
          <td>p50 &lt; 500 ms、p99 &lt; 2 s</td>
      </tr>
      <tr>
          <td>Index 大小</td>
          <td>GB - TB</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>$ / request、$ / token、$ / month</td>
      </tr>
      <tr>
          <td>Uptime</td>
          <td>99.9% SLA</td>
      </tr>
      <tr>
          <td>觀測</td>
          <td>metrics、traces、dashboards</td>
      </tr>
  </tbody>
</table>
<p>每個維度跳一個量級的 implication 不是「資源 × 10」、是「全新的失敗模式 + 新的設計取捨」。</p>
<h2 id="維度-1concurrent-users--throughput">維度 1：Concurrent users / Throughput</h2>
<h3 id="為什麼這個維度最關鍵">為什麼這個維度最關鍵</h3>
<p>本地 single-user 的 baseline 數字（<a href="/blog/llm/01-local-llm-services/hands-on/rag-mcp-resources/" data-link-title="Hands-on：RAG / MCP 的資源 footprint" data-link-desc="RAG ingest / query / MCP server 三階段的 RAM / 磁碟 / process 實測、多模型並存的 RAM 衝突、本地 LLM 跑 RAG 跟單純 chat 的差異">hands-on</a> 紀錄的 RAM / latency）<strong>在 multi-user 場景下幾乎無法 extrapolate</strong>、根因是資源爭用會放大原本看不到的成本：</p>
<ul>
<li>100 個 user 同時送 request → 不是「同樣 latency × 100」、是「queueing + memory contention + GPU 排隊」、單個 user 的 latency 可能漲 10×</li>
<li>同樣 model 服務 N 個 user → KV cache 占用要乘以 N、單卡 GPU 在容量限制下可能裝不下</li>
<li>Single-user 「200 ms latency」可能 production 變「p99 5 秒」</li>
</ul>
<h3 id="key-conceptbatching">Key concept：batching</h3>
<p><a href="/blog/llm/knowledge-cards/batching/" data-link-title="Batching" data-link-desc="多 request 一起跑、攤平 model load 成本：production LLM inference 的核心優化、決定 throughput vs latency 取捨">Batching</a> 跟 <a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a> 設計讓 GPU 能多 user 的 request 一次 forward pass、是 production <a href="/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">inference server</a> 的核心優化。但 batching 也帶取捨：</p>
<ul>
<li><strong>靜態 batching</strong>：等湊滿 N 個 request 才跑、提高 throughput、犧牲首字延遲</li>
<li><strong>連續 batching（continuous batching）</strong>：vLLM / TGI 等用、新 request 動態加入正在跑的 batch、平衡 throughput + latency</li>
<li><strong>No batching</strong>：每 request 獨立跑、latency 低、GPU 利用率差</li>
</ul>
<p>選 batching 策略主要取決於 latency 跟 throughput 哪個重要：</p>
<table>
  <thead>
      <tr>
          <th>應用場景</th>
          <th>適合 batching 策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>互動式對話（IDE plugin、chatbot UI）</td>
          <td>continuous batching、低 latency 優先</td>
      </tr>
      <tr>
          <td>批次處理（document summarization、code review）</td>
          <td>static batching、throughput 優先</td>
      </tr>
      <tr>
          <td>Embedding 服務</td>
          <td>batching 越大越好、embedding 是純 forward pass、batch 16-128 都 OK</td>
      </tr>
  </tbody>
</table>
<h3 id="評估-concurrent-throughput">評估 concurrent throughput</h3>
<p>要做的測試（不在本章 hands-on、是 framework）：</p>
<ol>
<li><strong>Single-user baseline</strong>：measure single request 在 idle server 上的 latency</li>
<li><strong>N-user load test</strong>：用 <a href="https://k6.io">k6</a> / <a href="https://github.com/tsenart/vegeta">vegeta</a> / 自寫 async client 跑 1、10、100 個並發 request</li>
<li><strong>觀察 p50 / p95 / p99 latency 隨並發數變化</strong>：通常 &lt; N=batch_size 時平、超過 batch_size 後 latency 線性漲</li>
<li><strong>GPU memory 飽和點</strong>：tokens-in-flight 超過某個量、新 request 開始排隊</li>
</ol>
<p>實務評估公式：</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">Max concurrent users (steady state)
</span></span><span class="line"><span class="ln">2</span><span class="cl">    = (GPU memory available - model weights) / (per-user KV cache size)</span></span></code></pre></div><p>例：H100 80 GB - 31B model 60 GB = 20 GB 可用 / 每 user 平均 200 MB KV cache = 100 個並發 user。</p>
<p>公式的失效條件（用這幾個 signal 判讀公式何時不可信）：</p>
<ul>
<li><strong>變長 context</strong>：per-user KV cache 隨 context 長度線性增長、長 context 用戶（10K+ tokens）的 KV cache 是短 context 用戶的 5-10 倍、用平均值會嚴重低估。修法：依 P95 context 長度估、不用 average。</li>
<li><strong>Prefix cache 啟用</strong>：vLLM、TGI 等用 prefix sharing 大幅省 KV cache、實際容量比公式高 2-3 倍。修法：跑實測量 prefix cache hit rate。</li>
<li><strong>Speculative decoding</strong>：drafter 跟 target 的 KV cache 都要算進去、每 user 佔用會比 dense baseline 高 10-20%。修法：用 drafter+target 合計算。</li>
<li><strong>不同 batching 策略</strong>：static batching 上限是「batch_size × 等待時間」、continuous batching 是「平均 in-flight tokens」、不同策略下公式的「per-user」定義不同。</li>
</ul>
<p>但這是上限、實際還要考慮 latency target。</p>
<h2 id="維度-2latency-budget">維度 2：Latency budget</h2>
<h3 id="latency-sensitive-vs-throughput-sensitive">Latency-sensitive vs throughput-sensitive</h3>
<p>兩類應用的設計取捨完全不同：</p>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>Latency-sensitive</th>
          <th>Throughput-sensitive</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>範例</td>
          <td>IDE 補完、chat UI、search assistant</td>
          <td>批次標籤、文件摘要、離線 RAG ingest</td>
      </tr>
      <tr>
          <td>目標 metric</td>
          <td>p99 latency</td>
          <td>tokens / second / GPU</td>
      </tr>
      <tr>
          <td>User 經驗影響</td>
          <td>直接（卡住）</td>
          <td>間接（總時間）</td>
      </tr>
      <tr>
          <td>Batching</td>
          <td>小 batch / continuous</td>
          <td>大 batch</td>
      </tr>
      <tr>
          <td>資源規劃</td>
          <td>預留 headroom 給 spike</td>
          <td>跑滿 GPU 利用率</td>
      </tr>
  </tbody>
</table>
<p>混合應用（如 chat with RAG）有兩段：retrieval（throughput-friendly、可 batch）+ generation（latency-sensitive、要 stream）。兩段獨立優化。</p>
<h3 id="latency-預算分配">Latency 預算分配</h3>
<p>一個 RAG 應用的 p99 latency 是各段加總：</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">Total p99 = client → API gateway → retrieval → LLM prefill → LLM decode → response stream
</span></span><span class="line"><span class="ln">2</span><span class="cl">         ≈ 50 ms      20 ms        50 ms        500 ms       1500 ms      100 ms
</span></span><span class="line"><span class="ln">3</span><span class="cl">         ≈ 2.2 seconds</span></span></code></pre></div><p>如果 p99 budget 是 2 秒、要先確認<strong>最大消耗段是哪個</strong>：</p>
<ul>
<li>通常 LLM generation 是最大、是優化重心</li>
<li>Retrieval 在大 corpus 場景可能超過 100 ms、要 index 優化（HNSW、近似 nearest neighbor）</li>
<li>API gateway 通常可忽略、超過 50 ms 就有 SRE 議題</li>
</ul>
<p>各段監控分開、把監控拆到各段才找得到 root cause；只看 total latency 會錯失定位線索。</p>
<h2 id="維度-3cost-model">維度 3：Cost model</h2>
<h3 id="三種計費單位">三種計費單位</h3>
<table>
  <thead>
      <tr>
          <th>單位</th>
          <th>怎麼算</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>$/request</td>
          <td>每 API call 固定價</td>
          <td>簡單應用、可預測流量</td>
      </tr>
      <tr>
          <td>$/token</td>
          <td>看 input + output token 數</td>
          <td>OpenAI / Anthropic 主流、混合輸入長度應用</td>
      </tr>
      <tr>
          <td>$/server-hour</td>
          <td>自家跑 GPU instance、月租</td>
          <td>高 throughput、可預測 utilization</td>
      </tr>
  </tbody>
</table>
<p>雲端 API（OpenAI / Anthropic）幾乎都 $/token、給定 model 不同 price tier。自家跑（vLLM on Lambda Labs / RunPod）是 $/server-hour。</p>
<h3 id="成本估算-worked-example">成本估算 worked example</h3>
<p>假設應用：</p>
<ul>
<li>1000 active users / day</li>
<li>每 user 平均 10 requests / day</li>
<li>每 request 平均 1000 input tokens + 500 output tokens</li>
<li>用 Claude Sonnet 4.6（假設 $3 input / $15 output per million tokens）</li>
</ul>
<p>每日 cost：</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">total_requests = 1000 × 10 = 10000 / day
</span></span><span class="line"><span class="ln">2</span><span class="cl">input_tokens = 10000 × 1000 = 10M
</span></span><span class="line"><span class="ln">3</span><span class="cl">output_tokens = 10000 × 500 = 5M
</span></span><span class="line"><span class="ln">4</span><span class="cl">daily_cost = 10M × $3/M + 5M × $15/M = $30 + $75 = $105 / day
</span></span><span class="line"><span class="ln">5</span><span class="cl">monthly_cost ≈ $3150</span></span></code></pre></div><p>跑自家 GPU 比較：</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">H100 instance: ~$2/hour（以 2026 年 spot price 為例、實際隨雲廠商與當期報價變動）
</span></span><span class="line"><span class="ln">2</span><span class="cl">H100 monthly = $2 × 24 × 30 = $1440
</span></span><span class="line"><span class="ln">3</span><span class="cl">若 utilization &gt; 50% 且團隊有 SRE 能力維運、自架較划算
</span></span><span class="line"><span class="ln">4</span><span class="cl">若 utilization &lt; 30%、或團隊無 GPU 維運經驗、API 較划算</span></span></code></pre></div><p><strong>Breakeven 點通常在「持續高 utilization + 團隊有維運能力」</strong>——尖峰流量短的應用、或團隊無 GPU 維運經驗、API 更划算（不用養閒置 capacity 跟 SRE 人力）。實際判讀還要加合規 / 資料主權 / vendor lock-in 等非價格因素。</p>
<h3 id="hidden-cost">Hidden cost</h3>
<p>容易漏算的：</p>
<ul>
<li><strong>Egress bandwidth</strong>：cloud GPU instance 出流量、AWS / GCP 都 $/GB</li>
<li><strong>Storage</strong>：vector DB / log retention / metric retention</li>
<li><strong>失敗 retry</strong>：5xx error 自動 retry、token 重算</li>
<li><strong>Cold start</strong>：scale-to-zero 設定、cold start 浪費 5-30 秒 GPU time / 次</li>
</ul>
<h2 id="維度-4storage--vector-db">維度 4：Storage / Vector DB</h2>
<p>本地 <a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a> demo 用 pickle、production 不行——pickle 不支援並發 read、不支援 update、不支援 partition、必須換 <a href="/blog/llm/knowledge-cards/vector-database/" data-link-title="Vector Database" data-link-desc="為高維向量 (embedding) 設計的儲存 &#43; 近似最近鄰 (ANN) 檢索系統：RAG 從 prototype 跨到 production 的關鍵元件">vector database</a>。</p>
<h3 id="vector-db-的設計取捨">Vector DB 的設計取捨</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Hosted vs self-host</strong></td>
          <td>Hosted（Pinecone、Weaviate Cloud）省維護、self-host 控制成本</td>
      </tr>
      <tr>
          <td><strong>In-memory vs disk-based</strong></td>
          <td>In-memory 快但記憶體限制、disk-based 大但 latency 高</td>
      </tr>
      <tr>
          <td><strong>HNSW vs flat</strong></td>
          <td>HNSW 近似但 sublinear、flat 精確但 linear</td>
      </tr>
      <tr>
          <td><strong>Update strategy</strong></td>
          <td>Periodic batch index rebuild vs incremental update</td>
      </tr>
  </tbody>
</table>
<p>具體選型半年一變、本章不展開。<strong>設計時要回答的問題</strong>：</p>
<ol>
<li>Corpus 多大？1M 以下 in-memory 就好、1M 以上要 disk-based</li>
<li>Update 頻率？每天一次 vs 即時、影響 architecture</li>
<li>Latency target？&lt; 50 ms 要 in-memory / HNSW、&lt; 200 ms 用 disk-based</li>
<li>並發 query 量？每秒 100 query 跟每秒 10000 query 設計完全不同</li>
</ol>
<h3 id="index-大小成長">Index 大小成長</h3>
<p>從 hands-on 章節 extrapolate：</p>
<table>
  <thead>
      <tr>
          <th>Corpus 規模</th>
          <th>Index 大小（含 chunks + embeddings）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1K docs</td>
          <td>~50 MB</td>
      </tr>
      <tr>
          <td>100K docs</td>
          <td>~5 GB</td>
      </tr>
      <tr>
          <td>1M docs</td>
          <td>~50 GB</td>
      </tr>
      <tr>
          <td>10M docs</td>
          <td>~500 GB</td>
      </tr>
      <tr>
          <td>100M docs</td>
          <td>~5 TB</td>
      </tr>
  </tbody>
</table>
<p>10M docs 以上、單機（256GB RAM、商用 SSD）放不進 in-memory index、要 sharding + 分散式 index。</p>
<h2 id="維度-5observability">維度 5：Observability</h2>
<p>Single-user <code>tail log</code> 不夠 production 用。要看的 metric：</p>
<h3 id="latency-metrics">Latency metrics</h3>
<ul>
<li><strong>TTFT (Time to First Token)</strong>：user-perceived「響應時間」、streaming 場景關鍵</li>
<li><strong>TPS (Tokens per second)</strong>：generation 速度</li>
<li><strong>End-to-end latency</strong>：含 retrieval + LLM + post-processing</li>
<li><strong>Per-percentile breakdown</strong>：p50 / p90 / p95 / p99——p99 反映最差 user 體驗</li>
</ul>
<h3 id="throughput-metrics">Throughput metrics</h3>
<ul>
<li><strong>Requests per second</strong>：API 端 RPS</li>
<li><strong>Tokens per second</strong>（aggregate）：GPU 整體 throughput</li>
<li><strong>Queue depth</strong>：等待 batch 的 request 數量、暴漲表示 overload</li>
</ul>
<h3 id="cost-metrics">Cost metrics</h3>
<ul>
<li><strong>$ per active user per day</strong>：產品經濟學基本盤</li>
<li><strong>Cost per session</strong>：互動式應用單位成本</li>
<li><strong>Cache hit rate</strong>：prompt cache / embedding cache 命中率、直接影響 cost</li>
</ul>
<h3 id="quality-metrics">Quality metrics</h3>
<ul>
<li><strong>Refusal rate</strong>：模型 refuse 回應的比例</li>
<li><strong>Hallucination rate</strong>：（要 reviewer 標）</li>
<li><strong>User feedback score</strong>：thumb up / down</li>
</ul>
<h3 id="工具metrics--traces--logs-三層">工具：metrics / traces / logs 三層</h3>





<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">Metrics（Prometheus / Datadog / CloudWatch）
</span></span><span class="line"><span class="ln">2</span><span class="cl">    → time-series、aggregate、適合 alerting
</span></span><span class="line"><span class="ln">3</span><span class="cl">Traces（OpenTelemetry / Datadog APM）
</span></span><span class="line"><span class="ln">4</span><span class="cl">    → per-request、可追蹤跨服務 latency
</span></span><span class="line"><span class="ln">5</span><span class="cl">Logs（structured JSON、推 ELK / Loki）
</span></span><span class="line"><span class="ln">6</span><span class="cl">    → 詳細 context、debug 用</span></span></code></pre></div><p>三層各司其職、各層保留專屬職責：metric 看到 p99 漲、用 trace 找哪個 request 哪段慢、用 log 看那 request 的具體 prompt / response。</p>
<h2 id="維度-6reliability--sla">維度 6：Reliability / SLA</h2>
<h3 id="可預期的失敗模式">可預期的失敗模式</h3>
<table>
  <thead>
      <tr>
          <th>失敗類型</th>
          <th>處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Transient GPU OOM</strong></td>
          <td>retry with smaller batch、circuit breaker</td>
      </tr>
      <tr>
          <td><strong>Inference timeout</strong></td>
          <td>切短 max_tokens、拒絕過長 prompt</td>
      </tr>
      <tr>
          <td><strong>Model server crash</strong></td>
          <td>health check + auto-restart（systemd / k8s）</td>
      </tr>
      <tr>
          <td><strong>Vector DB unavailable</strong></td>
          <td>fallback：跳過 RAG、純 chat 答</td>
      </tr>
      <tr>
          <td><strong>Upstream API rate limit</strong></td>
          <td>exponential backoff + jitter</td>
      </tr>
  </tbody>
</table>
<h3 id="graceful-degradation">Graceful degradation</h3>
<p>設計 production LLM 應用、要回答「失敗時降級到什麼」：</p>
<table>
  <thead>
      <tr>
          <th>Component down</th>
          <th>Acceptable degradation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Vector DB</td>
          <td>用 LLM 內知識回答 + 標明「未查最新文件」</td>
      </tr>
      <tr>
          <td>RAG retrieval 但 LLM 仍跑</td>
          <td>用退役 cache 結果 + retry</td>
      </tr>
      <tr>
          <td>Primary LLM API</td>
          <td>fallback 到 secondary（OpenAI ↔ Anthropic ↔ 本地）</td>
      </tr>
      <tr>
          <td>全部 down</td>
          <td>顯示維護頁、回 503 + Retry-After、避免直接 5xx</td>
      </tr>
  </tbody>
</table>
<p>在 SLA 承諾下、每個 fallback 路徑都要事前設計、避免出事時臨時決策（早期 prototype / 內部工具可接受 reactive 處理、production 階段不行）。</p>
<h3 id="capacity-planning">Capacity planning</h3>
<p>簡單公式：</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">Required capacity = peak_concurrent_users × per_user_RAM
</span></span><span class="line"><span class="ln">2</span><span class="cl">                  × overhead_factor (1.3-1.5)
</span></span><span class="line"><span class="ln">3</span><span class="cl">                  × redundancy_factor (2x for HA)</span></span></code></pre></div><p>例：peak 100 並發、每 user ~500 MB KV cache、overhead 1.3、HA 2x → 130 GB GPU memory。一張 H100 不夠、要兩張 A100 80GB 或 H100 + sharding。</p>
<h2 id="跟本地-hands-on-的對照">跟本地 hands-on 的對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>本地 hands-on 紀錄</th>
          <th>Production 該量什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-user latency</td>
          <td>30-60s for SDXL、5-20s for chat</td>
          <td>p50 / p95 / p99 latency</td>
      </tr>
      <tr>
          <td>Index size</td>
          <td>~3.7 MB / 463 chunks</td>
          <td>sharded index、GB-TB 規模</td>
      </tr>
      <tr>
          <td>Process management</td>
          <td><code>pkill -9</code></td>
          <td>systemd / k8s liveness probe</td>
      </tr>
      <tr>
          <td>Disk cleanup</td>
          <td>手動 <code>ollama rm</code></td>
          <td>自動 retention policy</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>一次性硬體</td>
          <td>$/token / day budget alerts</td>
      </tr>
      <tr>
          <td>Observability</td>
          <td><code>tail log</code></td>
          <td>Prometheus + Grafana / Datadog</td>
      </tr>
      <tr>
          <td>Failure response</td>
          <td>自己重啟</td>
          <td>auto-recover + alert + runbook</td>
      </tr>
  </tbody>
</table>
<p>本地數字是「能跑」的證明、production 數字是「能用」的驗證。本地驗證完 architecture 後、production deployment 該重做 load test、不能 assume 線性 scale。</p>
<h2 id="跨-framework-不變的設計問題">跨 framework 不變的設計問題</h2>
<p>不管你用 vLLM / TGI / Triton / SGLang / OpenAI API、production 設計都要回答：</p>
<ol>
<li><strong>Latency vs throughput</strong>：哪個是主要 metric？</li>
<li><strong>Batch strategy</strong>：static / continuous / per-request？</li>
<li><strong>Cost ceiling</strong>：$/day budget 多少？超過怎麼處理？</li>
<li><strong>Storage</strong>：vector DB 規模？update 頻率？</li>
<li><strong>Observability</strong>：哪些 metric 是 alert worthy？</li>
<li><strong>Reliability</strong>：failure mode + graceful degradation 設計</li>
<li><strong>Capacity</strong>：peak + redundancy 需要多少 GPU memory</li>
</ol>
<p>這 7 個問題回答一致時、framework 選擇通常不是 production 失敗的根因——資源評估跟設計取捨已對齊、framework 多半是配套選項。</p>
<h2 id="何時這篇會過時">何時這篇會過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>6 個維度（concurrency / latency / cost / storage / observability / reliability）</li>
<li>Latency-sensitive vs throughput-sensitive 應用的設計差異</li>
<li>三類計費單位的取捨</li>
<li>Metrics / traces / logs 三層觀測</li>
<li>Graceful degradation 設計</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 inference framework（vLLM / TGI / SGLang 等）的 ranking</li>
<li>雲端 API price tier</li>
<li>哪些 vector DB 主流</li>
</ul>
<p>新 framework 出來時、回到 6 維度 framework 問：它在哪個維度有突破？對既有設計問題的答案有沒有改變？通常會發現核心問題沒變、只是工具更熟。</p>
<h2 id="跟其他章節的關係">跟其他章節的關係</h2>
<ul>
<li><a href="/blog/llm/01-local-llm-services/hands-on/rag-mcp-resources/" data-link-title="Hands-on：RAG / MCP 的資源 footprint" data-link-desc="RAG ingest / query / MCP server 三階段的 RAM / 磁碟 / process 實測、多模型並存的 RAM 衝突、本地 LLM 跑 RAG 跟單純 chat 的差異">hands-on RAG/MCP 資源</a>：本地 baseline 數字、本章的 production extrapolation 起點</li>
<li><a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG</a> / <a href="/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">4.3 Tool use</a> / <a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent</a>：應用層設計、本章是「應用如何跑」的補完</li>
<li><a href="/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">0.5 硬體記憶體預算</a>：本地單機 perspective、本章對應 multi-machine production</li>
<li><a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">1.7 排錯方法論</a>：本地 trouble-shooting、本章是 production observability 的對照</li>
</ul>
]]></content:encoded></item><item><title>5.C9 反例：平台切流未先 Draining</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/</guid><description>&lt;p>這個反例的核心責任是說明部署平台切換失敗常在 connection lifecycle 管理——平台元件本身健康，事故來源是切換時序錯位。&lt;/p>
&lt;h2 id="事故長相">事故長相&lt;/h2>
&lt;p>平台切流一開始看似成功，新的 instance 也通過 readiness，但長連線、背景工作與 load balancer 仍把流量送到即將下線的節點。使用者看到的是短時間大量 5xx、重連風暴與 timeout。&lt;/p>
&lt;p>典型 timeline：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>T+0&lt;/strong>：開始切流，新版本 pod readiness 通過，LB 開始導入流量。&lt;/li>
&lt;li>&lt;strong>T+30s&lt;/strong>：5xx spike 出現。舊 pod 的 endpoint 尚未從所有 kube-proxy / envoy 移除，部分客戶端仍打到舊 pod。舊 pod 同時收到 SIGTERM 開始 shutdown，在途請求被中斷。&lt;/li>
&lt;li>&lt;strong>T+2m&lt;/strong>：長連線客戶端偵測到斷線，觸發 reconnect。大量客戶端同時重連到新 pod，形成 reconnect storm。新 pod 的連線數瞬間飆高，部分 pod 因連線數超出預期開始 timeout。&lt;/li>
&lt;li>&lt;strong>T+5m&lt;/strong>：on-call 判斷切流失敗，決定回退。但回退操作需要時間——DNS 權重切回、LB 規則恢復、舊 pod 重新啟動。&lt;/li>
&lt;li>&lt;strong>T+15m&lt;/strong>：回退完成，舊版本重新接流量。但 reconnect storm 尚未收斂，連線數曲線仍高於 baseline，客戶端在新舊入口之間震盪。&lt;/li>
&lt;li>&lt;strong>T+30m&lt;/strong>：連線數逐漸回落，錯誤率回到 baseline。事故實際影響時間遠超切流本身。&lt;/li>
&lt;/ul>
&lt;h2 id="為什麼會擴大">為什麼會擴大&lt;/h2>
&lt;p>事故擴大的根因是 drain、idle timeout、health check、client retry 四者節奏錯位。每一對的不同步都會放大問題：&lt;/p>
&lt;p>&lt;strong>drain 與 endpoint 摘除不同步&lt;/strong>：pod 收到 SIGTERM 開始 shutdown，但 endpoint 還在 LB 的可用集合中（endpoint controller 同步有延遲）。這段窗口內新請求仍被導到即將關閉的 pod，產生 5xx。解法是 preStop hook 先等 endpoint 傳播（5-15 秒），再開始 graceful shutdown。&lt;/p>
&lt;p>&lt;strong>idle timeout 與 drain window 不同步&lt;/strong>：LB 的 idle timeout 設 60 秒，但 drain window 只有 30 秒。drain 結束後 pod 被強制終止，LB 側認為連線還活著（60 秒內不算 idle），繼續送流量到已不存在的 pod。結果是 LB 拿到 connection reset，觸發重試或回 502。&lt;/p>
&lt;p>&lt;strong>health check 與 readiness 語意不同步&lt;/strong>：LB health check 每 10 秒打一次，連續 3 次失敗才摘除。pod 已經 not-ready 但 LB 要 30 秒後才反映。這 30 秒窗口跟 drain window 疊加，讓舊 pod 在 shutdown 狀態下持續收到流量。&lt;/p>
&lt;p>&lt;strong>client retry 與 reconnect 策略不同步&lt;/strong>：客戶端偵測到連線中斷後立即重試（無 backoff），大量客戶端同時重連。如果客戶端沒有 jitter，重連請求會集中在同一毫秒到達，形成 thundering herd。&lt;/p>
&lt;p>這四組錯位在穩態下不會出現——穩態時 drain / timeout / health check 各自運作不衝突。只有在切流時四者同時被觸發，錯位才會互相放大。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>回退分兩個階段，性質不同、節奏不同、不能合併執行。&lt;/p>
&lt;p>&lt;strong>第一階段：凍結 + 恢復穩定路徑（分鐘級）&lt;/strong>。發現切流失敗的第一動作是停止下一批切流（freeze rollout），然後恢復舊入口權重（DNS 加權切回 / LB 規則回復）。新版本 pod 不立即關閉——保留作為對照證據，也避免關閉動作觸發第二波 reconnect。這個階段的目標是「讓震盪不擴大」，所有動作要在 5 分鐘內完成。&lt;/p>
&lt;p>&lt;strong>第二階段：等待收斂 + 修正錯位（小時級）&lt;/strong>。凍結後進入觀察狀態。reconnect storm 需要時間消化——客戶端逐漸穩定到舊入口、連線數曲線下降、5xx 回到 baseline。觀察指標：連線數曲線、reconnect rate、per-version error rate。三項都回到 baseline 且持續 N 分鐘（通常 10-15 分鐘），才算穩定。穩定後開始修正：找出 drain / timeout / health check / retry 的具體錯位點，修正後重新進入小範圍驗證。&lt;/p></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明部署平台切換失敗常在 connection lifecycle 管理——平台元件本身健康，事故來源是切換時序錯位。</p>
<h2 id="事故長相">事故長相</h2>
<p>平台切流一開始看似成功，新的 instance 也通過 readiness，但長連線、背景工作與 load balancer 仍把流量送到即將下線的節點。使用者看到的是短時間大量 5xx、重連風暴與 timeout。</p>
<p>典型 timeline：</p>
<ul>
<li><strong>T+0</strong>：開始切流，新版本 pod readiness 通過，LB 開始導入流量。</li>
<li><strong>T+30s</strong>：5xx spike 出現。舊 pod 的 endpoint 尚未從所有 kube-proxy / envoy 移除，部分客戶端仍打到舊 pod。舊 pod 同時收到 SIGTERM 開始 shutdown，在途請求被中斷。</li>
<li><strong>T+2m</strong>：長連線客戶端偵測到斷線，觸發 reconnect。大量客戶端同時重連到新 pod，形成 reconnect storm。新 pod 的連線數瞬間飆高，部分 pod 因連線數超出預期開始 timeout。</li>
<li><strong>T+5m</strong>：on-call 判斷切流失敗，決定回退。但回退操作需要時間——DNS 權重切回、LB 規則恢復、舊 pod 重新啟動。</li>
<li><strong>T+15m</strong>：回退完成，舊版本重新接流量。但 reconnect storm 尚未收斂，連線數曲線仍高於 baseline，客戶端在新舊入口之間震盪。</li>
<li><strong>T+30m</strong>：連線數逐漸回落，錯誤率回到 baseline。事故實際影響時間遠超切流本身。</li>
</ul>
<h2 id="為什麼會擴大">為什麼會擴大</h2>
<p>事故擴大的根因是 drain、idle timeout、health check、client retry 四者節奏錯位。每一對的不同步都會放大問題：</p>
<p><strong>drain 與 endpoint 摘除不同步</strong>：pod 收到 SIGTERM 開始 shutdown，但 endpoint 還在 LB 的可用集合中（endpoint controller 同步有延遲）。這段窗口內新請求仍被導到即將關閉的 pod，產生 5xx。解法是 preStop hook 先等 endpoint 傳播（5-15 秒），再開始 graceful shutdown。</p>
<p><strong>idle timeout 與 drain window 不同步</strong>：LB 的 idle timeout 設 60 秒，但 drain window 只有 30 秒。drain 結束後 pod 被強制終止，LB 側認為連線還活著（60 秒內不算 idle），繼續送流量到已不存在的 pod。結果是 LB 拿到 connection reset，觸發重試或回 502。</p>
<p><strong>health check 與 readiness 語意不同步</strong>：LB health check 每 10 秒打一次，連續 3 次失敗才摘除。pod 已經 not-ready 但 LB 要 30 秒後才反映。這 30 秒窗口跟 drain window 疊加，讓舊 pod 在 shutdown 狀態下持續收到流量。</p>
<p><strong>client retry 與 reconnect 策略不同步</strong>：客戶端偵測到連線中斷後立即重試（無 backoff），大量客戶端同時重連。如果客戶端沒有 jitter，重連請求會集中在同一毫秒到達，形成 thundering herd。</p>
<p>這四組錯位在穩態下不會出現——穩態時 drain / timeout / health check 各自運作不衝突。只有在切流時四者同時被觸發，錯位才會互相放大。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>回退分兩個階段，性質不同、節奏不同、不能合併執行。</p>
<p><strong>第一階段：凍結 + 恢復穩定路徑（分鐘級）</strong>。發現切流失敗的第一動作是停止下一批切流（freeze rollout），然後恢復舊入口權重（DNS 加權切回 / LB 規則回復）。新版本 pod 不立即關閉——保留作為對照證據，也避免關閉動作觸發第二波 reconnect。這個階段的目標是「讓震盪不擴大」，所有動作要在 5 分鐘內完成。</p>
<p><strong>第二階段：等待收斂 + 修正錯位（小時級）</strong>。凍結後進入觀察狀態。reconnect storm 需要時間消化——客戶端逐漸穩定到舊入口、連線數曲線下降、5xx 回到 baseline。觀察指標：連線數曲線、reconnect rate、per-version error rate。三項都回到 baseline 且持續 N 分鐘（通常 10-15 分鐘），才算穩定。穩定後開始修正：找出 drain / timeout / health check / retry 的具體錯位點，修正後重新進入小範圍驗證。</p>
<p>第一階段的陷阱是「回退了但沒凍結」——回退流量的同時繼續推下一批切流，兩個動作互相衝突。第二階段的陷阱是「時間到了就解凍」——用時間而非指標判斷穩定，可能在連線數仍高時重新切流。</p>
<h2 id="這個事故教給後續章節什麼">這個事故教給後續章節什麼</h2>
<ul>
<li><strong>5.3 load balancer 合約</strong>的「切流告警條件」段：四條告警（批次 5xx、reconnect rate、RTO 超時、per-version error rate 偏離）直接來自這類事故的觀測需求。</li>
<li><strong>5.6 Platform Lifecycle Contract</strong>的「三種 Workload 的 Drain 差異」段：短 API、長連線、worker 的 drain 條件不同——這個事故揭露混用單一 drain window 的後果。</li>
<li><strong>5.8 Rollout/Drain/Rollback</strong>的「Traffic / Drain」段退場順序：readiness 先轉 not-ready → 保留 drain 窗口 → 確認連線數下降 → 終止進程，是從這類事故的 timeline 反推出來的。</li>
</ul>
<h2 id="部署專屬告警條件">部署專屬告警條件</h2>
<ul>
<li>切流批次內 5xx 突增（相對於前一批的升幅超過閾值）</li>
<li>長連線重連率快速上升（reconnect rate 超過 baseline N 倍）</li>
<li>rollback time 超過既定 RTO（執行回退後恢復時間超標）</li>
<li>per-version error rate 偏離（新舊版本 error rate 差距持續不收斂）</li>
</ul>
<p>這些告警的閾值要在 release plan 中先定義。切流期告警跟日常告警分流到不同 channel，避免日常 noise 淹沒切流期的關鍵訊號。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a> 看流量契約與回退框架。回 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 看 drain 的 workload 分類。回 <a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR/Rollback Rehearsal</a> 看回退演練如何預防這類事故。</p>
]]></content:encoded></item><item><title>Deployment Dry Run</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/deployment-dry-run/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/deployment-dry-run/</guid><description>&lt;p>Deployment Dry Run 的核心概念是「在正式部署前預演關鍵步驟」。它讓流程在低風險條件下先驗證 artifact、權限與目標環境配置。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Deployment Dry Run 位在 build / test 完成後、production deploy 之前，通常以 preflight check、模擬發布或目標環境校驗實作。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>正式部署常失敗於權限、路徑或配置差異。&lt;/li>
&lt;li>團隊需要在不影響使用者前提下驗證部署條件。&lt;/li>
&lt;li>發布流程包含高成本動作或不可逆步驟。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>部署腳本先驗證 artifact 存在、環境密鑰可讀、目標 bucket 或 registry 可寫，再進入正式 deploy。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Deployment Dry Run 要定義檢查範圍、成功條件、失敗回饋與執行時機，並和正式部署命令保持一致語意。&lt;/p></description><content:encoded><![CDATA[<p>Deployment Dry Run 的核心概念是「在正式部署前預演關鍵步驟」。它讓流程在低風險條件下先驗證 artifact、權限與目標環境配置。</p>
<h2 id="概念位置">概念位置</h2>
<p>Deployment Dry Run 位在 build / test 完成後、production deploy 之前，通常以 preflight check、模擬發布或目標環境校驗實作。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>正式部署常失敗於權限、路徑或配置差異。</li>
<li>團隊需要在不影響使用者前提下驗證部署條件。</li>
<li>發布流程包含高成本動作或不可逆步驟。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>部署腳本先驗證 artifact 存在、環境密鑰可讀、目標 bucket 或 registry 可寫，再進入正式 deploy。</p>
<h2 id="設計責任">設計責任</h2>
<p>Deployment Dry Run 要定義檢查範圍、成功條件、失敗回饋與執行時機，並和正式部署命令保持一致語意。</p>
]]></content:encoded></item><item><title>Consul</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/consul/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/consul/</guid><description>&lt;p>Consul 是 HashiCorp 出品的 service networking 平台、承擔三個責任：service registry + discovery + health check（跨 VM / container / bare metal）、KV store + watch（dynamic config）、service mesh（Consul Connect、mTLS sidecar）。設計取捨偏向「跨平台統一 registry + multi-datacenter 一級公民 + DNS interface」、適合非 K8s-only 環境。BSL 授權變動同 Terraform。&lt;/p>
&lt;p>對「非 K8s 環境 service discovery、跨平台統一 registry、KV store + watch、跨 datacenter mesh」這條路徑、Consul 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Consul cluster（Server + Agent）&lt;/li>
&lt;li>註冊 service + 配置 health check&lt;/li>
&lt;li>用 KV store + watch 做 dynamic config&lt;/li>
&lt;li>部署 Consul Connect（mTLS service mesh）&lt;/li>
&lt;li>評估 BSL 授權影響跟 alternative（etcd / ZooKeeper）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-consul-跑起來">最短路徑：5 分鐘把 Consul 跑起來&lt;/h2>





&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"># 1. 啟動 dev mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">consul agent -dev -client&lt;span class="o">=&lt;/span>0.0.0.0
&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"># 2. 註冊 service（用 JSON 定義）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">cat &amp;gt; web.json &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SVC&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s">{&amp;#34;service&amp;#34;: {&amp;#34;name&amp;#34;: &amp;#34;web&amp;#34;, &amp;#34;port&amp;#34;: 8080,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;check&amp;#34;: {&amp;#34;http&amp;#34;: &amp;#34;http://localhost:8080/health&amp;#34;, &amp;#34;interval&amp;#34;: &amp;#34;10s&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="s">SVC&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">consul services register web.json
&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="c1"># 3. 查詢（DNS + HTTP API）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">dig @127.0.0.1 -p &lt;span class="m">8600&lt;/span> web.service.consul SRV
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">curl -s http://localhost:8500/v1/catalog/service/web &lt;span class="p">|&lt;/span> jq .&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="agent--server-拓樸">Agent / Server 拓樸&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Server：Raft consensus、quorum（3 / 5 node）&lt;/li>
&lt;li>Agent：每 host 一個、forward 到 server&lt;/li>
&lt;li>Client mode（不參 Raft、純 forward）&lt;/li>
&lt;li>對應 K8s 內 sidecar mode&lt;/li>
&lt;/ul>
&lt;h3 id="service-registration">Service registration&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>API / CLI / config file 註冊&lt;/li>
&lt;li>Health check：HTTP / TCP / Script / TTL&lt;/li>
&lt;li>Tags / metadata&lt;/li>
&lt;li>對應指令：&lt;code>consul services register&lt;/code>、&lt;code>consul catalog services&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="kv-store--watch">KV store + watch&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Consul 是 HashiCorp 出品的 service networking 平台、承擔三個責任：service registry + discovery + health check（跨 VM / container / bare metal）、KV store + watch（dynamic config）、service mesh（Consul Connect、mTLS sidecar）。設計取捨偏向「跨平台統一 registry + multi-datacenter 一級公民 + DNS interface」、適合非 K8s-only 環境。BSL 授權變動同 Terraform。</p>
<p>對「非 K8s 環境 service discovery、跨平台統一 registry、KV store + watch、跨 datacenter mesh」這條路徑、Consul 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Consul cluster（Server + Agent）</li>
<li>註冊 service + 配置 health check</li>
<li>用 KV store + watch 做 dynamic config</li>
<li>部署 Consul Connect（mTLS service mesh）</li>
<li>評估 BSL 授權影響跟 alternative（etcd / ZooKeeper）</li>
</ol>
<h2 id="最短路徑5-分鐘把-consul-跑起來">最短路徑：5 分鐘把 Consul 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 啟動 dev mode</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">consul agent -dev -client<span class="o">=</span>0.0.0.0
</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. 註冊 service（用 JSON 定義）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">cat &gt; web.json <span class="s">&lt;&lt;&#39;SVC&#39;
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">{&#34;service&#34;: {&#34;name&#34;: &#34;web&#34;, &#34;port&#34;: 8080,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  &#34;check&#34;: {&#34;http&#34;: &#34;http://localhost:8080/health&#34;, &#34;interval&#34;: &#34;10s&#34;}}}
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">SVC</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">consul services register web.json
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 3. 查詢（DNS + HTTP API）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">dig @127.0.0.1 -p <span class="m">8600</span> web.service.consul SRV
</span></span><span class="line"><span class="ln">13</span><span class="cl">curl -s http://localhost:8500/v1/catalog/service/web <span class="p">|</span> jq .</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="agent--server-拓樸">Agent / Server 拓樸</h3>
<p>子議題：</p>
<ul>
<li>Server：Raft consensus、quorum（3 / 5 node）</li>
<li>Agent：每 host 一個、forward 到 server</li>
<li>Client mode（不參 Raft、純 forward）</li>
<li>對應 K8s 內 sidecar mode</li>
</ul>
<h3 id="service-registration">Service registration</h3>
<p>子議題：</p>
<ul>
<li>API / CLI / config file 註冊</li>
<li>Health check：HTTP / TCP / Script / TTL</li>
<li>Tags / metadata</li>
<li>對應指令：<code>consul services register</code>、<code>consul catalog services</code></li>
</ul>
<h3 id="kv-store--watch">KV store + watch</h3>
<p>子議題：</p>
<ul>
<li>HTTP API：PUT / GET / DELETE</li>
<li>Watch：long polling / blocking query</li>
<li>適合：dynamic config / feature flag / leader election</li>
<li>對應 consul-template 用 KV 模板生 config</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="consul-connectmtls-service-mesh">Consul Connect（mTLS service mesh）</h3>
<p>子議題：</p>
<ul>
<li>Sidecar proxy（Envoy-based）</li>
<li>Service intentions（誰可訪誰）</li>
<li>mTLS 自動憑證</li>
<li>跟 Istio / Linkerd 對比</li>
</ul>
<h3 id="dns-interface">DNS interface</h3>
<p>子議題：</p>
<ul>
<li>Consul DNS port 8600（dig 可訪）</li>
<li>跟 system resolver 整合（unbound / dnsmasq forward to Consul）</li>
<li>SRV record / A record</li>
<li>對應 service discovery 替代 client-side library</li>
</ul>
<h3 id="multi-datacenter">Multi-datacenter</h3>
<p>子議題：</p>
<ul>
<li>Consul 一級公民跨 DC 設計</li>
<li>WAN federation</li>
<li>Network areas</li>
<li>跟 etcd（單 DC focused）對比</li>
</ul>
<h3 id="acl-system">ACL system</h3>
<p>子議題：</p>
<ul>
<li>Token-based ACL</li>
<li>Policy / Role</li>
<li>Bootstrap token / agent token / management token</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a> IAM</li>
</ul>
<h3 id="bsl-授權影響">BSL 授權影響</h3>
<p>子議題：</p>
<ul>
<li>2023 改 BSL（同 Terraform）</li>
<li>不能 host Consul-as-a-Service 對外</li>
<li>對 internal 用沒影響</li>
<li>Fork：HashFork / no major fork yet（vs OpenTofu 對 Terraform）</li>
</ul>
<h3 id="跟-etcd--zookeeper-對比">跟 etcd / ZooKeeper 對比</h3>
<p>子議題：</p>
<ul>
<li>etcd：K8s control plane 後端、API minimal</li>
<li>ZooKeeper：老牌、Java-heavy、Kafka 跟 HBase 用</li>
<li>Consul：service discovery first、DNS / health check 內建</li>
<li>選擇判讀：K8s 內 → etcd（就在那）；non-K8s 多 DC → Consul</li>
</ul>
<h3 id="consul--nomad--vault-integration">Consul + Nomad / Vault integration</h3>
<p>子議題：</p>
<ul>
<li>跟 HashiCorp Nomad（替代 K8s）整合</li>
<li>跟 Vault（secrets）整合</li>
<li>三件套：Consul + Nomad + Vault</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="service-不出現在-catalog">Service 不出現在 catalog</h3>
<p>操作原則：先確認 registration API 成功、再看 health check 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">consul catalog services
</span></span><span class="line"><span class="ln">2</span><span class="cl">consul members
</span></span><span class="line"><span class="ln">3</span><span class="cl">consul catalog nodes -service<span class="o">=</span>web</span></span></code></pre></div><h3 id="health-check-flapping">Health check flapping</h3>
<p>操作原則：check interval / timeout 設定 + 應用本身不穩定。判讀：UI 看 check history。</p>
<h3 id="split-brainraft">Split brain（Raft）</h3>
<p>操作原則：Server 數量 &lt; quorum（&lt; 半數）會 split brain。修法：recover snapshot / 加 server。</p>
<h3 id="kv-race-condition">KV race condition</h3>
<p>操作原則：多 client 同時改、要用 CAS（compare-and-swap）。判讀：API ModifyIndex。</p>
<h3 id="consul-connect-sidecar-連不上">Consul Connect sidecar 連不上</h3>
<p>操作原則：proxy config 錯 / intention 沒設 / cert 過期。判讀：Envoy admin endpoint（sidecar 後面）。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>K8s 內 service discovery</td>
          <td>K8s 內建 Service / DNS</td>
      </tr>
      <tr>
          <td>K8s service mesh</td>
          <td>Istio / Linkerd / Cilium</td>
      </tr>
      <tr>
          <td>純 K8s control plane backend</td>
          <td>etcd</td>
      </tr>
      <tr>
          <td>純 Java 生態</td>
          <td>ZooKeeper / Eureka</td>
      </tr>
      <tr>
          <td>BSL 敏感</td>
          <td>etcd（OSI）/ ZooKeeper（OSI）</td>
      </tr>
      <tr>
          <td>Cloud-native（AWS）</td>
          <td>Service Connect for ECS / Cloud Map</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Consul API 完整 reference</li>
<li>Vault / Nomad 細節（各自獨立工具）</li>
<li>Raft protocol 內部</li>
<li>BSL 法律細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Consul 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>Tradeshift 用 Linkerd 做切流、對照 Consul Connect 做跨叢集 mTLS 的取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio</a></td>
          <td>大規模 mesh 升級節奏的對照、Consul Connect 在類似治理上要設計分批與回退窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>非 K8s 多 DC 場景 Consul 首選、K8s-only 場景則退到 K8s 內建 service discovery</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Consul 案例</strong>：HashiCorp customer story、Bloomberg / Cloudflare / Stripe 等大規模 Consul 案例、Consul → K8s service mesh 遷移案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 deployment platform</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a>（K8s 內建 service discovery）</li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security IAM</a>、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6 reliability</a></li>
</ul>
]]></content:encoded></item><item><title>5.10 Outbound Tunnel 入口與生命週期</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/</guid><description>&lt;p>家用主機沒有固定 IP、路由器不想開 port，但手機要能連進來操作 — outbound tunnel 用反向連線解這個入口問題。它跟 load balancer 入口是兩種不同的入口形態：LB 假設 instance 有對外可達位址、流量從外網路由進來;tunnel 由本機進程主動外連到邊緣、把流量沿反向隧道帶回來、路由器零開 port、對公網零入站面。家用服務、個人自架工具、無固定 IP 的環境常用這種入口。&lt;/p>
&lt;h2 id="適用判斷">適用判斷&lt;/h2>
&lt;p>選 outbound tunnel 的前提是「要被外部觸及、但不想暴露公網入口」。典型場景：手機遠端操作自有主機、家庭網路內的服務對外、開發環境臨時對外驗證。服務本身值不值得自建、見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a> 的個人自架工具段;這裡只處理「入口形態選了 tunnel 之後」的部署合約。&lt;/p>
&lt;p>cloudflared（綁 Cloudflare 邊緣與網域）、Tailscale（綁私有網路 / Funnel 對外）、Boundary 各有定位差異，但入口生命週期的判讀框架相同。&lt;/p>
&lt;h2 id="tunnel-contract-組成">tunnel contract 組成&lt;/h2>
&lt;p>tunnel 入口合約跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer contract&lt;/a> 對照、差異集中在連線方向與就緒語意：&lt;/p>
&lt;ol>
&lt;li>connection contract：本機進程主動對邊緣建立並維持反向隧道、無入站 port;隧道斷線的重連策略決定外部可達性的恢復速度。&lt;/li>
&lt;li>readiness contract：對外可達 = 隧道已建立 &lt;strong>且&lt;/strong> 後端服務已可服務。兩個條件任一不成立、外部請求就拿到 502 / 連線中斷。&lt;/li>
&lt;li>ordering contract：啟動順序是後端服務先就緒、tunnel 再宣告 ready;關閉順序相反、tunnel 先收斂停止帶入新流量、後端再退出。&lt;/li>
&lt;li>auth contract：tunnel 只負責把流量帶回來、本身不是認證。隧道網址是位址、不是密碼 — 任何拿到網址的人都可達後端、所以認證必須疊在 tunnel 之後（見下）。&lt;/li>
&lt;/ol>
&lt;h2 id="生命週期與-readiness-對齊">生命週期與 readiness 對齊&lt;/h2>
&lt;p>tunnel 入口的就緒判讀比 LB 多一層。LB 的 health check 打後端 instance、通過代表可接流量;tunnel 場景下、「後端 health check 通過」不等於「外部可達」 — 還要隧道本身連上邊緣。readiness 要同時涵蓋兩者、否則會出現「服務自己覺得健康、外面卻連不進來」的盲區。&lt;/p>
&lt;p>啟動順序錯位的後果具體：tunnel 比後端早 ready、邊緣開始導流量進來、後端還沒起、外部看到一批 502。所以 startup 階段 tunnel 的 ready 訊號要 gate 在後端 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 之後。關閉時序則相反、先讓 tunnel 停止帶入新連線、給在途請求收斂窗口、後端再 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a>;這層責任跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 的 startup / readiness / drain 一致、只是 drain 的對象從 LB 摘流量換成 tunnel 收斂。&lt;/p>
&lt;h2 id="穩態維持與重連策略">穩態維持與重連策略&lt;/h2>
&lt;p>隧道建立後進入穩態：tunnel 進程與邊緣之間維持長連線，邊緣用心跳（keepalive）偵測連線是否存活。心跳間隔與超時由供應商決定（cloudflared 預設每 5 秒心跳、連續失敗觸發重連；Tailscale 由 WireGuard 層的 persistent keepalive 維持 NAT 映射）。穩態下不需要額外操作，但要理解一個語意：邊緣側判定「連線已斷」到本機進程偵測到斷線之間有延遲，這段時間外部請求會 timeout 而非立即拿到錯誤。&lt;/p>
&lt;p>連線中斷後 tunnel 進程自動重連，重連策略的關鍵是 backoff：首次斷線立即重試、連續失敗拉長間隔、避免在邊緣側故障時打滿重連請求。重連成功後 readiness 要重新驗證——隧道恢復不等於後端仍然健康，特別是斷線期間後端可能已經被別的事件影響。&lt;/p>
&lt;h3 id="隧道多連線與冗餘">隧道多連線與冗餘&lt;/h3>
&lt;p>cloudflared 預設對每個 tunnel 建立 4 條連線到不同邊緣節點（Cloudflare 在不同 data center 的 edge server）。單條連線斷線時，流量自動切到其餘連線，外部使用者感受不到中斷。4 條連線全部斷開才會觸發完全不可達。&lt;/p>
&lt;p>Tailscale 的冗餘模型不同：WireGuard tunnel 是點對點連線，沒有多邊緣節點分散。Tailscale 的高可用靠 DERP relay server 做中繼——直連失敗時退到 relay，延遲增加但可達性維持。&lt;/p></description><content:encoded><![CDATA[<p>家用主機沒有固定 IP、路由器不想開 port，但手機要能連進來操作 — outbound tunnel 用反向連線解這個入口問題。它跟 load balancer 入口是兩種不同的入口形態：LB 假設 instance 有對外可達位址、流量從外網路由進來;tunnel 由本機進程主動外連到邊緣、把流量沿反向隧道帶回來、路由器零開 port、對公網零入站面。家用服務、個人自架工具、無固定 IP 的環境常用這種入口。</p>
<h2 id="適用判斷">適用判斷</h2>
<p>選 outbound tunnel 的前提是「要被外部觸及、但不想暴露公網入口」。典型場景：手機遠端操作自有主機、家庭網路內的服務對外、開發環境臨時對外驗證。服務本身值不值得自建、見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 的個人自架工具段;這裡只處理「入口形態選了 tunnel 之後」的部署合約。</p>
<p>cloudflared（綁 Cloudflare 邊緣與網域）、Tailscale（綁私有網路 / Funnel 對外）、Boundary 各有定位差異，但入口生命週期的判讀框架相同。</p>
<h2 id="tunnel-contract-組成">tunnel contract 組成</h2>
<p>tunnel 入口合約跟 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer contract</a> 對照、差異集中在連線方向與就緒語意：</p>
<ol>
<li>connection contract：本機進程主動對邊緣建立並維持反向隧道、無入站 port;隧道斷線的重連策略決定外部可達性的恢復速度。</li>
<li>readiness contract：對外可達 = 隧道已建立 <strong>且</strong> 後端服務已可服務。兩個條件任一不成立、外部請求就拿到 502 / 連線中斷。</li>
<li>ordering contract：啟動順序是後端服務先就緒、tunnel 再宣告 ready;關閉順序相反、tunnel 先收斂停止帶入新流量、後端再退出。</li>
<li>auth contract：tunnel 只負責把流量帶回來、本身不是認證。隧道網址是位址、不是密碼 — 任何拿到網址的人都可達後端、所以認證必須疊在 tunnel 之後（見下）。</li>
</ol>
<h2 id="生命週期與-readiness-對齊">生命週期與 readiness 對齊</h2>
<p>tunnel 入口的就緒判讀比 LB 多一層。LB 的 health check 打後端 instance、通過代表可接流量;tunnel 場景下、「後端 health check 通過」不等於「外部可達」 — 還要隧道本身連上邊緣。readiness 要同時涵蓋兩者、否則會出現「服務自己覺得健康、外面卻連不進來」的盲區。</p>
<p>啟動順序錯位的後果具體：tunnel 比後端早 ready、邊緣開始導流量進來、後端還沒起、外部看到一批 502。所以 startup 階段 tunnel 的 ready 訊號要 gate 在後端 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 之後。關閉時序則相反、先讓 tunnel 停止帶入新連線、給在途請求收斂窗口、後端再 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>;這層責任跟 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 的 startup / readiness / drain 一致、只是 drain 的對象從 LB 摘流量換成 tunnel 收斂。</p>
<h2 id="穩態維持與重連策略">穩態維持與重連策略</h2>
<p>隧道建立後進入穩態：tunnel 進程與邊緣之間維持長連線，邊緣用心跳（keepalive）偵測連線是否存活。心跳間隔與超時由供應商決定（cloudflared 預設每 5 秒心跳、連續失敗觸發重連；Tailscale 由 WireGuard 層的 persistent keepalive 維持 NAT 映射）。穩態下不需要額外操作，但要理解一個語意：邊緣側判定「連線已斷」到本機進程偵測到斷線之間有延遲，這段時間外部請求會 timeout 而非立即拿到錯誤。</p>
<p>連線中斷後 tunnel 進程自動重連，重連策略的關鍵是 backoff：首次斷線立即重試、連續失敗拉長間隔、避免在邊緣側故障時打滿重連請求。重連成功後 readiness 要重新驗證——隧道恢復不等於後端仍然健康，特別是斷線期間後端可能已經被別的事件影響。</p>
<h3 id="隧道多連線與冗餘">隧道多連線與冗餘</h3>
<p>cloudflared 預設對每個 tunnel 建立 4 條連線到不同邊緣節點（Cloudflare 在不同 data center 的 edge server）。單條連線斷線時，流量自動切到其餘連線，外部使用者感受不到中斷。4 條連線全部斷開才會觸發完全不可達。</p>
<p>Tailscale 的冗餘模型不同：WireGuard tunnel 是點對點連線，沒有多邊緣節點分散。Tailscale 的高可用靠 DERP relay server 做中繼——直連失敗時退到 relay，延遲增加但可達性維持。</p>
<p>這個差異在穩定性預期上很重要：cloudflared 的可達性依賴 Cloudflare 邊緣網路的多點冗餘，Tailscale 的可達性依賴直連品質與 DERP 中繼。選擇時要問「我的網路環境是否穩定到不需要多連線冗餘」。</p>
<h2 id="故障模式network-層與-application-層的分離">故障模式：network 層與 application 層的分離</h2>
<p>tunnel 斷線跟 LB health check 失敗是不同層的故障。LB health check 失敗多半是 application 層（後端掛了、依賴不通）；tunnel 斷線常是 network 層（邊緣連線中斷、本機外連受阻、供應商側問題）、而後端服務本身完全健康。事故判讀要先分清這兩層：後端 log 一切正常、但外部全部連不進來、第一個要看的是 tunnel 進程的連線狀態、不是後端。</p>
<p>這也改變監控訊號的設計。LB 場景看後端 5xx 與 latency 就能覆蓋多數入口問題；tunnel 場景要額外監控隧道本身的連線狀態與重連次數——隧道靜默斷掉時、後端指標一片祥和、唯一的訊號在 tunnel 進程那邊。</p>
<h3 id="故障分類與判讀順序">故障分類與判讀順序</h3>
<p>tunnel 環境下的故障可按層級分類，判讀順序從外到內：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>症狀</th>
          <th>判讀第一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>供應商邊緣</td>
          <td>所有 tunnel 用戶同時受影響</td>
          <td>查供應商 status page</td>
      </tr>
      <tr>
          <td>本機外連</td>
          <td>單一 tunnel 斷線、其他外連也有問題</td>
          <td>查本機網路、NAT、防火牆</td>
      </tr>
      <tr>
          <td>tunnel 進程</td>
          <td>tunnel 進程 crash 或 hang</td>
          <td>查 tunnel 進程 log 與 restart 狀態</td>
      </tr>
      <tr>
          <td>後端服務</td>
          <td>tunnel 正常但外部拿到 502</td>
          <td>查後端服務 readiness</td>
      </tr>
      <tr>
          <td>認證閘道</td>
          <td>tunnel + 後端正常但外部拿到 403</td>
          <td>查認證設定（token / ACL 過期）</td>
      </tr>
  </tbody>
</table>
<p>判讀順序的重點是「先確認 tunnel 層是否正常、再往內看」。如果跳過 tunnel 層直接排查後端，會在後端 log 一切正常的情況下浪費時間。</p>
<h2 id="認證必須疊在-tunnel-之後">認證必須疊在 tunnel 之後</h2>
<p>tunnel 把後端的可達性開到了外部、但它不認證。隧道網址可能從瀏覽器紀錄、分享連結、Referer 外洩、不該被當成安全機制。所以 tunnel 之後必須疊認證閘道、且預設拒絕 — 未通過認證的流量不該觸及後端。</p>
<p>常見的疊法是邊緣與本機各一層：邊緣層（cloudflared 配 Cloudflare Access service token、Tailscale 配 ACL）讓未授權流量在邊緣就被擋、根本到不了本機;本機層（反向代理驗共享密鑰 / basic auth）作為邊緣萬一失效的縱深。入口威脅建模見 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>;單人自用工具的裝置綁定認證見 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/#%e5%96%ae%e4%ba%ba%e8%a3%9d%e7%bd%ae%e8%aa%8d%e8%ad%89%e6%a8%a1%e5%9e%8b" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 單人裝置認證模型</a>。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>外部全部連不進來、後端 log 正常</td>
          <td>故障在 network 層、隧道斷線</td>
          <td>先查 tunnel 進程連線狀態、不是後端</td>
      </tr>
      <tr>
          <td>啟動後短時間外部拿到一批 502</td>
          <td>tunnel 比後端早 ready、導流量進空服務</td>
          <td>把 tunnel ready gate 在後端 readiness 後</td>
      </tr>
      <tr>
          <td>隧道頻繁重連、外部間歇中斷</td>
          <td>本機外連不穩或邊緣側抖動</td>
          <td>查 cloudflared / tailscaled 的重連 log、確認 backoff 間隔是否正常拉長</td>
      </tr>
      <tr>
          <td>拿到網址的人直接連到後端</td>
          <td>認證沒疊在 tunnel 之後、網址被當密碼</td>
          <td>補邊緣 / 本機認證閘道、預設拒絕</td>
      </tr>
      <tr>
          <td>部署切換隧道時對外中斷拉長</td>
          <td>關閉順序錯位、tunnel 未先收斂</td>
          <td>先停 tunnel 帶入新連線、再退後端</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 tunnel 網址當密碼、是最常見也最危險的誤判。網址不好猜不代表是祕密、它會從各種地方外洩、認證要靠 tunnel 之後的閘道、不是靠網址難猜。</p>
<p>把「後端健康」當成「外部可達」、忽略隧道本身是獨立的失效點。tunnel 場景的可達性是後端健康與隧道連線的交集、監控要覆蓋兩者。</p>
<p>把 tunnel 當「永久掛著」的常駐入口、放大暴露窗。自用場景常更適合用時起、用完關 — 暴露窗壓到最小;要常駐時、認證閘道與監控的投資等級要隨之上調。</p>
<p>把 tunnel 供應商視為零停機、不設本機降級預案。tunnel 依賴外部供應商的邊緣網路與協調伺服器，供應商事故期間本機服務完全健康但外部無法觸及。有降級需求的場景要準備替代入口路徑（如臨時開 port + 反向代理），或接受供應商 SLA 決定自身可用性。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 的交接：tunnel 的 startup / readiness / drain 對齊生命週期合約、只是 drain 對象換成隧道收斂。</li>
<li>與 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 的交接：tunnel 作為對外入口的威脅建模與認證疊法。</li>
<li>與 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a> 的交接：tunnel 憑證與認證閘道密鑰的保管與輪替。</li>
<li>與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 觀測</a> 的交接：隧道連線狀態與重連次數要進監控、否則 network 層故障無訊號。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 tunnel 入口放進整體生命週期、接著讀 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。要把 tunnel 之後的認證做紮實、接著讀 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 與 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>。判斷服務是否屬於個人自架工具形態、回 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。</p>
]]></content:encoded></item><item><title>Kubernetes Graceful Shutdown：termination 序列跟你以為的不一樣</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/graceful-shutdown/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/graceful-shutdown/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 K8s 在 deployment platform 譜系的定位、本文聚焦 &lt;em>pod termination&lt;/em> 這個 production 最常踩、被誤解最深的議題：序列、配置、五個 case、跟 service mesh 整合。&lt;/p>&lt;/blockquote>
&lt;h2 id="graceful-shutdown-沒做對500-期間每次-deploy-都吃-502">Graceful shutdown 沒做對、500 期間每次 deploy 都吃 502&lt;/h2>
&lt;p>最常見的觸發場景：deploy 新 image、prometheus alert 在 5 分鐘內收到一波 502 / 503、SRE 翻 application log 看到「正在處理 request」「connection closed」交替出現。Application 本身沒 bug、但 K8s 在 pod terminate 時跟 traffic 來源 &lt;em>沒對齊步調&lt;/em>、舊 pod 還在處理請求時就被 SIGKILL、新 request 還在打到準備關閉的 pod 上。&lt;/p>
&lt;p>很多團隊修法是 &lt;em>把 terminationGracePeriodSeconds 從 30 拉到 120&lt;/em>、暫時掩蓋問題；但症狀會在下次 rolling update / HPA scale-down / node drain 時換個形式回來。根因在 &lt;em>termination 序列&lt;/em> — pod 不是收到 SIGTERM 就 graceful、序列裡每一步出錯都有不同 fail mode。&lt;/p>
&lt;h2 id="termination-序列五步每步都能爆">Termination 序列：五步、每步都能爆&lt;/h2>
&lt;p>K8s 收到 delete pod 請求後、發生的事 &lt;em>按時間&lt;/em> 是：&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>t=0&lt;/td>
 &lt;td>API server 標 pod 為 Terminating&lt;/td>
 &lt;td>kubelet 收到 delete&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>t=0&lt;/td>
 &lt;td>Pod 從 Service Endpoints 移除（&lt;strong>async&lt;/strong>）&lt;/td>
 &lt;td>endpoint controller&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>t=0&lt;/td>
 &lt;td>kubelet 跑 preStop hook（若有定義）&lt;/td>
 &lt;td>container runtime&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>t=preStop 結束&lt;/td>
 &lt;td>container 收到 SIGTERM&lt;/td>
 &lt;td>container runtime&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>t=SIGTERM + terminationGracePeriodSeconds&lt;/td>
 &lt;td>container 收到 SIGKILL&lt;/td>
 &lt;td>container runtime&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵誤解：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>「pod 從 Service 移除」跟「container 收到 SIGTERM」是 &lt;em>平行&lt;/em>、不是序列&lt;/strong>。Endpoint controller 更新 Endpoints object → kube-proxy 重新寫 iptables → 各 node 的 traffic 才真正停 — 這條鏈通常需要 &lt;em>1-5 秒&lt;/em>；同時間 SIGTERM 已經發給 application。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>preStop hook 是「container 還在跑、SIGTERM 還沒發」期間執行&lt;/strong>。pre-Stop 設 &lt;code>sleep 10&lt;/code> 是 production 標準作法 — 用 sleep 讓 endpoint controller 有時間把 pod 從 Service 移除、避免 SIGTERM 期間還有新 request 進來。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a> overview 的 implementation-layer deep article。Overview 已說明 K8s 在 deployment platform 譜系的定位、本文聚焦 <em>pod termination</em> 這個 production 最常踩、被誤解最深的議題：序列、配置、五個 case、跟 service mesh 整合。</p></blockquote>
<h2 id="graceful-shutdown-沒做對500-期間每次-deploy-都吃-502">Graceful shutdown 沒做對、500 期間每次 deploy 都吃 502</h2>
<p>最常見的觸發場景：deploy 新 image、prometheus alert 在 5 分鐘內收到一波 502 / 503、SRE 翻 application log 看到「正在處理 request」「connection closed」交替出現。Application 本身沒 bug、但 K8s 在 pod terminate 時跟 traffic 來源 <em>沒對齊步調</em>、舊 pod 還在處理請求時就被 SIGKILL、新 request 還在打到準備關閉的 pod 上。</p>
<p>很多團隊修法是 <em>把 terminationGracePeriodSeconds 從 30 拉到 120</em>、暫時掩蓋問題；但症狀會在下次 rolling update / HPA scale-down / node drain 時換個形式回來。根因在 <em>termination 序列</em> — pod 不是收到 SIGTERM 就 graceful、序列裡每一步出錯都有不同 fail mode。</p>
<h2 id="termination-序列五步每步都能爆">Termination 序列：五步、每步都能爆</h2>
<p>K8s 收到 delete pod 請求後、發生的事 <em>按時間</em> 是：</p>
<table>
  <thead>
      <tr>
          <th>時序</th>
          <th>事件</th>
          <th>動作來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>t=0</td>
          <td>API server 標 pod 為 Terminating</td>
          <td>kubelet 收到 delete</td>
      </tr>
      <tr>
          <td>t=0</td>
          <td>Pod 從 Service Endpoints 移除（<strong>async</strong>）</td>
          <td>endpoint controller</td>
      </tr>
      <tr>
          <td>t=0</td>
          <td>kubelet 跑 preStop hook（若有定義）</td>
          <td>container runtime</td>
      </tr>
      <tr>
          <td>t=preStop 結束</td>
          <td>container 收到 SIGTERM</td>
          <td>container runtime</td>
      </tr>
      <tr>
          <td>t=SIGTERM + terminationGracePeriodSeconds</td>
          <td>container 收到 SIGKILL</td>
          <td>container runtime</td>
      </tr>
  </tbody>
</table>
<p>關鍵誤解：</p>
<ol>
<li>
<p><strong>「pod 從 Service 移除」跟「container 收到 SIGTERM」是 <em>平行</em>、不是序列</strong>。Endpoint controller 更新 Endpoints object → kube-proxy 重新寫 iptables → 各 node 的 traffic 才真正停 — 這條鏈通常需要 <em>1-5 秒</em>；同時間 SIGTERM 已經發給 application。</p>
</li>
<li>
<p><strong>preStop hook 是「container 還在跑、SIGTERM 還沒發」期間執行</strong>。pre-Stop 設 <code>sleep 10</code> 是 production 標準作法 — 用 sleep 讓 endpoint controller 有時間把 pod 從 Service 移除、避免 SIGTERM 期間還有新 request 進來。</p>
</li>
<li>
<p><strong>terminationGracePeriodSeconds 是 <em>從 preStop 開始</em> 計時、不是從 SIGTERM</strong>。preStop sleep 10s + application 30s graceful = 至少要設 40s。</p>
</li>
<li>
<p><strong>graceful 不是 framework 自動的</strong>。Application 必須 <em>主動處理 SIGTERM</em>：拒絕新 request、等 in-flight 完成、close DB connection、flush log。沒處理 SIGTERM、container 會在 grace period 後被強殺。</p>
</li>
<li>
<p><strong>readiness probe 在 Terminating 期間 <em>仍會被執行</em>、但結果不影響 traffic</strong>（已經從 Endpoints 移除）。但若 application 沒主動讓 readiness fail、service mesh / external LB 可能仍在送 request（依不同 mesh 行為）。</p>
</li>
</ol>
<h2 id="配置全圖">配置全圖</h2>
<h3 id="deployment-spec">Deployment spec</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">terminationGracePeriodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">          </span><span class="c"># SIGTERM 後 60s 才 SIGKILL</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">          </span><span class="nt">lifecycle</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">            </span><span class="nt">preStop</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">              </span><span class="nt">exec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">                </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/bin/sh&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-c&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;sleep 10&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">          </span><span class="nt">readinessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/healthz/ready</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">            </span><span class="nt">periodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">            </span><span class="nt">failureThreshold</span><span class="p">:</span><span class="w"> </span><span class="m">2</span></span></span></code></pre></div><p>時序：t=0 preStop 開始 sleep 10s → t=10s container SIGTERM → t=70s SIGKILL（不是 t=60s、是 60s after SIGTERM）。</p>
<h3 id="application-處理-sigtermgo-範例">Application 處理 SIGTERM（Go 範例）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">sigs</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">os</span><span class="p">.</span><span class="nx">Signal</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">signal</span><span class="p">.</span><span class="nf">Notify</span><span class="p">(</span><span class="nx">sigs</span><span class="p">,</span> <span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGTERM</span><span class="p">)</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="nx">server</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">http</span><span class="p">.</span><span class="nx">Server</span><span class="p">{</span><span class="nx">Addr</span><span class="p">:</span> <span class="s">&#34;:8080&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">go</span> <span class="nx">server</span><span class="p">.</span><span class="nf">ListenAndServe</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 class="o">&lt;-</span><span class="nx">sigs</span>                                              <span class="c1">// 等 SIGTERM</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;SIGTERM received, draining...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 1. readiness fail（讓 mesh-aware 流量停）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nx">ready</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="kc">false</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="c1">// 2. wait 5s 讓 readiness probe failureThreshold 觸發</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">5</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1">// 3. graceful shutdown server（拒新請求、等 in-flight）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="mi">45</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nx">server</span><span class="p">.</span><span class="nf">Shutdown</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">// 4. close DB / cache / message consumer</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="nx">consumer</span><span class="p">.</span><span class="nf">Stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1">// 5. flush log + exit</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Sync</span><span class="p">()</span></span></span></code></pre></div><p>關鍵：<code>server.Shutdown(ctx)</code> 是 <em>拒新請求、等 in-flight</em>、ctx timeout 設 <em>grace period 減去 preStop sleep 跟 readiness fail 等待時間</em>（60s - 10s - 5s = 45s）。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1rolling-update-期間-502--503">Case 1：Rolling update 期間 502 / 503</h3>
<p><strong>徵兆</strong>：每次 deploy 後 5 分鐘內 LB / ingress log 一波 502 / 503、application log 顯示「context canceled」「connection closed by peer」、新 pod 已 ready 但舊 pod 在 grace period 內仍收 request。</p>
<p><strong>根因</strong>：沒設 preStop sleep、container 收到 SIGTERM 後立刻 <code>server.Shutdown()</code>、但 kube-proxy 還沒把舊 pod 從 iptables 移除、新 request 持續送到舊 pod、舊 pod 已拒收。</p>
<p><strong>修法</strong>：preStop <code>sleep 10</code>、讓 endpoint propagation 完成再進入 SIGTERM 流程。</p>
<h3 id="case-2connection-drain-racelong-running-request-被中斷">Case 2：Connection drain race，long-running request 被中斷</h3>
<p><strong>徵兆</strong>：deploy 後 application log 有大量 <code>context canceled</code> 對應到 long-running endpoint（例：報表生成、檔案上傳）、user 端看到 transaction 失敗、但短 request 沒事。</p>
<p><strong>根因</strong>：long-running endpoint 處理時間 &gt; terminationGracePeriodSeconds、<code>server.Shutdown(ctx)</code> ctx timeout 設太短、in-flight 強制中斷。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>把 long-running endpoint 改 async（背景 job + status endpoint）、HTTP request 立刻 return job ID</li>
<li>短期：terminationGracePeriodSeconds 拉到 long-running 99 percentile + buffer</li>
<li>application 側 ctx timeout = grace period - preStop - readiness fail wait</li>
</ol>
<h3 id="case-3init-container-在-grace-period-期間重啟sigterm-沒到-main">Case 3：Init container 在 grace period 期間重啟、SIGTERM 沒到 main</h3>
<p><strong>徵兆</strong>：pod 顯示 Terminating 但 phase 一直在 Running、main container restart count + 1、application log 沒看到「SIGTERM received」。</p>
<p><strong>根因</strong>：init container 用 <code>restartPolicy: Always</code>（K8s 1.28+ sidecar 模式）、或 main container 在 SIGTERM 前先 crash 觸發 restart、kubelet 在 restart 後 <em>不重發 SIGTERM</em>、main container 跑到 grace period 結束直接 SIGKILL。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Sidecar container（restartPolicy: Always）的 preStop 也要設 <code>sleep</code>、跟 main 同 lifecycle</li>
<li>main container readinessProbe 失敗時 <em>別自動 restart</em>（restartPolicy: OnFailure + crashLoopBackOff 觀察）</li>
<li>觀察 <code>kubectl describe pod</code> 的 events、SIGTERM 沒發出來會有 <code>Killing container</code> event 缺失</li>
</ol>
<h3 id="case-4statefulset-串行終止總時間--pod-數--grace-period">Case 4：StatefulSet 串行終止、總時間 = pod 數 × grace period</h3>
<p><strong>徵兆</strong>：StatefulSet rolling update / scale-down 比 Deployment 慢 N 倍（N = replica 數）、deploy 一個 5 replica 的 statefulset 要 5 分鐘以上。</p>
<p><strong>根因</strong>：StatefulSet 預設 <code>podManagementPolicy: OrderedReady</code> — pod 串行終止 + 串行創建、每個 pod 至少要 grace period 完成才動下一個。Deployment 用 <code>RollingUpdate</code> 預設 maxUnavailable=25% 平行終止。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>StatefulSet 改 <code>podManagementPolicy: Parallel</code>（若 application 不要求嚴格順序）</li>
<li>嚴格順序情境（Cassandra / Kafka / etcd）保留 OrderedReady、但 grace period 設 <em>單 pod 必要時間</em>、不要設 <em>總時間能承受</em></li>
<li>接受序列化代價、把 deploy 排在低流量時段</li>
</ol>
<h3 id="case-5job--cronjob-不-gracefulsigterm-直接-sigkill">Case 5：Job / CronJob 不 graceful、SIGTERM 直接 SIGKILL</h3>
<p><strong>徵兆</strong>：CronJob 在 Job timeout / pod eviction 時不 graceful、寫一半的 file 留在 PVC、下次跑時 corrupt；application log 沒「SIGTERM received」、直接斷。</p>
<p><strong>根因</strong>：Job 的 <code>activeDeadlineSeconds</code> 到期 / node eviction 觸發時、K8s 對 Job pod <em>仍會發 SIGTERM</em>、但 <em>很多 batch framework（Spring Batch / Argo Workflow worker）沒處理 SIGTERM</em>、application 沒主動 checkpoint。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Batch application 處理 SIGTERM、checkpoint 進度寫 storage、下次跑時 resume</li>
<li>不適合 checkpoint 的 batch、保證 <em>idempotent re-run</em>、SIGKILL 後重跑不會 corrupt</li>
<li>Job spec 加 <code>terminationGracePeriodSeconds</code>（預設 30、batch 通常要 60-300）</li>
</ol>
<h2 id="規模影響">規模影響</h2>
<p>Graceful shutdown 的成本主要在 <em>deploy 時間</em> 跟 <em>capacity buffer</em>：</p>
<table>
  <thead>
      <tr>
          <th>規模因素</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>terminationGracePeriod 60s</td>
          <td>單 pod deploy ~70-80s（含 preStop + grace + new pod startup）</td>
      </tr>
      <tr>
          <td>Deployment 100 replica + maxSurge 25%</td>
          <td>全 deploy ~5-10 分鐘、需要 <em>25% extra capacity</em>（25 replica buffer）</td>
      </tr>
      <tr>
          <td>StatefulSet 串行 + 60s grace</td>
          <td>10 replica 約 10-12 分鐘、deploy window 要在低流量時段</td>
      </tr>
      <tr>
          <td>HPA scale-down 跟 graceful 一起跑</td>
          <td>scale-down 觸發 → preStop + grace + new metric → 下次 scale 判斷、avg 反應週期 ≈ 3-5 分鐘</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>Web service：<code>terminationGracePeriodSeconds: 60</code>、preStop sleep 10、application graceful 45s</li>
<li>Backend worker（消費 queue）：<code>terminationGracePeriodSeconds: 120</code>、preStop 不 sleep（用 readiness 控）、application 處理當前 message + commit offset</li>
<li>Batch job：<code>terminationGracePeriodSeconds: 300</code>、checkpoint pattern</li>
<li>StatefulSet（DB / queue）：grace period 對齊 vendor 建議（Kafka 90s、PostgreSQL 60s）</li>
</ul>
<h2 id="跟其他元件整合">跟其他元件整合</h2>
<h3 id="service-meshistio--linkerd">Service mesh（Istio / Linkerd）</h3>
<p>Service mesh sidecar（envoy / linkerd-proxy）也有自己的 termination — 通常比 main container 晚一點關。配置原則：</p>
<ol>
<li>mesh sidecar 設 <code>terminationGracePeriodSeconds</code> 比 main 多 5-10s、main 處理完才換 sidecar</li>
<li>Istio 1.12+ 的 <code>proxy.istio.io/config.holdApplicationUntilProxyStarts</code> 控啟動順序、shutdown 也要對應</li>
<li>mTLS 環境 graceful 多一道：在 SIGTERM 後等 mesh 主動 close cert rotation、不要硬斷</li>
</ol>
<h3 id="readiness-probe-跟-mesh-aware-traffic">Readiness probe 跟 mesh-aware traffic</h3>
<p>純 K8s Service（kube-proxy iptables）：endpoint 移除後 <em>已建立 connection 仍會跑完</em>、新 connection 不來。Mesh-aware traffic（service mesh / external LB with health check）：要 readiness fail 才會停送。</p>
<p>修法：application graceful 第一步是 <code>ready.Store(false)</code> + 等 readiness probe 至少 fail 一次（5-10s）、才開始 server.Shutdown。</p>
<h3 id="跟-pod-disruption-budgetpdb的衝突">跟 Pod Disruption Budget（PDB）的衝突</h3>
<p>Node drain 時 PDB 限制可同時 unavailable 的 pod 數、graceful shutdown 拖長會讓 drain 卡住。對策：</p>
<ol>
<li>緊急 drain（node 硬體故障）：<code>kubectl drain --grace-period=30 --force</code>、接受短時間 502</li>
<li>正常 drain（升級 / 維運）：PDB 設 <code>minAvailable: &lt;replicas-1&gt;</code>、容許單 pod 慢慢 graceful</li>
<li>不要設 <code>maxUnavailable: 0</code>、會讓 drain 卡死</li>
</ol>
<h2 id="下一步">下一步</h2>
<ul>
<li><strong>Application graceful 寫法</strong>：<a href="https://12factor.net/disposability">12-factor app</a> disposability 章節給 framework-agnostic 模板、各語言 SDK 寫法見對應 framework</li>
<li><strong>Queue consumer 的 graceful</strong>：訊息 ack / offset commit 必須在 SIGTERM 內完成、否則 duplicate message — 對應 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a> 模組的 consumer-design 段</li>
<li><strong>跨 region / 多 cluster 的 graceful</strong>：multi-cluster service mesh（Istio multicluster / Linkerd multicluster）的 traffic shift 期間 graceful 行為跟單 cluster 不同、需要對齊 mesh 配置</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a></li>
<li>上游 chapter：<a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.X deployment-rollout-drain-rollback</a></li>
<li>對照案例：rolling update 期間 502 多見於 stage-3 mesh adoption case 庫</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a> / <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault Dynamic Credential</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>4.10 衍生產物管理原理：什麼進 git、什麼不該</title><link>https://tarrragon.github.io/blog/llm/04-applications/artifact-management/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/artifact-management/</guid><description>&lt;p>LLM 應用的 codebase 不只 source code、還含 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/embedding-model/" data-link-title="Embedding Model" data-link-desc="把文字轉成向量的模型：用於 codebase 索引與語意搜尋">embedding&lt;/a> index、cache、model weights、prompt config、lockfile、log 等各種「衍生」或「外部」產物。每個產物該不該進 git、有沒有共通邏輯？&lt;/p>
&lt;p>本章寫的是「&lt;strong>source / derived / external 三類產物的判讀框架&lt;/strong>」、跟「production deployment 怎麼處理 share + reproducibility 取捨」。對應到 hands-on 系列實際遇到的問題——為什麼 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG&lt;/a> demo 的 &lt;code>index.pkl&lt;/code> 進 &lt;code>.gitignore&lt;/code>、Hugging Face model weights 為什麼不能塞進 repo、prompt template 該怎麼版本管理。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">4.9 Production resource planning&lt;/a> 對應「production 怎麼跑」、本章對應「production 怎麼版本控制 + 部署」。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後你能：&lt;/p>
&lt;ol>
&lt;li>用「source / derived / external」三分類判讀任何產物該不該進 git。&lt;/li>
&lt;li>看到 &lt;code>.gitignore&lt;/code> 設計、能解釋每條規則的邏輯。&lt;/li>
&lt;li>在 reproducibility 跟 repo 大小之間做合理取捨。&lt;/li>
&lt;li>知道 derived / external 產物該用什麼機制 share（registry、build script、artifact storage）。&lt;/li>
&lt;/ol>
&lt;h2 id="三類產物-framework">三類產物 framework&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類別&lt;/th>
 &lt;th>定義&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;th>該進 git？&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Source&lt;/strong>&lt;/td>
 &lt;td>人類撰寫、是真理來源&lt;/td>
 &lt;td>code、prompt template、test fixture、config schema&lt;/td>
 &lt;td>必須&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Derived&lt;/strong>&lt;/td>
 &lt;td>從 source 自動產出、可重建&lt;/td>
 &lt;td>binary、index、cache、compiled output、generated docs&lt;/td>
 &lt;td>不該&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>External&lt;/strong>&lt;/td>
 &lt;td>從外部下載、跟 source 解耦&lt;/td>
 &lt;td>model weights、dependency package、dataset&lt;/td>
 &lt;td>用 registry / manifest&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判讀問題：「&lt;strong>刪掉重來、用什麼能 reconstruct 一模一樣？&lt;/strong>」&lt;/p>
&lt;ul>
&lt;li>用人手寫 → source、必須 commit&lt;/li>
&lt;li>用 build script + source → derived、commit manifest（如 lockfile）不 commit output&lt;/li>
&lt;li>用 download script + URL → external、commit URL 不 commit content&lt;/li>
&lt;/ul>
&lt;p>這個 framework 跨任何技術 stack 都成立（不只 LLM）、但 LLM 應用尤其放大 derived / external 比例。&lt;/p>
&lt;h2 id="llm-應用具體對應">LLM 應用具體對應&lt;/h2>
&lt;h3 id="source進-git">Source（進 git）&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>產物&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>程式 source code&lt;/td>
 &lt;td>wrapper script、framework 整合 code&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Prompt template&lt;/td>
 &lt;td>system prompt、few-shot example、prompt structure&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Config schema&lt;/td>
 &lt;td>哪些參數可調、合法範圍、default value&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test fixture&lt;/td>
 &lt;td>測試輸入 / 預期輸出 pair&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Markdown content（如本 blog）&lt;/td>
 &lt;td>文章本身就是 source&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>.gitignore&lt;/code> / lock file 規則&lt;/td>
 &lt;td>描述哪些不進 git 也是 source&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Build script&lt;/td>
 &lt;td>&lt;code>ingest.py&lt;/code>、&lt;code>build.sh&lt;/code>、能從 source 重建 derived&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="derived不進-git但-build-path-進-git">Derived（不進 git、但 build path 進 git）&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>產物&lt;/th>
 &lt;th>為什麼不 commit&lt;/th>
 &lt;th>怎麼 share&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>index.pkl&lt;/code>（RAG embedding index）&lt;/td>
 &lt;td>從 corpus + embedding model 重建、跟 model 版本綁、3.7 MB-GB 級&lt;/td>
 &lt;td>&lt;code>ingest.py&lt;/code> script、跑一次就 reconstruct&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Embedding cache（per-document hash）&lt;/td>
 &lt;td>跑時動態建、避免重 embed 同 chunk&lt;/td>
 &lt;td>不 share、各自 rebuild&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Python &lt;code>__pycache__/&lt;/code>&lt;/td>
 &lt;td>跑時自動產、Python 版本敏感&lt;/td>
 &lt;td>不 share、各自 rebuild&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Compiled binary（如 &lt;code>bin/mdtools&lt;/code>）&lt;/td>
 &lt;td>從 Go source build、平台敏感&lt;/td>
 &lt;td>source + build instructions、可選 release page 提供&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Generated docs（如 Hugo &lt;code>public/&lt;/code>）&lt;/td>
 &lt;td>從 markdown source build、deploy 時自動生&lt;/td>
 &lt;td>source + deploy pipeline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Log files&lt;/td>
 &lt;td>runtime output、量大、有 PII 風險&lt;/td>
 &lt;td>不 share、log retention 政策另立&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="external不進-git用-manifest--registry">External（不進 git、用 manifest / registry）&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>產物&lt;/th>
 &lt;th>Manifest / registry&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>LLM model weights&lt;/td>
 &lt;td>Hugging Face / Ollama registry tag&lt;/td>
 &lt;td>&lt;code>nomic-embed-text:latest&lt;/code>、&lt;code>sd_xl_base_1.0&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Python dependency&lt;/td>
 &lt;td>&lt;code>requirements.txt&lt;/code> / &lt;code>pyproject.toml&lt;/code>&lt;/td>
 &lt;td>&lt;code>requests==2.31.0&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Node modules&lt;/td>
 &lt;td>&lt;code>package.json&lt;/code> + &lt;code>package-lock.json&lt;/code>&lt;/td>
 &lt;td>&lt;code>react@18.2.0&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dataset&lt;/td>
 &lt;td>&lt;code>data.dvc&lt;/code> / S3 URL + checksum&lt;/td>
 &lt;td>training data、eval set&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Docker image&lt;/td>
 &lt;td>&lt;code>Dockerfile&lt;/code> + image tag&lt;/td>
 &lt;td>&lt;code>python:3.11-slim&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>External 跟 derived 的差別：external 來自 git 外的 source、derived 來自 git 內的 source。&lt;strong>機制上都用同套路徑&lt;/strong>——manifest 進 git、實際 bytes 存 registry、避免大檔直接進 commit history。&lt;/p></description><content:encoded><![CDATA[<p>LLM 應用的 codebase 不只 source code、還含 <a href="/blog/llm/knowledge-cards/embedding-model/" data-link-title="Embedding Model" data-link-desc="把文字轉成向量的模型：用於 codebase 索引與語意搜尋">embedding</a> index、cache、model weights、prompt config、lockfile、log 等各種「衍生」或「外部」產物。每個產物該不該進 git、有沒有共通邏輯？</p>
<p>本章寫的是「<strong>source / derived / external 三類產物的判讀框架</strong>」、跟「production deployment 怎麼處理 share + reproducibility 取捨」。對應到 hands-on 系列實際遇到的問題——為什麼 <a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a> demo 的 <code>index.pkl</code> 進 <code>.gitignore</code>、Hugging Face model weights 為什麼不能塞進 repo、prompt template 該怎麼版本管理。</p>
<p>跟 <a href="/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">4.9 Production resource planning</a> 對應「production 怎麼跑」、本章對應「production 怎麼版本控制 + 部署」。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後你能：</p>
<ol>
<li>用「source / derived / external」三分類判讀任何產物該不該進 git。</li>
<li>看到 <code>.gitignore</code> 設計、能解釋每條規則的邏輯。</li>
<li>在 reproducibility 跟 repo 大小之間做合理取捨。</li>
<li>知道 derived / external 產物該用什麼機制 share（registry、build script、artifact storage）。</li>
</ol>
<h2 id="三類產物-framework">三類產物 framework</h2>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>定義</th>
          <th>例子</th>
          <th>該進 git？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Source</strong></td>
          <td>人類撰寫、是真理來源</td>
          <td>code、prompt template、test fixture、config schema</td>
          <td>必須</td>
      </tr>
      <tr>
          <td><strong>Derived</strong></td>
          <td>從 source 自動產出、可重建</td>
          <td>binary、index、cache、compiled output、generated docs</td>
          <td>不該</td>
      </tr>
      <tr>
          <td><strong>External</strong></td>
          <td>從外部下載、跟 source 解耦</td>
          <td>model weights、dependency package、dataset</td>
          <td>用 registry / manifest</td>
      </tr>
  </tbody>
</table>
<p>判讀問題：「<strong>刪掉重來、用什麼能 reconstruct 一模一樣？</strong>」</p>
<ul>
<li>用人手寫 → source、必須 commit</li>
<li>用 build script + source → derived、commit manifest（如 lockfile）不 commit output</li>
<li>用 download script + URL → external、commit URL 不 commit content</li>
</ul>
<p>這個 framework 跨任何技術 stack 都成立（不只 LLM）、但 LLM 應用尤其放大 derived / external 比例。</p>
<h2 id="llm-應用具體對應">LLM 應用具體對應</h2>
<h3 id="source進-git">Source（進 git）</h3>
<table>
  <thead>
      <tr>
          <th>產物</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>程式 source code</td>
          <td>wrapper script、framework 整合 code</td>
      </tr>
      <tr>
          <td>Prompt template</td>
          <td>system prompt、few-shot example、prompt structure</td>
      </tr>
      <tr>
          <td>Config schema</td>
          <td>哪些參數可調、合法範圍、default value</td>
      </tr>
      <tr>
          <td>Test fixture</td>
          <td>測試輸入 / 預期輸出 pair</td>
      </tr>
      <tr>
          <td>Markdown content（如本 blog）</td>
          <td>文章本身就是 source</td>
      </tr>
      <tr>
          <td><code>.gitignore</code> / lock file 規則</td>
          <td>描述哪些不進 git 也是 source</td>
      </tr>
      <tr>
          <td>Build script</td>
          <td><code>ingest.py</code>、<code>build.sh</code>、能從 source 重建 derived</td>
      </tr>
  </tbody>
</table>
<h3 id="derived不進-git但-build-path-進-git">Derived（不進 git、但 build path 進 git）</h3>
<table>
  <thead>
      <tr>
          <th>產物</th>
          <th>為什麼不 commit</th>
          <th>怎麼 share</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>index.pkl</code>（RAG embedding index）</td>
          <td>從 corpus + embedding model 重建、跟 model 版本綁、3.7 MB-GB 級</td>
          <td><code>ingest.py</code> script、跑一次就 reconstruct</td>
      </tr>
      <tr>
          <td>Embedding cache（per-document hash）</td>
          <td>跑時動態建、避免重 embed 同 chunk</td>
          <td>不 share、各自 rebuild</td>
      </tr>
      <tr>
          <td>Python <code>__pycache__/</code></td>
          <td>跑時自動產、Python 版本敏感</td>
          <td>不 share、各自 rebuild</td>
      </tr>
      <tr>
          <td>Compiled binary（如 <code>bin/mdtools</code>）</td>
          <td>從 Go source build、平台敏感</td>
          <td>source + build instructions、可選 release page 提供</td>
      </tr>
      <tr>
          <td>Generated docs（如 Hugo <code>public/</code>）</td>
          <td>從 markdown source build、deploy 時自動生</td>
          <td>source + deploy pipeline</td>
      </tr>
      <tr>
          <td>Log files</td>
          <td>runtime output、量大、有 PII 風險</td>
          <td>不 share、log retention 政策另立</td>
      </tr>
  </tbody>
</table>
<h3 id="external不進-git用-manifest--registry">External（不進 git、用 manifest / registry）</h3>
<table>
  <thead>
      <tr>
          <th>產物</th>
          <th>Manifest / registry</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>LLM model weights</td>
          <td>Hugging Face / Ollama registry tag</td>
          <td><code>nomic-embed-text:latest</code>、<code>sd_xl_base_1.0</code></td>
      </tr>
      <tr>
          <td>Python dependency</td>
          <td><code>requirements.txt</code> / <code>pyproject.toml</code></td>
          <td><code>requests==2.31.0</code></td>
      </tr>
      <tr>
          <td>Node modules</td>
          <td><code>package.json</code> + <code>package-lock.json</code></td>
          <td><code>react@18.2.0</code></td>
      </tr>
      <tr>
          <td>Dataset</td>
          <td><code>data.dvc</code> / S3 URL + checksum</td>
          <td>training data、eval set</td>
      </tr>
      <tr>
          <td>Docker image</td>
          <td><code>Dockerfile</code> + image tag</td>
          <td><code>python:3.11-slim</code></td>
      </tr>
  </tbody>
</table>
<p>External 跟 derived 的差別：external 來自 git 外的 source、derived 來自 git 內的 source。<strong>機制上都用同套路徑</strong>——manifest 進 git、實際 bytes 存 registry、避免大檔直接進 commit history。</p>
<h2 id="為什麼-derived--external-不該進-git">為什麼 derived / external 不該進 git</h2>
<p>每條限制有具體技術理由：</p>
<h3 id="size">Size</h3>
<p>Git 設計給 source code（小、純文字、頻繁 diff）。Derived / external 通常大、binary、不適合：</p>
<ul>
<li>Git 對 large binary 沒有有效 delta 演算法、每次小改 → 完整 copy 進 history</li>
<li>Repo size 線性漲、clone 變慢、CI cache 爆炸</li>
<li>GitHub 等服務有 file size 上限（GitHub 100 MB / file）</li>
</ul>
<p>實例：<code>scripts/rag-demo/index.pkl</code> 3.7 MB、每次 corpus 改 → 重 ingest → 整檔變。Commit 100 次 = git history 多 370 MB。Clone 痛。</p>
<h3 id="reproducibility反直覺">Reproducibility（反直覺）</h3>
<p>直覺：「commit derived 保證每個 clone 都拿到一樣的 output」——錯。</p>
<p>實際：</p>
<ul>
<li>Derived 跟 build env 綁（Python 3.13 build 的 pickle 在 3.14 不一定能 load）</li>
<li>Embedding index 跟 model version 綁（pull 不同 model 結果不同）</li>
<li>用舊 commit 的 derived 跑在新 env 反而比 rebuild 更脆弱</li>
</ul>
<p>正確 reproducibility 機制：commit <strong>build instruction + lockfile</strong>、別人 rebuild 時用同樣輸入產同樣 output。</p>
<h3 id="update-frequency-mismatch">Update frequency mismatch</h3>
<p>Source 改慢、derived 改快。<code>content/</code> 加一句話、<code>index.pkl</code> 整個重建。如果都進 git：</p>
<ul>
<li>90% 的 commit 是「rebuild artifact」、語意上不是真正的「source change」</li>
<li>git log 看不出真正 source 改動</li>
<li>diff review 被 derived noise 淹沒</li>
</ul>
<h3 id="cost--performance">Cost / Performance</h3>
<p>CI / CD pipeline 通常自動 rebuild derived。不 commit 反而：</p>
<ul>
<li>Source-only PR 較易 review（沒 generated diff）</li>
<li>CI build cache 重用、不需從 git 拉 derived</li>
<li>Deploy artifact registry 跟 git 分離、各自 scale</li>
</ul>
<h2 id="llm-應用-gitignore-設計模式">LLM 應用 <code>.gitignore</code> 設計模式</h2>
<p>LLM 應用典型 <code>.gitignore</code> 結構：</p>





<pre tabindex="0"><code class="language-gitignore" data-lang="gitignore"># === Source-side build output (derived) ===
# Compiled binaries
bin/
dist/
build/
*.pyc
__pycache__/

# Hugo / static site generators
public/
.hugo_build.lock
resources/

# RAG / vector indexes (regenerable)
scripts/rag-demo/index.pkl
*.pkl
*.index

# Embedding caches
.embedding_cache/
.vector_cache/

# === External-bound (don&#39;t commit, use manifest) ===
# Python deps (commit requirements.txt instead)
.venv/
venv/
env/

# Node deps
node_modules/

# Model weights / large files
*.safetensors
*.gguf
*.onnx
*.bin

# Datasets
data/raw/
data/processed/

# === Runtime / Local ===
# Logs
*.log
logs/

# OS / IDE
.DS_Store
.vscode/
.idea/

# Local secrets / API keys
.env
.env.local
*.key

# Temp / cache
*.tmp
.cache/</code></pre><h3 id="邊界-case-思考">邊界 case 思考</h3>
<p>幾個容易誤判的：</p>
<table>
  <thead>
      <tr>
          <th>產物</th>
          <th>該不該 commit</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>package-lock.json</code> / <code>poetry.lock</code></td>
          <td>commit</td>
          <td>是 manifest、保證 reproducibility</td>
      </tr>
      <tr>
          <td><code>node_modules/</code></td>
          <td>不 commit</td>
          <td>是 derived、可從 lockfile 重建</td>
      </tr>
      <tr>
          <td>小型 fixture data（&lt; 1 MB）</td>
          <td>commit（作 source）</td>
          <td>是 test 的一部分、不 reconstruct</td>
      </tr>
      <tr>
          <td>大型 eval dataset（&gt; 100 MB）</td>
          <td>用 dvc / S3 manifest</td>
          <td>量大、改用 dvc / S3 manifest 管理</td>
      </tr>
      <tr>
          <td>Pre-built model 用於 demo</td>
          <td>用 release artifact / Hugging Face</td>
          <td>量大、版本要可追蹤</td>
      </tr>
      <tr>
          <td>Prompt template (markdown / yaml)</td>
          <td>commit</td>
          <td>是 source、影響行為、要 diff</td>
      </tr>
      <tr>
          <td>從 LLM 生的 sample output</td>
          <td>不 commit（除非當 fixture）</td>
          <td>是 demo artifact、不 reconstruct 來源</td>
      </tr>
  </tbody>
</table>
<p>判讀 heuristic：</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">這個檔案、半年後 production deploy 時要不要存在？
</span></span><span class="line"><span class="ln">2</span><span class="cl">├─ 要：source 或 manifest 進 git
</span></span><span class="line"><span class="ln">3</span><span class="cl">└─ 不要：runtime / 開發環境 only、用 .gitignore</span></span></code></pre></div><h3 id="三分類的退化情境">三分類的退化情境</h3>
<p>三分類是 default framework、實務上有幾類「該不該 commit 的判讀走兩條岔路」的情境、需要特別判讀：</p>
<ul>
<li><strong>Generated client SDK in monorepo</strong>：protobuf / OpenAPI spec 產出的 client code 屬於 derived（從 .proto / .yaml 生）、但 monorepo 場景常 commit 進去、目的是「跨語言版本對齊 + CI 不用每次重生」。判讀：若 .proto / spec 改動頻率低 + 跨語言一致性比 build 速度重要、commit；變動頻繁就回到 derived 路徑。</li>
<li><strong>Jupyter notebook 的 output cell</strong>：技術上是 derived（執行 notebook 產出）、但語意上常被視為 source 的一部分（教學、demo、結果展示）。判讀：教學 / 展示 / 帶 figures 的 notebook 通常 commit 含 output；機械化的 batch run / CI notebook 走 derived、用 nbstripout 清掉 output 再 commit。</li>
<li><strong>Git LFS / git-annex 介於 commit 跟 manifest 之間</strong>：把大檔案 commit 進 git 但實際 bytes 存 LFS server、worktree 看起來像直接 commit、metadata 卻是 manifest pointer。判讀：適合「需要在 git history 中追蹤大檔案版本、但不想讓 repo 體積爆炸」的場景（如 game asset、訓練資料集 snapshot）。介於 commit 跟 dvc / S3 manifest 之間的折衷選項。</li>
<li><strong>Lockfile vs build artifact 的灰色帶</strong>：<code>yarn-error.log</code> 算 log（不 commit）還是 derived 但對 debug 重要（commit）？實務上多數選 .gitignore、但若團隊在 CI 失敗時要 reproduce 環境、保留少量 build log 也合理。</li>
</ul>
<p>判讀原則：三分類給 default、灰色帶用「reproducibility + 變動頻率 + 團隊協作需求」三軸決定具體路徑。</p>
<h2 id="source--derived--external-的-share-機制">Source / Derived / External 的 share 機制</h2>
<p>不 commit 不代表不 share、只是用對的 channel。</p>
<h3 id="source-share--git">Source share = git</h3>
<p>直接 clone 即可。</p>
<h3 id="derived-share-三種模式">Derived share 三種模式</h3>
<ol>
<li><strong>Build script in repo</strong>：別人 clone 後跑 script 重建（本 blog 用這條：<code>ingest.py</code> 重建 index）
<ul>
<li>優點：無外部依賴、self-contained</li>
<li>缺點：每個 clone 都要重跑、累積 compute time</li>
</ul>
</li>
<li><strong>Release artifact</strong>：把 build output 上傳 GitHub Releases / S3、clone 後下載
<ul>
<li>優點：clone 快、不用各自 rebuild</li>
<li>缺點：要 maintain release pipeline、artifact 版本管理另立</li>
</ul>
</li>
<li><strong>Artifact registry</strong>：用 OCI registry、Docker registry、artifact storage（如 GitHub Packages / JFrog Artifactory）
<ul>
<li>優點：production-grade、跨 team / 跨 org share</li>
<li>缺點：複雜、配 auth、cost</li>
</ul>
</li>
</ol>
<p>選擇：小專案用 script、中型用 release、大型 / 多人 collaboration 用 registry。</p>
<h3 id="external-share--manifest">External share = manifest</h3>
<p>把「<strong>從哪下載 + checksum</strong>」commit 進 git、實際 content 不進。常見 manifest format：</p>
<table>
  <thead>
      <tr>
          <th>Manifest</th>
          <th>描述</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>requirements.txt</code> / <code>pyproject.toml</code></td>
          <td>Python deps + version</td>
      </tr>
      <tr>
          <td><code>package.json</code> + <code>package-lock.json</code></td>
          <td>Node deps + exact version + integrity hash</td>
      </tr>
      <tr>
          <td><code>Dockerfile</code></td>
          <td>OS + 環境 + 依賴 + entrypoint</td>
      </tr>
      <tr>
          <td><code>dvc.yaml</code> + <code>dvc.lock</code></td>
          <td>dataset + model version</td>
      </tr>
      <tr>
          <td>Ollama Modelfile（如果寫了）</td>
          <td>LLM model + system prompt 組合</td>
      </tr>
      <tr>
          <td><code>Cargo.lock</code> / <code>go.sum</code></td>
          <td>Rust / Go 的 dep checksum</td>
      </tr>
  </tbody>
</table>
<p>Manifest 自己是 source（人寫、進 git）、它指向的 external content 不進 git（用 download script 取回）。</p>
<h2 id="prompt-跟-config-的版本控制">Prompt 跟 config 的版本控制</h2>
<p>LLM 應用特有的問題：<strong>prompt template 是 source、但 prompt 改變影響行為跟 derived 改變不同</strong>。</p>
<table>
  <thead>
      <tr>
          <th>Prompt 操作</th>
          <th>git 行為</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>改一個字</td>
          <td>一個 commit</td>
          <td>模型行為可能大變、要重跑 eval</td>
      </tr>
      <tr>
          <td>加 few-shot example</td>
          <td>一個 commit</td>
          <td>同上</td>
      </tr>
      <tr>
          <td>換不同模型（在 config）</td>
          <td>config commit</td>
          <td>用 prompt 沒變、行為變</td>
      </tr>
  </tbody>
</table>
<p>Prompt + model 是一對組合、行為相依、改一個都要重 test。建議在 commit message / PR description 描述「這個 prompt 改動的 expected behavior change」、用規格層級的 review 對待、勿視為 trivial 小改。</p>
<h3 id="prompt-跟-evaluation-一起管理">Prompt 跟 evaluation 一起管理</h3>
<p>進階做法：每個 prompt 配 evaluation set、commit 在同 PR：</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">prompts/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── code_review.md           ← prompt template
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── code_review_eval.json    ← input + expected output pair
</span></span><span class="line"><span class="ln">4</span><span class="cl">└── code_review_history.md   ← 改動記錄 + 對應 eval score</span></span></code></pre></div><p>每次改 prompt、跑 eval、比較 score、進 commit message。這比「改完 push 看看效果」可控很多、是 prompt engineering 的基本姿勢。</p>
<h2 id="production-deployment-的對接">Production deployment 的對接</h2>
<p>本地 hands-on 跟 production 對應：</p>
<table>
  <thead>
      <tr>
          <th>本地 hands-on</th>
          <th>Production</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>python ingest.py</code> build index</td>
          <td>Build pipeline 跑同樣 script、output 進 artifact storage</td>
      </tr>
      <tr>
          <td><code>ollama pull nomic-embed-text</code></td>
          <td>Container image 預載 model 或 mount volume</td>
      </tr>
      <tr>
          <td><code>.gitignore</code> 排除 index.pkl</td>
          <td>CI 自動 rebuild、deploy 時讀 artifact storage</td>
      </tr>
      <tr>
          <td>Source code 進 git</td>
          <td>Source 觸發 CI、build &amp; deploy</td>
      </tr>
  </tbody>
</table>
<p>成熟的 LLM 應用部署 pipeline：</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">Source change → git push
</span></span><span class="line"><span class="ln">2</span><span class="cl">              → CI triggered
</span></span><span class="line"><span class="ln">3</span><span class="cl">              → Build derived artifacts (index, container image)
</span></span><span class="line"><span class="ln">4</span><span class="cl">              → Run evaluation suite (prompt + model behavior tests)
</span></span><span class="line"><span class="ln">5</span><span class="cl">              → Push artifacts to registry
</span></span><span class="line"><span class="ln">6</span><span class="cl">              → Deploy with manifest pointing to specific artifact version
</span></span><span class="line"><span class="ln">7</span><span class="cl">              → Smoke test against production data
</span></span><span class="line"><span class="ln">8</span><span class="cl">              → Auto-rollback if metrics regress</span></span></code></pre></div><p>每一步都要 commit-able 的 manifest。在可審計 / 多人協作 / 有 SLA 承諾的場景、「手動 build 完 ssh 進 prod scp」這種 ad-hoc 流程會破壞 reproducibility、出問題時無法 revert 到具體 build；早期 prototype / 單人專案 / 一次性 demo 可接受 ad-hoc 流程、進入 production 前再改成 manifest-based。Manifest 是 reproducibility 跟 audit 的基礎。</p>
<h2 id="何時這篇會過時">何時這篇會過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>Source / derived / external 三分類 framework</li>
<li>「commit manifest、不 commit content」核心原則</li>
<li><code>.gitignore</code> 通用模式</li>
<li>Reproducibility 來自 build instruction、不來自 commit derived</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 manifest format（半年一個新 lockfile 格式）</li>
<li>Artifact registry 主流（OCI / Conda / npm 等都會演化）</li>
<li>LLM model registry（Hugging Face / Ollama 都會演化）</li>
</ul>
<p>新 lock 格式 / registry 出來時、回到三分類問：它解的是哪類產物？我能用它 commit manifest 不 commit content 嗎？通常答案 yes。</p>
<h2 id="跟其他章節的關係">跟其他章節的關係</h2>
<ul>
<li><a href="https://github.com/tarrragon/blog/blob/main/scripts/README.md">scripts/README.md</a>：本章原理的實作 reference</li>
<li><a href="/blog/llm/01-local-llm-services/hands-on/quickstart/" data-link-title="Hands-on Quickstart：clone repo 後跑通所有 demo" data-link-desc="4 步驟跑通 RAG / MCP / permission demo 的 setup 跟驗證指令、整合 hands-on 系列所有章節的 prerequisite">Hands-on quickstart</a>：跑通 demo 步驟、為什麼要 rebuild <code>index.pkl</code></li>
<li><a href="/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">4.9 Production resource planning</a>：production runtime 視角、本章是 deployment 視角</li>
<li><a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理</a>：什麼可以離開機器、本章是「什麼可以進 git」的 sibling</li>
<li><a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 RAG storage 工程</a>：本章把 embedding index 判為 derived（不進 git、<code>ingest.py</code> 重建）、該章接手 vector index 存進 backend 之後的生命週期管理</li>
</ul>
]]></content:encoded></item><item><title>5.C10 對照：規模差異下的平台遷移</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/</guid><description>&lt;p>這篇對照的核心責任是避免把同一套切流流程套到所有組織規模。遷移策略的切換單位、回退腳本化程度、依賴同步範圍與協同治理工具，在小中大型組織各有不同取捨。&lt;/p>
&lt;h2 id="小型組織常見判讀">小型組織常見判讀&lt;/h2>
&lt;p>小型組織通常能快速完成單叢集遷移，但最容易漏掉回退腳本化。結果是第一次回退就需要人工拼接操作，恢復時間不可預測。&lt;/p>
&lt;p>回退腳本化缺失的具體表現：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>手動 kubectl 操作&lt;/strong>：回退時 on-call 逐一執行 &lt;code>kubectl rollout undo&lt;/code>、手動修改 DNS 權重、手動切回 LB 規則。每一步都依賴執行者的記憶與判斷，步驟順序錯誤或遺漏都會延長恢復時間。&lt;/li>
&lt;li>&lt;strong>無 rollback script&lt;/strong>：回退流程沒有腳本化，也沒有在 staging 驗證過。第一次真正回退就是在 production 事故中。&lt;/li>
&lt;li>&lt;strong>恢復時間不可預測&lt;/strong>：手動操作的恢復時間取決於 on-call 的經驗與當下判斷力。同一個回退在不同人手上可能差 3-10 倍時間。&lt;/li>
&lt;/ul>
&lt;p>小型組織的回退投資最小可行版本是一個 shell script：按正確順序執行回退步驟、每步帶 dry-run 模式、在 staging 驗證過。這個投資的 ROI 在第一次真正回退時就回收。&lt;/p>
&lt;h2 id="中型組織常見判讀">中型組織常見判讀&lt;/h2>
&lt;p>中型組織的主要風險是依賴錯位。服務本身切過去了，但資料面、認證面、觀測面還沒同步，造成切換後局部成功、整體失敗。&lt;/p>
&lt;p>依賴錯位的常見維度：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Database endpoint&lt;/strong>：應用在新叢集但仍連舊叢集的資料庫。跨網路延遲從 &amp;lt;1ms 跳到 5-20ms，慢查詢變多、connection pool 壓力增加。嚴重時跨 AZ / region 的網路分區直接斷開連線。&lt;/li>
&lt;li>&lt;strong>Auth service&lt;/strong>：新叢集的服務用舊叢集的 auth endpoint，token 驗證走跨網路。auth 延遲增加讓每個 request 的總延遲上升，高峰時 auth 成為瓶頸。&lt;/li>
&lt;li>&lt;strong>Observability pipeline&lt;/strong>：新叢集的 metrics / logs / traces 仍送到舊叢集的收集器，或送到新收集器但 dashboard 還指向舊資料源。事故時看不到新叢集的指標，判讀盲區。&lt;/li>
&lt;li>&lt;strong>DNS 解析路徑&lt;/strong>：新叢集的 CoreDNS 設定跟舊叢集不同（upstream resolver、search domain、ndots），服務的 DNS 解析行為改變但沒被偵測到。表現為間歇性連線失敗或解析延遲。&lt;/li>
&lt;/ul>
&lt;p>中型組織的遷移 checklist 要把這四個維度列為切換前驗證項目。每個維度各自有切換時機——資料庫通常最後切（風險最高），auth 跟 observability 要先切或同步切。切換順序規劃見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移&lt;/a>。&lt;/p>
&lt;h2 id="大型組織常見判讀">大型組織常見判讀&lt;/h2>
&lt;p>大型組織的遷移失敗主要來自協同節奏失控。若沒有固定升級節奏與責任分工，單次變更容易演變成廣域事故。&lt;/p>
&lt;p>協同節奏的具體治理工具：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Upgrade calendar&lt;/strong>：所有平台級變更（叢集升級、service mesh 升級、CNI 更新）排進共用日曆。避免兩個團隊同週做影響面重疊的變更。日曆的維護者是 platform team，變更申請需提供 blast radius 估算。&lt;/li>
&lt;li>&lt;strong>Freeze window&lt;/strong>：業務高峰期（促銷、財報季、年終）凍結非緊急平台變更。freeze window 的開始 / 結束時間要明確公告，例外申請需 VP 級批准。&lt;/li>
&lt;li>&lt;strong>Blast radius estimation&lt;/strong>：每次變更前估算影響範圍——影響幾個 namespace、幾個 service、幾個使用者。估算結果進 release gate 的判定條件。工具層面可用 admission webhook 掃描變更影響的 namespace 數量。&lt;/li>
&lt;li>&lt;strong>Responsibility matrix&lt;/strong>：遷移期間的 RACI 明確化——誰負責切換、誰負責監控、誰負責回退決策、誰負責對外溝通。大型組織的遷移通常跨 3+ 團隊，責任模糊是事故升級的主要原因。&lt;/li>
&lt;/ul>
&lt;p>大型組織的平台元件升級治理見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程&lt;/a>。&lt;/p>
&lt;h2 id="跨規模的共通判讀">跨規模的共通判讀&lt;/h2>
&lt;p>三個規模的失敗模式不同（小型漏回退腳本、中型漏依賴同步、大型漏協同節奏），但共通原則是「先定回退條件再開始切換」。回退條件包含三個面向：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>觸發條件&lt;/strong>：哪些指標偏離到什麼程度就停止切換（5xx 升幅、延遲惡化、reconnect rate）。&lt;/li>
&lt;li>&lt;strong>執行路徑&lt;/strong>：回退的具體步驟、順序、負責人，且在 staging 驗證過。&lt;/li>
&lt;li>&lt;strong>完成判定&lt;/strong>：回退完成的訊號是什麼（連線數回 baseline、error rate 回 baseline、持續 N 分鐘）。&lt;/li>
&lt;/ol>
&lt;p>三個面向任一缺失，回退就會變成臨時決策——壓力下的臨時決策品質不穩定，是切流事故擴大的共通機制。&lt;/p>
&lt;h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>切流批次 &lt;code>5xx&lt;/code> 異常升高&lt;/li>
&lt;li>長連線重連率飆升&lt;/li>
&lt;li>回退時間超過既定 RTO&lt;/li>
&lt;li>跨叢集依賴延遲突增（中型組織特有）&lt;/li>
&lt;/ul>
&lt;p>任一條件成立就停止下一批切換，先完成上一批穩定化與回退驗證。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移&lt;/a> 看切換順序規劃。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 看遷移後的 lifecycle 重新驗證。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例&lt;/a> 看切流未 drain 的具體事故 timeline。&lt;/p></description><content:encoded><![CDATA[<p>這篇對照的核心責任是避免把同一套切流流程套到所有組織規模。遷移策略的切換單位、回退腳本化程度、依賴同步範圍與協同治理工具，在小中大型組織各有不同取捨。</p>
<h2 id="小型組織常見判讀">小型組織常見判讀</h2>
<p>小型組織通常能快速完成單叢集遷移，但最容易漏掉回退腳本化。結果是第一次回退就需要人工拼接操作，恢復時間不可預測。</p>
<p>回退腳本化缺失的具體表現：</p>
<ul>
<li><strong>手動 kubectl 操作</strong>：回退時 on-call 逐一執行 <code>kubectl rollout undo</code>、手動修改 DNS 權重、手動切回 LB 規則。每一步都依賴執行者的記憶與判斷，步驟順序錯誤或遺漏都會延長恢復時間。</li>
<li><strong>無 rollback script</strong>：回退流程沒有腳本化，也沒有在 staging 驗證過。第一次真正回退就是在 production 事故中。</li>
<li><strong>恢復時間不可預測</strong>：手動操作的恢復時間取決於 on-call 的經驗與當下判斷力。同一個回退在不同人手上可能差 3-10 倍時間。</li>
</ul>
<p>小型組織的回退投資最小可行版本是一個 shell script：按正確順序執行回退步驟、每步帶 dry-run 模式、在 staging 驗證過。這個投資的 ROI 在第一次真正回退時就回收。</p>
<h2 id="中型組織常見判讀">中型組織常見判讀</h2>
<p>中型組織的主要風險是依賴錯位。服務本身切過去了，但資料面、認證面、觀測面還沒同步，造成切換後局部成功、整體失敗。</p>
<p>依賴錯位的常見維度：</p>
<ul>
<li><strong>Database endpoint</strong>：應用在新叢集但仍連舊叢集的資料庫。跨網路延遲從 &lt;1ms 跳到 5-20ms，慢查詢變多、connection pool 壓力增加。嚴重時跨 AZ / region 的網路分區直接斷開連線。</li>
<li><strong>Auth service</strong>：新叢集的服務用舊叢集的 auth endpoint，token 驗證走跨網路。auth 延遲增加讓每個 request 的總延遲上升，高峰時 auth 成為瓶頸。</li>
<li><strong>Observability pipeline</strong>：新叢集的 metrics / logs / traces 仍送到舊叢集的收集器，或送到新收集器但 dashboard 還指向舊資料源。事故時看不到新叢集的指標，判讀盲區。</li>
<li><strong>DNS 解析路徑</strong>：新叢集的 CoreDNS 設定跟舊叢集不同（upstream resolver、search domain、ndots），服務的 DNS 解析行為改變但沒被偵測到。表現為間歇性連線失敗或解析延遲。</li>
</ul>
<p>中型組織的遷移 checklist 要把這四個維度列為切換前驗證項目。每個維度各自有切換時機——資料庫通常最後切（風險最高），auth 跟 observability 要先切或同步切。切換順序規劃見 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移</a>。</p>
<h2 id="大型組織常見判讀">大型組織常見判讀</h2>
<p>大型組織的遷移失敗主要來自協同節奏失控。若沒有固定升級節奏與責任分工，單次變更容易演變成廣域事故。</p>
<p>協同節奏的具體治理工具：</p>
<ul>
<li><strong>Upgrade calendar</strong>：所有平台級變更（叢集升級、service mesh 升級、CNI 更新）排進共用日曆。避免兩個團隊同週做影響面重疊的變更。日曆的維護者是 platform team，變更申請需提供 blast radius 估算。</li>
<li><strong>Freeze window</strong>：業務高峰期（促銷、財報季、年終）凍結非緊急平台變更。freeze window 的開始 / 結束時間要明確公告，例外申請需 VP 級批准。</li>
<li><strong>Blast radius estimation</strong>：每次變更前估算影響範圍——影響幾個 namespace、幾個 service、幾個使用者。估算結果進 release gate 的判定條件。工具層面可用 admission webhook 掃描變更影響的 namespace 數量。</li>
<li><strong>Responsibility matrix</strong>：遷移期間的 RACI 明確化——誰負責切換、誰負責監控、誰負責回退決策、誰負責對外溝通。大型組織的遷移通常跨 3+ 團隊，責任模糊是事故升級的主要原因。</li>
</ul>
<p>大型組織的平台元件升級治理見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程</a>。</p>
<h2 id="跨規模的共通判讀">跨規模的共通判讀</h2>
<p>三個規模的失敗模式不同（小型漏回退腳本、中型漏依賴同步、大型漏協同節奏），但共通原則是「先定回退條件再開始切換」。回退條件包含三個面向：</p>
<ol>
<li><strong>觸發條件</strong>：哪些指標偏離到什麼程度就停止切換（5xx 升幅、延遲惡化、reconnect rate）。</li>
<li><strong>執行路徑</strong>：回退的具體步驟、順序、負責人，且在 staging 驗證過。</li>
<li><strong>完成判定</strong>：回退完成的訊號是什麼（連線數回 baseline、error rate 回 baseline、持續 N 分鐘）。</li>
</ol>
<p>三個面向任一缺失，回退就會變成臨時決策——壓力下的臨時決策品質不穩定，是切流事故擴大的共通機制。</p>
<h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件</h2>
<ul>
<li>切流批次 <code>5xx</code> 異常升高</li>
<li>長連線重連率飆升</li>
<li>回退時間超過既定 RTO</li>
<li>跨叢集依賴延遲突增（中型組織特有）</li>
</ul>
<p>任一條件成立就停止下一批切換，先完成上一批穩定化與回退驗證。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移</a> 看切換順序規劃。回 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 看遷移後的 lifecycle 重新驗證。回 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 看切流未 drain 的具體事故 timeline。</p>
]]></content:encoded></item><item><title>前端部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/frontend-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/frontend-deploy/</guid><description>&lt;p>前端部署 CI/CD 的核心責任是把瀏覽器可執行的靜態產物安全交付到 hosting、CDN 或 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">preview environment&lt;/a>。前端部署常見輸出是 HTML、CSS、JavaScript、圖片與搜尋索引；它的風險集中在 build &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact&lt;/a>、路由、cache、環境變數與使用者可見回歸。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>前端部署和後端部署的差異在於 runtime 責任位置。前端產物通常在 build time 完成大部分工作，發布後由 browser、CDN 或 static hosting 提供服務；後端服務則要在 runtime 處理連線、資料庫、migration、狀態與 rollback。&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>Build&lt;/td>
 &lt;td>bundle、static site、asset hashing&lt;/td>
 &lt;td>build 是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>browser regression、a11y、layout&lt;/td>
 &lt;td>Playwright / visual diff 是否通過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact&lt;/a>&lt;/td>
 &lt;td>static files、search index、sourcemap&lt;/td>
 &lt;td>測試與發布是否同一份產物&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>hosting、CDN、Pages、preview URL&lt;/td>
 &lt;td>cache invalidation 與路由是否正確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>&lt;/td>
 &lt;td>回退前一版 static artifact&lt;/td>
 &lt;td>是否保留可回復版本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生 browser 實際會執行的內容。真實服務常見訊號是 bundle size、asset hash、base URL、環境變數與 static route 是否穩定；若 build 只能在開發機成功，CI 就要把 Node 版本、package lock、build command 與環境變數收斂成固定入口。&lt;/p>
&lt;p>Test 階段負責驗證使用者可見行為。前端常見測試包含 component test、browser regression、accessibility check 與 layout check；測試應盡量靠近 production artifact，讓 dev server 的寬鬆行為不會蓋掉實際部署問題。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact&lt;/a> 階段負責保存可發布產物。靜態檔、搜尋索引與 sourcemap 都可能影響使用者體驗與除錯能力；測試與發布共用同一份 artifact，可以避免「測試通過的是 A，發布出去的是 B」的漂移。&lt;/p>
&lt;p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。真實風險通常集中在 HTML cache、asset cache、SPA fallback、preview URL 與 production domain 是否對齊。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a> 階段負責讓上一個可用 artifact 能重新服務使用者。前端 rollback 通常比後端快，但若 build time 環境變數、資料 schema 或 CDN cache 已變更，回退仍需要驗證頁面路由與 API 相容性。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>CDN cache 要和 asset hash、HTML cache policy 分開看。&lt;/li>
&lt;li>Preview environment 要能對應 PR，讓 reviewer 看到真實 build。&lt;/li>
&lt;li>前端測試要跑在 production-like artifact 上，避免 dev server 行為遮蔽問題。&lt;/li>
&lt;li>環境變數若在 build time 注入，重新發布才會生效。&lt;/li>
&lt;li>SPA route 需要 fallback 設定，靜態站 route 需要檔案路徑與 base URL 對齊。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&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;a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程&lt;/a>&lt;/td>
 &lt;td>Static artifact and preview&lt;/td>
 &lt;td>串起 build、browser test、preview 與 rollback&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>前端 artifact 流程：讀 &lt;a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程&lt;/a>。&lt;/li>
&lt;li>本 blog 的靜態站案例：讀 &lt;a href="../blog-project-deploy/">本 blog 專案部署&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>失敗處理：讀 &lt;a href="../github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>前端部署 CI/CD 的核心責任是把瀏覽器可執行的靜態產物安全交付到 hosting、CDN 或 <a href="/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">preview environment</a>。前端部署常見輸出是 HTML、CSS、JavaScript、圖片與搜尋索引；它的風險集中在 build <a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact</a>、路由、cache、環境變數與使用者可見回歸。</p>
<h2 id="場域定位">場域定位</h2>
<p>前端部署和後端部署的差異在於 runtime 責任位置。前端產物通常在 build time 完成大部分工作，發布後由 browser、CDN 或 static hosting 提供服務；後端服務則要在 runtime 處理連線、資料庫、migration、狀態與 rollback。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>前端部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>bundle、static site、asset hashing</td>
          <td>build 是否可重現</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>browser regression、a11y、layout</td>
          <td>Playwright / visual diff 是否通過</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a></td>
          <td>static files、search index、sourcemap</td>
          <td>測試與發布是否同一份產物</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>hosting、CDN、Pages、preview URL</td>
          <td>cache invalidation 與路由是否正確</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a></td>
          <td>回退前一版 static artifact</td>
          <td>是否保留可回復版本</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生 browser 實際會執行的內容。真實服務常見訊號是 bundle size、asset hash、base URL、環境變數與 static route 是否穩定；若 build 只能在開發機成功，CI 就要把 Node 版本、package lock、build command 與環境變數收斂成固定入口。</p>
<p>Test 階段負責驗證使用者可見行為。前端常見測試包含 component test、browser regression、accessibility check 與 layout check；測試應盡量靠近 production artifact，讓 dev server 的寬鬆行為不會蓋掉實際部署問題。</p>
<p><a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a> 階段負責保存可發布產物。靜態檔、搜尋索引與 sourcemap 都可能影響使用者體驗與除錯能力；測試與發布共用同一份 artifact，可以避免「測試通過的是 A，發布出去的是 B」的漂移。</p>
<p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。真實風險通常集中在 HTML cache、asset cache、SPA fallback、preview URL 與 production domain 是否對齊。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a> 階段負責讓上一個可用 artifact 能重新服務使用者。前端 rollback 通常比後端快，但若 build time 環境變數、資料 schema 或 CDN cache 已變更，回退仍需要驗證頁面路由與 API 相容性。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>CDN cache 要和 asset hash、HTML cache policy 分開看。</li>
<li>Preview environment 要能對應 PR，讓 reviewer 看到真實 build。</li>
<li>前端測試要跑在 production-like artifact 上，避免 dev server 行為遮蔽問題。</li>
<li>環境變數若在 build time 注入，重新發布才會生效。</li>
<li>SPA route 需要 fallback 設定，靜態站 route 需要檔案路徑與 base URL 對齊。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程</a></td>
          <td>Static artifact and preview</td>
          <td>串起 build、browser test、preview 與 rollback</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>前端 artifact 流程：讀 <a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程</a>。</li>
<li>本 blog 的靜態站案例：讀 <a href="../blog-project-deploy/">本 blog 專案部署</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>失敗處理：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
</ul>
]]></content:encoded></item><item><title>程式碼版控與 FTP 部署紀律</title><link>https://tarrragon.github.io/blog/infra/takeover/legacy-code-versioning-deployment/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/takeover/legacy-code-versioning-deployment/</guid><description>&lt;p>無 SSH 環境的 PHP 專案通常沒有版本歷史——程式碼直接透過 FTP 覆蓋伺服器上的檔案，每次上傳就是一次不可回溯的覆寫。接手這類專案時，第一步是在本地建立 Git repo 作為程式碼的唯一事實來源，第二步是把 FTP 上傳從「隨手改隨手傳」轉成有紀錄、可回退的部署流程。本篇聚焦在程式碼端的版控與部署；資料庫的備份與變更紀律見&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-database-backup-migration/" data-link-title="無 SSH 環境的資料庫備份與變更管理" data-link-desc="在只有 phpMyAdmin 或有限遠端連線的無 SSH 環境裡，怎麼建立可靠的資料庫備份策略、schema 變更紀律與還原演練流程">資料庫備份與變更管理&lt;/a>；帳號與存取的安全管理見&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-php-security-audit/" data-link-title="Legacy PHP 的安全盤點" data-link-desc="接手 legacy PHP 專案後的系統性安全審查：credential 掃描、PHP 版本風險、常見漏洞模式的 grep 偵測、.htaccess 防線、檔案權限、外部依賴與掃描工具">Legacy PHP 的安全盤點&lt;/a>。&lt;/p>
&lt;h2 id="從-ftp-拉下來建立-git-repo">從 FTP 拉下來建立 Git repo&lt;/h2>
&lt;p>用 FTP client 把整個站台完整下載到本地目錄，這份下載就是 production 的快照。下載完成後在該目錄初始化 Git：&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="nb">cd&lt;/span> /path/to/downloaded-site
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git init&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在第一次 commit 之前先處理 &lt;code>.gitignore&lt;/code>。PHP 專案需要排除的檔案分三類：套件依賴（由 Composer 或 npm 管理、可重建）、執行期產物（快取、session、上傳檔案）、以及含有機密值的設定檔。&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"># 套件依賴
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">vendor/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">node_modules/
&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">cache/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">tmp/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">sessions/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">*.log
&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">uploads/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">media/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">wp-content/uploads/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&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">.env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">config.local.php
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">wp-config.php&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用者上傳的內容（&lt;code>uploads/&lt;/code>、&lt;code>media/&lt;/code>）不進 Git 的理由是它屬於資料層：檔案數量可能成千上萬、總容量可能數 GB，Git 不適合管理這類大量二進位檔案。這些檔案的備份策略跟程式碼不同——用 FTP mirror 或 rclone 定期同步到本地即可。&lt;/p>
&lt;p>設好 &lt;code>.gitignore&lt;/code> 後做第一次 commit：&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">git add -A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;production snapshot &lt;/span>&lt;span class="k">$(&lt;/span>date +%Y-%m-%d&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 commit 就是「接手時 production 長什麼樣」的基準線。後續所有改動都從這裡開始有版本歷史。&lt;/p>
&lt;h2 id="config-分離讓-git-repo-不含機密值">Config 分離：讓 Git repo 不含機密值&lt;/h2>
&lt;p>無 SSH 環境的 PHP 專案常把資料庫密碼、API key、SMTP 憑證直接寫在 &lt;code>config.php&lt;/code> 或 &lt;code>wp-config.php&lt;/code> 裡。這些檔案如果進了 Git，機密值就跟著 repo 走——推到 GitHub 就等於公開。&lt;/p>
&lt;p>分離的模式是把設定拆成兩份：一份進 Git（結構與預設值）、一份不進 Git（實際機密值）。&lt;/p>
&lt;h3 id="模式一env-檔案">模式一：.env 檔案&lt;/h3>
&lt;p>使用 &lt;code>vlucas/phpdotenv&lt;/code> 套件或手動解析，讓程式碼從 &lt;code>.env&lt;/code> 檔案讀取環境變數：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// config.php — 進 Git
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nv">$dotenv&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Dotenv\Dotenv&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="na">createImmutable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="no">__DIR__&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nv">$dotenv&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">load&lt;/span>&lt;span class="p">();&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;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nv">$db_host&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nv">$_ENV&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;DB_HOST&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nv">$db_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nv">$_ENV&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;DB_NAME&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="nv">$db_user&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nv">$_ENV&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;DB_USER&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="nv">$db_pass&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nv">$_ENV&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;DB_PASS&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl"># .env — 不進 Git（.gitignore 已排除）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">DB_HOST=localhost
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">DB_NAME=mysite_prod
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">DB_USER=mysite_user
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">DB_PASS=actual-password-here&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同時在 repo 裡放一份 &lt;code>.env.example&lt;/code>（進 Git），列出所有需要的環境變數但不填實際值：&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"># .env.example — 進 Git，作為範本
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">DB_HOST=
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">DB_NAME=
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">DB_USER=
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">DB_PASS=
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">SMTP_HOST=
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">SMTP_USER=
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">SMTP_PASS=&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="模式二configlocalphp">模式二：config.local.php&lt;/h3>
&lt;p>如果專案不使用 Composer、引入 phpdotenv 成本太高，用 PHP include 分離：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// config.php — 進 Git
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">file_exists&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="no">__DIR__&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="s1">&amp;#39;/config.local.php&amp;#39;&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">require&lt;/span> &lt;span class="no">__DIR__&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="s1">&amp;#39;/config.local.php&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">die&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;config.local.php not found. Copy config.local.example.php and fill in values.&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// config.local.php — 不進 Git
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nv">$db_host&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;localhost&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nv">$db_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;mysite_prod&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nv">$db_user&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;mysite_user&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nv">$db_pass&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;actual-password-here&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="wordpress-的處理">WordPress 的處理&lt;/h3>
&lt;p>WordPress 的 &lt;code>wp-config.php&lt;/code> 同時包含機密值和非機密設定。把整份排除再 include 一份 local 版是最簡單的做法，但也可以只把機密值抽到 &lt;code>.env&lt;/code>、&lt;code>wp-config.php&lt;/code> 本身保留在 Git 裡：&lt;/p></description><content:encoded><![CDATA[<p>無 SSH 環境的 PHP 專案通常沒有版本歷史——程式碼直接透過 FTP 覆蓋伺服器上的檔案，每次上傳就是一次不可回溯的覆寫。接手這類專案時，第一步是在本地建立 Git repo 作為程式碼的唯一事實來源，第二步是把 FTP 上傳從「隨手改隨手傳」轉成有紀錄、可回退的部署流程。本篇聚焦在程式碼端的版控與部署；資料庫的備份與變更紀律見<a href="/blog/infra/takeover/legacy-database-backup-migration/" data-link-title="無 SSH 環境的資料庫備份與變更管理" data-link-desc="在只有 phpMyAdmin 或有限遠端連線的無 SSH 環境裡，怎麼建立可靠的資料庫備份策略、schema 變更紀律與還原演練流程">資料庫備份與變更管理</a>；帳號與存取的安全管理見<a href="/blog/infra/takeover/legacy-php-security-audit/" data-link-title="Legacy PHP 的安全盤點" data-link-desc="接手 legacy PHP 專案後的系統性安全審查：credential 掃描、PHP 版本風險、常見漏洞模式的 grep 偵測、.htaccess 防線、檔案權限、外部依賴與掃描工具">Legacy PHP 的安全盤點</a>。</p>
<h2 id="從-ftp-拉下來建立-git-repo">從 FTP 拉下來建立 Git repo</h2>
<p>用 FTP client 把整個站台完整下載到本地目錄，這份下載就是 production 的快照。下載完成後在該目錄初始化 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"><span class="nb">cd</span> /path/to/downloaded-site
</span></span><span class="line"><span class="ln">2</span><span class="cl">git init</span></span></code></pre></div><p>在第一次 commit 之前先處理 <code>.gitignore</code>。PHP 專案需要排除的檔案分三類：套件依賴（由 Composer 或 npm 管理、可重建）、執行期產物（快取、session、上傳檔案）、以及含有機密值的設定檔。</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"># 套件依賴
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">vendor/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">node_modules/
</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">cache/
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">tmp/
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">sessions/
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">*.log
</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">uploads/
</span></span><span class="line"><span class="ln">13</span><span class="cl">media/
</span></span><span class="line"><span class="ln">14</span><span class="cl">wp-content/uploads/
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"># 機密設定（下一節處理）
</span></span><span class="line"><span class="ln">17</span><span class="cl">.env
</span></span><span class="line"><span class="ln">18</span><span class="cl">config.local.php
</span></span><span class="line"><span class="ln">19</span><span class="cl">wp-config.php</span></span></code></pre></div><p>使用者上傳的內容（<code>uploads/</code>、<code>media/</code>）不進 Git 的理由是它屬於資料層：檔案數量可能成千上萬、總容量可能數 GB，Git 不適合管理這類大量二進位檔案。這些檔案的備份策略跟程式碼不同——用 FTP mirror 或 rclone 定期同步到本地即可。</p>
<p>設好 <code>.gitignore</code> 後做第一次 commit：</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 add -A
</span></span><span class="line"><span class="ln">2</span><span class="cl">git commit -m <span class="s2">&#34;production snapshot </span><span class="k">$(</span>date +%Y-%m-%d<span class="k">)</span><span class="s2">&#34;</span></span></span></code></pre></div><p>這個 commit 就是「接手時 production 長什麼樣」的基準線。後續所有改動都從這裡開始有版本歷史。</p>
<h2 id="config-分離讓-git-repo-不含機密值">Config 分離：讓 Git repo 不含機密值</h2>
<p>無 SSH 環境的 PHP 專案常把資料庫密碼、API key、SMTP 憑證直接寫在 <code>config.php</code> 或 <code>wp-config.php</code> 裡。這些檔案如果進了 Git，機密值就跟著 repo 走——推到 GitHub 就等於公開。</p>
<p>分離的模式是把設定拆成兩份：一份進 Git（結構與預設值）、一份不進 Git（實際機密值）。</p>
<h3 id="模式一env-檔案">模式一：.env 檔案</h3>
<p>使用 <code>vlucas/phpdotenv</code> 套件或手動解析，讓程式碼從 <code>.env</code> 檔案讀取環境變數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// config.php — 進 Git
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nv">$dotenv</span> <span class="o">=</span> <span class="nx">Dotenv\Dotenv</span><span class="o">::</span><span class="na">createImmutable</span><span class="p">(</span><span class="no">__DIR__</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">$dotenv</span><span class="o">-&gt;</span><span class="na">load</span><span class="p">();</span>
</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="nv">$db_host</span> <span class="o">=</span> <span class="nv">$_ENV</span><span class="p">[</span><span class="s1">&#39;DB_HOST&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nv">$db_name</span> <span class="o">=</span> <span class="nv">$_ENV</span><span class="p">[</span><span class="s1">&#39;DB_NAME&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nv">$db_user</span> <span class="o">=</span> <span class="nv">$_ENV</span><span class="p">[</span><span class="s1">&#39;DB_USER&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nv">$db_pass</span> <span class="o">=</span> <span class="nv">$_ENV</span><span class="p">[</span><span class="s1">&#39;DB_PASS&#39;</span><span class="p">];</span></span></span></code></pre></div>




<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"># .env — 不進 Git（.gitignore 已排除）
</span></span><span class="line"><span class="ln">2</span><span class="cl">DB_HOST=localhost
</span></span><span class="line"><span class="ln">3</span><span class="cl">DB_NAME=mysite_prod
</span></span><span class="line"><span class="ln">4</span><span class="cl">DB_USER=mysite_user
</span></span><span class="line"><span class="ln">5</span><span class="cl">DB_PASS=actual-password-here</span></span></code></pre></div><p>同時在 repo 裡放一份 <code>.env.example</code>（進 Git），列出所有需要的環境變數但不填實際值：</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"># .env.example — 進 Git，作為範本
</span></span><span class="line"><span class="ln">2</span><span class="cl">DB_HOST=
</span></span><span class="line"><span class="ln">3</span><span class="cl">DB_NAME=
</span></span><span class="line"><span class="ln">4</span><span class="cl">DB_USER=
</span></span><span class="line"><span class="ln">5</span><span class="cl">DB_PASS=
</span></span><span class="line"><span class="ln">6</span><span class="cl">SMTP_HOST=
</span></span><span class="line"><span class="ln">7</span><span class="cl">SMTP_USER=
</span></span><span class="line"><span class="ln">8</span><span class="cl">SMTP_PASS=</span></span></code></pre></div><h3 id="模式二configlocalphp">模式二：config.local.php</h3>
<p>如果專案不使用 Composer、引入 phpdotenv 成本太高，用 PHP include 分離：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// config.php — 進 Git
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="nx">file_exists</span><span class="p">(</span><span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/config.local.php&#39;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">require</span> <span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/config.local.php&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">die</span><span class="p">(</span><span class="s1">&#39;config.local.php not found. Copy config.local.example.php and fill in values.&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// config.local.php — 不進 Git
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nv">$db_host</span> <span class="o">=</span> <span class="s1">&#39;localhost&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">$db_name</span> <span class="o">=</span> <span class="s1">&#39;mysite_prod&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nv">$db_user</span> <span class="o">=</span> <span class="s1">&#39;mysite_user&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nv">$db_pass</span> <span class="o">=</span> <span class="s1">&#39;actual-password-here&#39;</span><span class="p">;</span></span></span></code></pre></div><h3 id="wordpress-的處理">WordPress 的處理</h3>
<p>WordPress 的 <code>wp-config.php</code> 同時包含機密值和非機密設定。把整份排除再 include 一份 local 版是最簡單的做法，但也可以只把機密值抽到 <code>.env</code>、<code>wp-config.php</code> 本身保留在 Git 裡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// wp-config.php — 進 Git（機密值從 .env 讀）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nv">$dotenv</span> <span class="o">=</span> <span class="nx">Dotenv\Dotenv</span><span class="o">::</span><span class="na">createImmutable</span><span class="p">(</span><span class="no">__DIR__</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">$dotenv</span><span class="o">-&gt;</span><span class="na">load</span><span class="p">();</span>
</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="nx">define</span><span class="p">(</span><span class="s1">&#39;DB_NAME&#39;</span><span class="p">,</span> <span class="nv">$_ENV</span><span class="p">[</span><span class="s1">&#39;DB_NAME&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">define</span><span class="p">(</span><span class="s1">&#39;DB_USER&#39;</span><span class="p">,</span> <span class="nv">$_ENV</span><span class="p">[</span><span class="s1">&#39;DB_USER&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nx">define</span><span class="p">(</span><span class="s1">&#39;DB_PASSWORD&#39;</span><span class="p">,</span> <span class="nv">$_ENV</span><span class="p">[</span><span class="s1">&#39;DB_PASSWORD&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nx">define</span><span class="p">(</span><span class="s1">&#39;DB_HOST&#39;</span><span class="p">,</span> <span class="nv">$_ENV</span><span class="p">[</span><span class="s1">&#39;DB_HOST&#39;</span><span class="p">]</span> <span class="o">??</span> <span class="s1">&#39;localhost&#39;</span><span class="p">);</span></span></span></code></pre></div><p>分離完成後，用 <code>grep</code> 確認 repo 裡沒有殘留的明文密碼：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git grep -in <span class="s2">&#34;password\|passwd\|secret\|api_key\|smtp&#34;</span> -- <span class="s1">&#39;*.php&#39;</span> <span class="s1">&#39;:!*.example*&#39;</span></span></span></code></pre></div><p>任何命中都要評估：是真的機密值（要移到 .env）還是變數名稱（可以保留）。</p>
<h2 id="ftp-部署的風險控制">FTP 部署的風險控制</h2>
<p>FTP 上傳是逐檔覆寫，沒有交易性——上傳到一半斷線、或上傳了有語法錯誤的 PHP 檔案，站台會立刻出問題。風險控制的核心是「每次上傳前知道在改什麼、上傳後知道改了什麼」。</p>
<h3 id="上傳前的比對">上傳前的比對</h3>
<p>FileZilla 的目錄比較功能（「檢視 → 目錄比較 → 啟用」）可以在上傳前看到本地與遠端的差異：哪些檔案是本地較新、哪些是遠端較新、哪些只存在於一邊。上傳前先跑比較、確認差異清單符合預期——如果出現預期外的「遠端較新」檔案，代表有人在伺服器上直接改了東西，要先下載回來合併再上傳。</p>
<h3 id="只上傳改過的檔案">只上傳改過的檔案</h3>
<p>一次上傳整個站台目錄既慢又危險。只上傳 Git diff 顯示的改動檔案：</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"># 列出相對於上次部署 tag 改了哪些檔案</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git diff --name-only deploy-2026-06-25 HEAD</span></span></code></pre></div><p>把這份清單對照 FileZilla 的比較結果，逐一上傳。量大時用 lftp 的 mirror 指令加 <code>--only-newer</code> flag 只傳新檔。</p>
<h3 id="關鍵檔案的額外保護">關鍵檔案的額外保護</h3>
<p><code>index.php</code>、<code>.htaccess</code>、設定檔這類檔案壞掉會讓整個站台無法存取。上傳這些檔案之前，先從伺服器下載一份當前版本存到本地的 <code>_backup/</code> 目錄（gitignored）。如果上傳後站台出問題，可以立刻把備份版本傳回去。</p>
<h2 id="部署前後的驗證">部署前後的驗證</h2>
<h3 id="部署前檢查">部署前檢查</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>確認方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本地測試通過</td>
          <td>在本地環境跑過改動的頁面 / 功能</td>
      </tr>
      <tr>
          <td>Git 已 commit</td>
          <td><code>git status</code> 顯示 clean</td>
      </tr>
      <tr>
          <td>要上傳的檔案清單已確認</td>
          <td><code>git diff --name-only</code> 輸出符合預期</td>
      </tr>
      <tr>
          <td>關鍵檔案已備份</td>
          <td><code>_backup/</code> 有當前版本</td>
      </tr>
  </tbody>
</table>
<h3 id="部署後驗證">部署後驗證</h3>
<p>上傳完成後立刻驗證：</p>
<ol>
<li>首頁能正常載入（HTTP 200、頁面內容正確）</li>
<li>本次改動涉及的功能可正常操作</li>
<li>如果是電商站：結帳流程、金流 callback 測試</li>
<li>檢查 PHP error log（cPanel → 錯誤日誌、或 FTP 下載 <code>error_log</code> 檔案）</li>
</ol>
<p>如果驗證失敗，回退方式是從 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"><span class="c1"># 取出上一個部署 tag 的特定檔案</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git show deploy-2026-06-25:path/to/file.php &gt; _rollback/file.php
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 用 FTP 上傳 _rollback/file.php 覆蓋 prod</span></span></span></code></pre></div><h2 id="ci-化-ftp-部署">CI 化 FTP 部署</h2>
<p>手動 FTP 部署的問題是它依賴特定人的 FTP client 和操作紀律。用 GitHub Actions 把 FTP 上傳自動化，可以讓部署變成「push 到 main → CI 跑測試 → CI 上傳到伺服器」的流程，不依賴任何人的本地環境。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy via FTP</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">          </span><span class="nt">fetch-depth</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy to FTP</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">SamKirkland/FTP-Deploy-Action@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span><span class="nt">server</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.FTP_HOST }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.FTP_USER }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.FTP_PASS }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">          </span><span class="nt">server-dir</span><span class="p">:</span><span class="w"> </span><span class="l">/public_html/</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">          </span><span class="nt">exclude</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="sd">            **/.git*
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="sd">            **/.git*/**
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="sd">            **/node_modules/**
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="sd">            **/.env
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="sd">            **/config.local.php</span></span></span></code></pre></div><p>FTP 憑證存在 GitHub repo 的 Secrets 裡（Settings → Secrets and variables → Actions），不寫在 workflow 檔案裡。</p>
<h3 id="ci-化後的改變">CI 化後的改變</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>手動 FTP</th>
          <th>CI 化 FTP</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署紀錄</td>
          <td>FTP client 的 log（通常不保留）</td>
          <td>GitHub Actions 的 run history（永久保留）</td>
      </tr>
      <tr>
          <td>部署觸發</td>
          <td>某人手動操作</td>
          <td>push 到 main 自動觸發</td>
      </tr>
      <tr>
          <td>上傳前測試</td>
          <td>依賴個人紀律</td>
          <td>CI 可加 lint / test step</td>
      </tr>
      <tr>
          <td>多人協作</td>
          <td>需要共用 FTP 帳密</td>
          <td>帳密在 GitHub Secrets、workflow 共用</td>
      </tr>
  </tbody>
</table>
<h3 id="限制">限制</h3>
<p>FTP 部署沒有原子性（atomic deployment）——檔案逐一上傳的過程中，伺服器上同時存在新舊版本的檔案混合狀態。如果上傳的檔案之間有依賴關係（新的 A.php 引用新的 B.php，但 B.php 還沒上傳完），短暫的錯誤窗口無法避免。流量高的站台如果需要零停機部署，需要升級到 SSH + symlink 切換的部署方式，那屬於 VPS 遷移之後的能力。</p>
<h2 id="git-tagging-部署紀錄">Git tagging 部署紀錄</h2>
<p>每次部署前在 Git 打一個 tag，讓「這次部署的是哪個版本」有明確的錨點：</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 tag deploy-<span class="k">$(</span>date +%Y-%m-%d-%H%M<span class="k">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git push origin --tags</span></span></code></pre></div><p>tag 的命名用日期時間戳而非版號，因為這類專案通常沒有語意化版號的概念。tag 的作用是：</p>
<ul>
<li>回退時知道要退到哪個版本（<code>git diff deploy-previous deploy-current</code> 看這次改了什麼）</li>
<li>多次部署之間的差異可追蹤</li>
<li>CI 化後可以用 tag 觸發部署而非每次 push 都部署</li>
</ul>
<p>資料庫變更的回退跟程式碼獨立處理——程式碼可以靠 Git 回退，資料庫要靠 SQL dump 回退，兩者的回退點要對齊但機制不同。資料庫的備份策略見<a href="/blog/infra/takeover/legacy-database-backup-migration/" data-link-title="無 SSH 環境的資料庫備份與變更管理" data-link-desc="在只有 phpMyAdmin 或有限遠端連線的無 SSH 環境裡，怎麼建立可靠的資料庫備份策略、schema 變更紀律與還原演練流程">資料庫備份與變更管理</a>。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">無 SSH 的 FTP / 面板管理環境接管</a>：本篇的母文章，涵蓋接手的完整流程</li>
<li>→ <a href="/blog/infra/takeover/legacy-database-backup-migration/" data-link-title="無 SSH 環境的資料庫備份與變更管理" data-link-desc="在只有 phpMyAdmin 或有限遠端連線的無 SSH 環境裡，怎麼建立可靠的資料庫備份策略、schema 變更紀律與還原演練流程">資料庫備份與變更管理</a>：資料庫端的備份、migration 紀律與回退策略</li>
<li>→ <a href="/blog/infra/takeover/legacy-php-security-audit/" data-link-title="Legacy PHP 的安全盤點" data-link-desc="接手 legacy PHP 專案後的系統性安全審查：credential 掃描、PHP 版本風險、常見漏洞模式的 grep 偵測、.htaccess 防線、檔案權限、外部依賴與掃描工具">Legacy PHP 的安全盤點</a>：credential 分離之後的存取控制與安全掃描</li>
<li>→ <a href="/blog/infra/takeover/legacy-external-monitoring/" data-link-title="無 SSH 環境的監控與告警" data-link-desc="無 SSH 環境沒辦法裝 agent、沒辦法串 log pipeline，用外部 HTTP check、錯誤追蹤服務與效能基線建立最低成本的監控能力">無 SSH 環境的監控與告警</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>：從 FTP CI 化進一步演進到完整的 PR review 流程</li>
</ul>
]]></content:encoded></item><item><title>後端部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/backend-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/backend-deploy/</guid><description>&lt;p>後端部署 CI/CD 的核心責任是把可執行服務安全推進到 runtime 環境。後端部署不只發布程式碼，還要處理資料庫 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>（backend 深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration&lt;/a>）、外部依賴、runtime config、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/readiness-health-check/" data-link-title="Readiness / Health Check" data-link-desc="說明服務存活與可接流量判斷在部署中的不同責任">Readiness / Health Check&lt;/a>（backend 深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">Readiness&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">Health Check&lt;/a>）、流量切換與 rollback。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>後端部署的主要風險來自有狀態依賴與長時間執行。API、worker、scheduler 與 consumer 會連到資料庫、queue、cache 與第三方服務；部署流程需要確認程式、資料與流量切換順序。&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>Build&lt;/td>
 &lt;td>binary、package、container image&lt;/td>
 &lt;td>build 是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>unit、integration、contract、migration&lt;/td>
 &lt;td>是否覆蓋跨服務契約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>&lt;/td>
 &lt;td>schema change、backfill、rollback path&lt;/td>
 &lt;td>是否可漸進、可停止、可驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy&lt;/a>&lt;/td>
 &lt;td>rolling、canary、blue-green&lt;/td>
 &lt;td>health / readiness 是否可信&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>&lt;/td>
 &lt;td>app rollback、migration rollback / forward fix&lt;/td>
 &lt;td>回復路徑是否演練&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生可部署服務。後端 build 常見形式是 binary、package 或 container image；判讀重點是版本是否能追到 commit、依賴是否固定、產物是否能在乾淨環境重建。&lt;/p>
&lt;p>Test 階段負責驗證服務契約。單元測試只能覆蓋局部邏輯，integration、contract 與 migration 測試才會揭露資料庫、queue、cache 與外部服務之間的相容性風險。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a> 階段負責推進資料結構與資料狀態。真實服務要支援新舊程式短暫共存，因此 migration 應偏向可漸進、可重試、可觀測，必要時用 forward fix 取代直接回滾資料。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy&lt;/a> 階段負責把流量安全導向新版本。Rolling、canary 與 blue-green 都需要可靠的 health、readiness、metrics 與 log；若 readiness 只檢查 process alive，流量仍可能被送到尚未準備好的服務。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a> 階段負責在新版本失效時縮小影響範圍。後端 rollback 要同時考慮程式、資料、queue message、外部 side effect 與 config；只回退 image tag，通常不足以處理已寫入的資料變化。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>Migration 要和 app rollout 分開設計，避免新舊版本不相容。&lt;/li>
&lt;li>Health check 只代表 process alive，readiness 才能判斷能否接流量。&lt;/li>
&lt;li>Worker / consumer 部署要考慮重複處理、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>。&lt;/li>
&lt;li>Config rollout 需要版本化與回退路徑（深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout&lt;/a>）。&lt;/li>
&lt;li>Rollback 不只回程式，也要處理資料與外部副作用（深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">Rollback Strategy&lt;/a>）。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&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;a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程&lt;/a>&lt;/td>
 &lt;td>Migration rollout and rollback&lt;/td>
 &lt;td>拆分資料變更、流量推進與回復路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>後端發布主流程：讀 &lt;a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>Backend reliability：讀 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程&lt;/a>。&lt;/li>
&lt;li>Release gate：讀 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>後端部署 CI/CD 的核心責任是把可執行服務安全推進到 runtime 環境。後端部署不只發布程式碼，還要處理資料庫 <a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a>（backend 深入見 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration</a>）、外部依賴、runtime config、<a href="/blog/ci/knowledge-cards/readiness-health-check/" data-link-title="Readiness / Health Check" data-link-desc="說明服務存活與可接流量判斷在部署中的不同責任">Readiness / Health Check</a>（backend 深入見 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">Readiness</a> / <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">Health Check</a>）、流量切換與 rollback。</p>
<h2 id="場域定位">場域定位</h2>
<p>後端部署的主要風險來自有狀態依賴與長時間執行。API、worker、scheduler 與 consumer 會連到資料庫、queue、cache 與第三方服務；部署流程需要確認程式、資料與流量切換順序。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>後端部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>binary、package、container image</td>
          <td>build 是否可重現</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>unit、integration、contract、migration</td>
          <td>是否覆蓋跨服務契約</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a></td>
          <td>schema change、backfill、rollback path</td>
          <td>是否可漸進、可停止、可驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy</a></td>
          <td>rolling、canary、blue-green</td>
          <td>health / readiness 是否可信</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a></td>
          <td>app rollback、migration rollback / forward fix</td>
          <td>回復路徑是否演練</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生可部署服務。後端 build 常見形式是 binary、package 或 container image；判讀重點是版本是否能追到 commit、依賴是否固定、產物是否能在乾淨環境重建。</p>
<p>Test 階段負責驗證服務契約。單元測試只能覆蓋局部邏輯，integration、contract 與 migration 測試才會揭露資料庫、queue、cache 與外部服務之間的相容性風險。</p>
<p><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a> 階段負責推進資料結構與資料狀態。真實服務要支援新舊程式短暫共存，因此 migration 應偏向可漸進、可重試、可觀測，必要時用 forward fix 取代直接回滾資料。</p>
<p><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy</a> 階段負責把流量安全導向新版本。Rolling、canary 與 blue-green 都需要可靠的 health、readiness、metrics 與 log；若 readiness 只檢查 process alive，流量仍可能被送到尚未準備好的服務。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a> 階段負責在新版本失效時縮小影響範圍。後端 rollback 要同時考慮程式、資料、queue message、外部 side effect 與 config；只回退 image tag，通常不足以處理已寫入的資料變化。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>Migration 要和 app rollout 分開設計，避免新舊版本不相容。</li>
<li>Health check 只代表 process alive，readiness 才能判斷能否接流量。</li>
<li>Worker / consumer 部署要考慮重複處理、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 與 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>。</li>
<li>Config rollout 需要版本化與回退路徑（深入見 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a>）。</li>
<li>Rollback 不只回程式，也要處理資料與外部副作用（深入見 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">Rollback Strategy</a>）。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程</a></td>
          <td>Migration rollout and rollback</td>
          <td>拆分資料變更、流量推進與回復路徑</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>後端發布主流程：讀 <a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>Backend reliability：讀 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程</a>。</li>
<li>Release gate：讀 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a>。</li>
</ul>
]]></content:encoded></item><item><title>App 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/app-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/app-deploy/</guid><description>&lt;p>App 部署 CI/CD 的核心責任是把可安裝的 client &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact&lt;/a> 安全送到發行通道。App 發布和 web 部署最大的差異是使用者裝置會保留舊版，app store 審核、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing&lt;/a>、版本號與分批發布會直接影響交付節奏。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>App 部署的風險集中在 artifact 不可變、簽章憑證、store review 與版本分佈。後端可以快速 rollback，前端靜態站可以重新部署，但已安裝的 App 需要靠更新、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag&lt;/a> 或服務端相容性管理。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>App 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>IPA、APK、AAB、desktop package&lt;/td>
 &lt;td>build number / version 是否遞增&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Signing&lt;/td>
 &lt;td>certificate、profile、keystore&lt;/td>
 &lt;td>secret 是否安全、是否可輪替&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>unit、UI、device matrix&lt;/td>
 &lt;td>是否覆蓋目標 OS 與裝置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Release&lt;/td>
 &lt;td>store review、phased rollout&lt;/td>
 &lt;td>審核狀態與 rollout 百分比&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>&lt;/td>
 &lt;td>hotfix、remote config、kill switch&lt;/td>
 &lt;td>是否能處理已安裝舊版&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生可安裝 artifact。Mobile 常見產物是 IPA、APK 或 AAB，desktop 則可能是 installer 或 signed package；版本號、build number 與 commit 對應關係會決定後續除錯與回報能否追溯。&lt;/p>
&lt;p>Signing 階段負責證明 artifact 由可信來源發布。憑證、profile、keystore 與 signing secret 都屬於發布能力；它們需要輪替、權限控管與備援流程，避免單一憑證問題中斷發布（安全治理延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>）。&lt;/p>
&lt;p>Test 階段負責驗證不同裝置與作業系統組合。App 測試常見風險是 emulator 通過但真機失敗、特定 OS 權限模型不同、背景執行限制不同；device matrix 要依使用者分佈與高風險功能選擇。&lt;/p>
&lt;p>Release 階段負責把 artifact 送進發行通道。Store review、phased rollout、internal testing、beta track 與 production track 都是 gate；發布節奏要把審核時間與分批比例納入 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">rollout strategy&lt;/a> 的風險控制（backend 延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout&lt;/a>）。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a> 階段負責處理已安裝版本。App 發布後會長期存在多個使用者版本，因此 hotfix、remote config、kill switch 與後端相容性要一起設計（相容治理延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract&lt;/a>）。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>簽章憑證是發布能力的一部分，要用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a> 管理。&lt;/li>
&lt;li>版本號與 build number 要可追溯到 commit 與 artifact。&lt;/li>
&lt;li>Store review 會讓 rollback 和 hotfix 變慢，風險要提前用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag&lt;/a> 控制。&lt;/li>
&lt;li>Client / server contract 要支援多版本共存。&lt;/li>
&lt;li>Crash reporting 與 phased rollout 是發布後 gate 的一部分。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&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;a href="signing-store-rollout-flow/">App 簽章、商店審核與分批發布流程&lt;/a>&lt;/td>
 &lt;td>Signing, review and rollout&lt;/td>
 &lt;td>管理簽章、審核、分批發布與多版本共存&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>App 發布主流程：讀 &lt;a href="signing-store-rollout-flow/">App 簽章、商店審核與分批發布流程&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>失敗處理：讀 &lt;a href="../github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>App 部署 CI/CD 的核心責任是把可安裝的 client <a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact</a> 安全送到發行通道。App 發布和 web 部署最大的差異是使用者裝置會保留舊版，app store 審核、<a href="/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing</a>、版本號與分批發布會直接影響交付節奏。</p>
<h2 id="場域定位">場域定位</h2>
<p>App 部署的風險集中在 artifact 不可變、簽章憑證、store review 與版本分佈。後端可以快速 rollback，前端靜態站可以重新部署，但已安裝的 App 需要靠更新、<a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag</a> 或服務端相容性管理。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>App 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>IPA、APK、AAB、desktop package</td>
          <td>build number / version 是否遞增</td>
      </tr>
      <tr>
          <td>Signing</td>
          <td>certificate、profile、keystore</td>
          <td>secret 是否安全、是否可輪替</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>unit、UI、device matrix</td>
          <td>是否覆蓋目標 OS 與裝置</td>
      </tr>
      <tr>
          <td>Release</td>
          <td>store review、phased rollout</td>
          <td>審核狀態與 rollout 百分比</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a></td>
          <td>hotfix、remote config、kill switch</td>
          <td>是否能處理已安裝舊版</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生可安裝 artifact。Mobile 常見產物是 IPA、APK 或 AAB，desktop 則可能是 installer 或 signed package；版本號、build number 與 commit 對應關係會決定後續除錯與回報能否追溯。</p>
<p>Signing 階段負責證明 artifact 由可信來源發布。憑證、profile、keystore 與 signing secret 都屬於發布能力；它們需要輪替、權限控管與備援流程，避免單一憑證問題中斷發布（安全治理延伸見 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>）。</p>
<p>Test 階段負責驗證不同裝置與作業系統組合。App 測試常見風險是 emulator 通過但真機失敗、特定 OS 權限模型不同、背景執行限制不同；device matrix 要依使用者分佈與高風險功能選擇。</p>
<p>Release 階段負責把 artifact 送進發行通道。Store review、phased rollout、internal testing、beta track 與 production track 都是 gate；發布節奏要把審核時間與分批比例納入 <a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">rollout strategy</a> 的風險控制（backend 延伸見 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a>）。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a> 階段負責處理已安裝版本。App 發布後會長期存在多個使用者版本，因此 hotfix、remote config、kill switch 與後端相容性要一起設計（相容治理延伸見 <a href="/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract</a>）。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>簽章憑證是發布能力的一部分，要用 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 管理。</li>
<li>版本號與 build number 要可追溯到 commit 與 artifact。</li>
<li>Store review 會讓 rollback 和 hotfix 變慢，風險要提前用 <a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag</a> 控制。</li>
<li>Client / server contract 要支援多版本共存。</li>
<li>Crash reporting 與 phased rollout 是發布後 gate 的一部分。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="signing-store-rollout-flow/">App 簽章、商店審核與分批發布流程</a></td>
          <td>Signing, review and rollout</td>
          <td>管理簽章、審核、分批發布與多版本共存</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>App 發布主流程：讀 <a href="signing-store-rollout-flow/">App 簽章、商店審核與分批發布流程</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>失敗處理：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
</ul>
]]></content:encoded></item><item><title>Container 部署設計</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/container-deployment/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/container-deployment/</guid><description>&lt;p>Container 部署讓 collector 完全隔離於 host 環境，開源使用者用 &lt;code>docker run&lt;/code> 一行部署，不需要安裝 Go 或管理 binary 版本。但 SQLite 在 container 中有特殊的 I/O 和持久化考量 — overlay filesystem 的寫入延遲和 container 生命週期對資料持久性的影響需要在部署設計中處理。&lt;/p>
&lt;h2 id="dockerfile-設計">Dockerfile 設計&lt;/h2>
&lt;p>Multi-stage build 把編譯環境和執行環境分離。Build stage 用 Go 官方 image 編譯 binary，runtime stage 只包含 binary 和必要的 CA 憑證。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="s"> golang:1.22-alpine AS build&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /src&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> go.mod go.sum ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> go mod download&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> . .&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> &lt;span class="nv">CGO_ENABLED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span> go build -o /collector ./cmd/collector&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> alpine:3.20&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> apk add --no-cache ca-certificates tzdata&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --from&lt;span class="o">=&lt;/span>build /collector /usr/local/bin/collector&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> adduser -D -u &lt;span class="m">1000&lt;/span> monitor&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="s"> monitor&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">EXPOSE&lt;/span>&lt;span class="s"> 8080&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENTRYPOINT&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;collector&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最終 image 包含 Go binary（~15MB）+ alpine base（~7MB）+ ca-certificates，總大小目標 &amp;lt; 25MB。用 &lt;code>scratch&lt;/code> 替代 &lt;code>alpine&lt;/code> 可以再小 7MB，但失去 shell debug 能力。&lt;/p>
&lt;h2 id="sqlite-在-container-中的-io-考量">SQLite 在 Container 中的 I/O 考量&lt;/h2>
&lt;p>Docker 的 overlay2 storage driver 在每次 fsync 時經過 overlay 層。SQLite 的 WAL mode 依賴 fsync 確保寫入持久性 — 每筆 transaction commit 觸發一次 fsync。Overlay 層增加的延遲讓每筆 fsync 慢 20-40%（取決於 host 的 storage driver 和檔案系統）。&lt;/p>
&lt;h3 id="volume-mount-繞過-overlay">Volume mount 繞過 overlay&lt;/h3>
&lt;p>把 SQLite 的資料目錄掛載為 host volume（&lt;code>-v /host/data:/data&lt;/code>），SQLite 直接寫 host 檔案系統、繞過 overlay 層。寫入效能和同機部署的 binary 版本相當。&lt;/p>
&lt;p>不用 volume mount 的風險：container 刪除時 overlay 層的資料一起消失。&lt;code>docker rm&lt;/code> = 所有事件資料消失。即使只是 &lt;code>docker run&lt;/code> 新版本的 image 也會建立新 container，舊 container 的資料不會自動遷移。&lt;/p>
&lt;h2 id="volume-mount-設計">Volume Mount 設計&lt;/h2>
&lt;p>兩個目錄分開掛載，職責和權限不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Mount&lt;/th>
 &lt;th>Container 路徑&lt;/th>
 &lt;th>Host 路徑（範例）&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>/data&lt;/code>&lt;/td>
 &lt;td>&lt;code>./monitor-data&lt;/code>&lt;/td>
 &lt;td>read-write&lt;/td>
 &lt;td>SQLite DB + WAL + 匯出檔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設定&lt;/td>
 &lt;td>&lt;code>/config&lt;/code>&lt;/td>
 &lt;td>&lt;code>./monitor-config&lt;/code>&lt;/td>
 &lt;td>read-only&lt;/td>
 &lt;td>retention config + rule config + sensor config&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Container 內用非 root user（UID 1000）執行。Host 的 volume 目錄 ownership 需要對應：&lt;/p></description><content:encoded><![CDATA[<p>Container 部署讓 collector 完全隔離於 host 環境，開源使用者用 <code>docker run</code> 一行部署，不需要安裝 Go 或管理 binary 版本。但 SQLite 在 container 中有特殊的 I/O 和持久化考量 — overlay filesystem 的寫入延遲和 container 生命週期對資料持久性的影響需要在部署設計中處理。</p>
<h2 id="dockerfile-設計">Dockerfile 設計</h2>
<p>Multi-stage build 把編譯環境和執行環境分離。Build stage 用 Go 官方 image 編譯 binary，runtime stage 只包含 binary 和必要的 CA 憑證。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">FROM</span><span class="s"> golang:1.22-alpine AS build</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /src</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="err"></span><span class="k">COPY</span> go.mod go.sum ./<span class="err">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="err"></span><span class="k">RUN</span> go mod download<span class="err">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="err"></span><span class="k">COPY</span> . .<span class="err">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="err"></span><span class="k">RUN</span> <span class="nv">CGO_ENABLED</span><span class="o">=</span><span class="m">0</span> go build -o /collector ./cmd/collector<span class="err">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="err"></span><span class="k">FROM</span><span class="s"> alpine:3.20</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="err"></span><span class="k">RUN</span> apk add --no-cache ca-certificates tzdata<span class="err">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="err"></span><span class="k">COPY</span> --from<span class="o">=</span>build /collector /usr/local/bin/collector<span class="err">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="err"></span><span class="k">RUN</span> adduser -D -u <span class="m">1000</span> monitor<span class="err">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="err"></span><span class="k">USER</span><span class="s"> monitor</span><span class="err">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="err"></span><span class="k">EXPOSE</span><span class="s"> 8080</span><span class="err">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;collector&#34;</span><span class="p">]</span></span></span></code></pre></div><p>最終 image 包含 Go binary（~15MB）+ alpine base（~7MB）+ ca-certificates，總大小目標 &lt; 25MB。用 <code>scratch</code> 替代 <code>alpine</code> 可以再小 7MB，但失去 shell debug 能力。</p>
<h2 id="sqlite-在-container-中的-io-考量">SQLite 在 Container 中的 I/O 考量</h2>
<p>Docker 的 overlay2 storage driver 在每次 fsync 時經過 overlay 層。SQLite 的 WAL mode 依賴 fsync 確保寫入持久性 — 每筆 transaction commit 觸發一次 fsync。Overlay 層增加的延遲讓每筆 fsync 慢 20-40%（取決於 host 的 storage driver 和檔案系統）。</p>
<h3 id="volume-mount-繞過-overlay">Volume mount 繞過 overlay</h3>
<p>把 SQLite 的資料目錄掛載為 host volume（<code>-v /host/data:/data</code>），SQLite 直接寫 host 檔案系統、繞過 overlay 層。寫入效能和同機部署的 binary 版本相當。</p>
<p>不用 volume mount 的風險：container 刪除時 overlay 層的資料一起消失。<code>docker rm</code> = 所有事件資料消失。即使只是 <code>docker run</code> 新版本的 image 也會建立新 container，舊 container 的資料不會自動遷移。</p>
<h2 id="volume-mount-設計">Volume Mount 設計</h2>
<p>兩個目錄分開掛載，職責和權限不同：</p>
<table>
  <thead>
      <tr>
          <th>Mount</th>
          <th>Container 路徑</th>
          <th>Host 路徑（範例）</th>
          <th>權限</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料</td>
          <td><code>/data</code></td>
          <td><code>./monitor-data</code></td>
          <td>read-write</td>
          <td>SQLite DB + WAL + 匯出檔</td>
      </tr>
      <tr>
          <td>設定</td>
          <td><code>/config</code></td>
          <td><code>./monitor-config</code></td>
          <td>read-only</td>
          <td>retention config + rule config + sensor config</td>
      </tr>
  </tbody>
</table>
<p>Container 內用非 root user（UID 1000）執行。Host 的 volume 目錄 ownership 需要對應：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mkdir -p monitor-data monitor-config
</span></span><span class="line"><span class="ln">2</span><span class="cl">chown 1000:1000 monitor-data</span></span></code></pre></div><h2 id="graceful-shutdown">Graceful Shutdown</h2>
<p><code>docker stop</code> 送 SIGTERM → collector 收到後執行 shutdown 序列：</p>
<ol>
<li>停止接受新的 HTTP request（listener close）</li>
<li>等待 in-flight request 完成（5 秒 context timeout）</li>
<li>Flush pending writes（尚未寫入 storage 的事件，5 秒）</li>
<li>停止定期 job（downsample / purge / rule engine 定期評估）</li>
<li>SQLite WAL checkpoint（TRUNCATE mode，15 秒）</li>
<li>關閉 DB connection</li>
<li>退出</li>
</ol>
<p>步驟 2-5 合計超時上限 25 秒。這個序列對應 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Backend 5.6 Platform Lifecycle Contract</a> 的 shutdown → drain 狀態：步驟 1-2 是 drain（停接新工作、等在途完成），步驟 3-6 是 shutdown（flush 狀態和釋放資源）。Collector 屬於短 request API 的 workload 類型（drain 窗口 5-30 秒），但多了 WAL checkpoint 步驟，讓 shutdown 時間可能超過一般 HTTP 服務。PID 1 信號處理的設計考量（exec form、避免 shell 攔截 SIGTERM）見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">Backend 5.1 PID 1 與信號處理</a>。</p>
<p><code>docker stop</code> 預設等 10 秒後送 SIGKILL。如果 WAL checkpoint 在大量未 checkpoint 的資料下需要超過 10 秒，Docker Compose 可以調 <code>stop_grace_period: 30s</code>。</p>
<p>SQLite 的 WAL 設計支援 crash recovery — SIGKILL 後 WAL 檔案仍在，下次開啟 DB 時自動 replay。但非 graceful shutdown 可能丟失 channel 中尚未寫入的事件（已收到 HTTP 202 但還在 buffer 中的事件）。</p>
<h2 id="資源限制">資源限制</h2>
<table>
  <thead>
      <tr>
          <th>資源</th>
          <th>建議值（自用）</th>
          <th>建議值（小團隊）</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Memory</td>
          <td>256MB</td>
          <td>512MB</td>
          <td>Collector + SQLite page cache + Go runtime</td>
      </tr>
      <tr>
          <td>CPU</td>
          <td>0.5 核</td>
          <td>1 核</td>
          <td>I/O bound、CPU 通常不是瓶頸</td>
      </tr>
      <tr>
          <td>磁碟</td>
          <td>volume mount 容量</td>
          <td>volume mount 容量</td>
          <td>保留策略控制、和 host 磁碟共享</td>
      </tr>
  </tbody>
</table>
<p>Memory 限制設太緊會觸發 OOMKill — container 突然消失且無 log。設定 memory limit 前先觀察 collector 的 baseline 記憶體使用（<code>docker stats</code>），再乘以 1.5 安全係數。CPU request/limit 的設定策略（guaranteed vs burstable QoS）和 memory limit 與 OOM 的判讀見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">Backend 5.1 Resource Limit</a>。</p>
<h2 id="docker-compose-範例">Docker Compose 範例</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">collector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">tarrragon/monitor:latest</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8080:8080&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span>- <span class="l">./monitor-data:/data</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span>- <span class="l">./monitor-config:/config:ro</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span>- <span class="l">MONITOR_STORAGE=sqlite</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span>- <span class="l">MONITOR_DB_PATH=/data/events.db</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">stop_grace_period</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">256M</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">cpus</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;0.5&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;wget&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-q&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--spider&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;http://localhost:8080/health&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">3</span></span></span></code></pre></div><p><code>restart: unless-stopped</code> 讓 container 在 crash 或 host 重啟後自動恢復。<code>healthcheck</code> 讓 Docker 偵測 collector 是否真的在回應 — 只有 process 活著但 HTTP 不回應的場景也會被標記為 unhealthy。</p>
<h2 id="和同機部署的效能對照">和同機部署的效能對照</h2>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>同機 binary</th>
          <th>Container + volume mount</th>
          <th>Container 無 volume（overlay）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫入吞吐（Mac SSD）</td>
          <td>~5,000/sec</td>
          <td>~4,500/sec（-10%）</td>
          <td>~3,000/sec（-40%）</td>
      </tr>
      <tr>
          <td>寫入吞吐（Linux VPS）</td>
          <td>~3,000/sec</td>
          <td>~2,700/sec（-10%）</td>
          <td>~1,800/sec（-40%）</td>
      </tr>
      <tr>
          <td>查詢延遲</td>
          <td>baseline</td>
          <td>baseline（volume = 直接讀 host）</td>
          <td>+20%（overlay 讀取開銷小）</td>
      </tr>
      <tr>
          <td>啟動時間</td>
          <td>&lt; 100ms</td>
          <td>&lt; 500ms（container 啟動開銷）</td>
          <td>同左</td>
      </tr>
      <tr>
          <td>記憶體額外開銷</td>
          <td>0</td>
          <td>~10-20MB（container runtime）</td>
          <td>同左</td>
      </tr>
  </tbody>
</table>
<p>Volume mount 後效能差異只有 ~10%（Go HTTP handler 的 overhead 大於 volume mount 的 overhead）。不用 volume mount 時 overlay fs 的 fsync 開銷顯著 — 寫入吞吐降 40%。</p>
<h2 id="何時用-container何時用-binary">何時用 container、何時用 binary</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開源使用者快速試用</td>
          <td>Container</td>
          <td><code>docker run</code> 一行、不需裝 Go</td>
      </tr>
      <tr>
          <td>長期自用部署</td>
          <td>Binary + systemd</td>
          <td>效能最佳、無 container overhead</td>
      </tr>
      <tr>
          <td>CI/CD 測試環境</td>
          <td>Container</td>
          <td>可拋棄式、每次乾淨環境</td>
      </tr>
      <tr>
          <td>Kubernetes 部署</td>
          <td>Container</td>
          <td>pod spec 標準化</td>
      </tr>
      <tr>
          <td>Raspberry Pi / 邊緣設備</td>
          <td>Binary</td>
          <td>低資源環境避免 container overhead</td>
      </tr>
  </tbody>
</table>
<h2 id="斷網環境的部署考量">斷網環境的部署考量</h2>
<p>Collector 在斷網環境（air-gapped）裡的部署跟連網環境的主要差異有三點。第一，SDK 的 endpoint 從外部 URL（<code>https://collect.example.com</code>）改為內網地址（<code>http://collector.internal:8080</code>），SDK 設定檔裡的 endpoint 要能按環境切換。第二，Collector 的 container image 無法從 Docker Hub 拉取——需要透過 content ferry 搬運映像、推送到內網的 private registry（Harbor 或 Docker Registry），Dockerfile 的 base image 來源也要改指 private registry。第三，Collector 的 storage backend 只能用本地磁碟或 NFS，不能用雲端物件儲存——SQLite backend 在斷網環境反而是優勢（零外部依賴），儲存容量規劃要在部署前就確定，因為斷網環境的磁碟擴容流程可能需要數週。</p>
<p>SDK 的 offline buffer（見<a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">SDK 設計：offline-buffer</a>）在斷網環境更重要——如果 Collector 重啟或暫時不可達，SDK 端的 buffer 是唯一能保住事件的機制。</p>
<p>斷網環境的 infra 層監控（Prometheus / Grafana / Loki）設定見<a href="/blog/infra/air-gapped/air-gapped-monitoring/" data-link-title="斷網環境的監控與可觀測性" data-link-desc="Self-hosted 監控（Prometheus &#43; Grafana）、離線 log 收集（Loki / ELK）、不能 phone home 的告警、NTP 時間同步">斷網環境的監控與可觀測性</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>SQLite 效能基準的詳細數字 → <a href="/blog/monitoring/04-collector/sqlite-performance-baseline/" data-link-title="SQLite Backend 效能基準" data-link-desc="寫入吞吐 / 查詢延遲 / 資源消耗的量化預期 — 不同硬體環境下 SQLite 能撐多少、邊界在哪、怎麼實測">SQLite Backend 效能基準</a></li>
<li>可插拔 Storage Backend 架構 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
<li>Container runtime 通用原則（base image 選擇、build 可重現性、PID 1 信號處理）→ <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">Backend 5.1 Container 與 Runtime</a></li>
<li>生命週期合約（startup / readiness / drain / shutdown 的責任分類）→ <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Backend 5.6 Platform Lifecycle Contract</a></li>
<li>容器化資源設計的通用原則 → <a href="/blog/devops/05-capacity-planning/container-resource-design/" data-link-title="容器化資源設計" data-link-desc="Container 的 memory / CPU / 磁碟限制設計 — 資源限制設太緊 OOMKill、設太鬆擠壓其他服務、overlay filesystem 的 I/O 影響">DevOps 容器化資源設計</a></li>
<li>服務探活和自動恢復 → <a href="/blog/devops/04-service-health/" data-link-title="模組四：服務探活與自動恢復" data-link-desc="服務掛了怎麼自動發現和恢復 — health check 設計、liveness vs readiness、systemd watchdog、process supervisor">DevOps 服務探活</a></li>
</ul>
]]></content:encoded></item><item><title>Serverless 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/serverless-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/serverless-deploy/</guid><description>&lt;p>Serverless 部署 CI/CD 的核心責任是把函式型服務安全推進到受管執行環境。它和長駐服務不同，風險集中在 artifact 打包、runtime 相容、權限設定、版本別名與冷啟動行為。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>Serverless 發布通常以函式版本為單位，並透過 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias&lt;/a> 或流量權重切換。部署步驟看起來短，但對權限、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source&lt;/a>、重試政策與 observability 欄位要求很高。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Serverless 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>function bundle、dependency、runtime target&lt;/td>
 &lt;td>package 是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>function version、alias、traffic shift&lt;/td>
 &lt;td>新舊版本是否可並存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Permission&lt;/td>
 &lt;td>IAM、resource policy、secret scope&lt;/td>
 &lt;td>執行是否具最小權限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Event Source&lt;/td>
 &lt;td>queue/topic/http trigger 設定&lt;/td>
 &lt;td>重試與死信策略是否明確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>alias rollback、disable trigger&lt;/td>
 &lt;td>故障時是否可快速止血&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>部署前要先驗證 runtime 與依賴版本，避免 deploy 成功但 invocation 失敗。&lt;/li>
&lt;li>事件觸發型函式要明確設定 retry、dead-letter 或回放策略。&lt;/li>
&lt;li>權限設定要收斂到最小範圍，避免函式擴權風險。&lt;/li>
&lt;li>冷啟動與併發上限要納入發布後觀測指標。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&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;a href="function-version-event-flow/">Serverless function 版本、事件來源與回復流程&lt;/a>&lt;/td>
 &lt;td>Function version and event&lt;/td>
 &lt;td>管理版本別名、事件來源、權限與回復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Serverless 發布主流程：讀 &lt;a href="function-version-event-flow/">Serverless function 版本、事件來源與回復流程&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>失敗處理：讀 &lt;a href="../github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>。&lt;/li>
&lt;li>Backend 相關概念：讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">Serverless / worker 相關知識卡&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Serverless 部署 CI/CD 的核心責任是把函式型服務安全推進到受管執行環境。它和長駐服務不同，風險集中在 artifact 打包、runtime 相容、權限設定、版本別名與冷啟動行為。</p>
<h2 id="場域定位">場域定位</h2>
<p>Serverless 發布通常以函式版本為單位，並透過 <a href="/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias</a> 或流量權重切換。部署步驟看起來短，但對權限、<a href="/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source</a>、重試政策與 observability 欄位要求很高。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Serverless 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>function bundle、dependency、runtime target</td>
          <td>package 是否可重現</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>function version、alias、traffic shift</td>
          <td>新舊版本是否可並存</td>
      </tr>
      <tr>
          <td>Permission</td>
          <td>IAM、resource policy、secret scope</td>
          <td>執行是否具最小權限</td>
      </tr>
      <tr>
          <td>Event Source</td>
          <td>queue/topic/http trigger 設定</td>
          <td>重試與死信策略是否明確</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>alias rollback、disable trigger</td>
          <td>故障時是否可快速止血</td>
      </tr>
  </tbody>
</table>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>部署前要先驗證 runtime 與依賴版本，避免 deploy 成功但 invocation 失敗。</li>
<li>事件觸發型函式要明確設定 retry、dead-letter 或回放策略。</li>
<li>權限設定要收斂到最小範圍，避免函式擴權風險。</li>
<li>冷啟動與併發上限要納入發布後觀測指標。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="function-version-event-flow/">Serverless function 版本、事件來源與回復流程</a></td>
          <td>Function version and event</td>
          <td>管理版本別名、事件來源、權限與回復</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Serverless 發布主流程：讀 <a href="function-version-event-flow/">Serverless function 版本、事件來源與回復流程</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>失敗處理：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
<li>Backend 相關概念：讀 <a href="/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">Serverless / worker 相關知識卡</a>。</li>
</ul>
]]></content:encoded></item><item><title>Data Pipeline 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/data-pipeline-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/data-pipeline-deploy/</guid><description>&lt;p>Data Pipeline 部署 CI/CD 的核心責任是把資料處理邏輯推進到生產環境，同時維持資料正確性與可回復性。它和 API 部署不同，重點在 schema 相容、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun&lt;/a> 風險。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>Data pipeline 常包含 batch job、stream processor、dbt model 或 workflow scheduler。部署判斷不只看程式可執行，還要看資料是否可追溯、可對帳、可修復。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Data pipeline 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>transform code、DAG、query model&lt;/td>
 &lt;td>版本是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Validation&lt;/td>
 &lt;td>schema check、sample run、contract check&lt;/td>
 &lt;td>輸出是否維持相容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>job version、schedule、trigger&lt;/td>
 &lt;td>新流程是否正確接管&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a>&lt;/td>
 &lt;td>歷史資料補算與節流&lt;/td>
 &lt;td>是否有 checkpoint 與停損條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>rerun、rollback、forward fix&lt;/td>
 &lt;td>異常資料是否可修補&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>schema 變更要先定義相容窗口，再切換 downstream。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a> 要有節流與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint&lt;/a>，避免壓垮上游與儲存層。&lt;/li>
&lt;li>部署後需比對新舊輸出一致性，建立 correctness check。&lt;/li>
&lt;li>重跑流程要有 runbook，避免人工臨場判斷失誤。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&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;a href="backfill-checkpoint-rerun-flow/">Data pipeline backfill、checkpoint 與 rerun 流程&lt;/a>&lt;/td>
 &lt;td>Backfill, checkpoint and rerun&lt;/td>
 &lt;td>控制歷史補算、重跑與資料修復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Data pipeline 發布主流程：讀 &lt;a href="backfill-checkpoint-rerun-flow/">Data pipeline backfill、checkpoint 與 rerun 流程&lt;/a>。&lt;/li>
&lt;li>後端資料遷移概念：讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration&lt;/a>。&lt;/li>
&lt;li>資料修補與比對：讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correctness-check/" data-link-title="Correctness Check" data-link-desc="說明遷移或重構期間如何驗證新舊結果是否符合規則">Correctness Check&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Data Pipeline 部署 CI/CD 的核心責任是把資料處理邏輯推進到生產環境，同時維持資料正確性與可回復性。它和 API 部署不同，重點在 schema 相容、<a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a>、<a href="/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint</a> 與 <a href="/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun</a> 風險。</p>
<h2 id="場域定位">場域定位</h2>
<p>Data pipeline 常包含 batch job、stream processor、dbt model 或 workflow scheduler。部署判斷不只看程式可執行，還要看資料是否可追溯、可對帳、可修復。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Data pipeline 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>transform code、DAG、query model</td>
          <td>版本是否可重現</td>
      </tr>
      <tr>
          <td>Validation</td>
          <td>schema check、sample run、contract check</td>
          <td>輸出是否維持相容</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>job version、schedule、trigger</td>
          <td>新流程是否正確接管</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a></td>
          <td>歷史資料補算與節流</td>
          <td>是否有 checkpoint 與停損條件</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>rerun、rollback、forward fix</td>
          <td>異常資料是否可修補</td>
      </tr>
  </tbody>
</table>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>schema 變更要先定義相容窗口，再切換 downstream。</li>
<li><a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a> 要有節流與 <a href="/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint</a>，避免壓垮上游與儲存層。</li>
<li>部署後需比對新舊輸出一致性，建立 correctness check。</li>
<li>重跑流程要有 runbook，避免人工臨場判斷失誤。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="backfill-checkpoint-rerun-flow/">Data pipeline backfill、checkpoint 與 rerun 流程</a></td>
          <td>Backfill, checkpoint and rerun</td>
          <td>控制歷史補算、重跑與資料修復</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Data pipeline 發布主流程：讀 <a href="backfill-checkpoint-rerun-flow/">Data pipeline backfill、checkpoint 與 rerun 流程</a>。</li>
<li>後端資料遷移概念：讀 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration</a>。</li>
<li>資料修補與比對：讀 <a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill</a> 與 <a href="/blog/backend/knowledge-cards/correctness-check/" data-link-title="Correctness Check" data-link-desc="說明遷移或重構期間如何驗證新舊結果是否符合規則">Correctness Check</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨</title><link>https://tarrragon.github.io/blog/llm/04-applications/static-and-serverless-rag-deployment/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/static-and-serverless-rag-deployment/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &amp;#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &amp;#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">4.12 embedding model&lt;/a> 寫的是「RAG 在做什麼、embedding 怎麼選」、預設「有 backend server」可跑 embedding 跟 LLM。但實際大量場景是&lt;strong>沒 backend&lt;/strong> — 個人 blog（Hugo / Jekyll / Astro）想加智能搜尋、docs site 想做 LLM 對話、demo 想離線跑。本章把這條「靜態 / serverless RAG」路線拆成四個方案、配合靜態場景&lt;strong>特有的資安議題&lt;/strong>（這些議題模組六沒覆蓋、屬本章新增）。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>區分四種 RAG deployment 方案（純前端 / edge serverless / RAG SaaS / 純文字 search）。&lt;/li>
&lt;li>對自己場景判斷該選哪個方案、看資料量 / 隱私 / 預算。&lt;/li>
&lt;li>認識靜態場景特有的資安議題：API key 暴露、CORS、abuse、第三方 SaaS 供應鏈、client-side 模型完整性。&lt;/li>
&lt;li>知道哪些資安議題在 &lt;a href="https://tarrragon.github.io/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六&lt;/a> 已覆蓋、哪些是本章獨有。&lt;/li>
&lt;/ol>
&lt;h2 id="為什麼這個議題重要">為什麼這個議題重要&lt;/h2>
&lt;p>傳統 RAG 教材預設架構：&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">User → backend server → embedding API → vector DB → LLM API → response&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>需要 backend 可執行 server-side code、藏 API key、控制 rate limit。但個人開發者場景常見的 deployment：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>Backend？&lt;/th>
 &lt;th>部署方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>個人 Hugo blog&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>GitHub Pages / Cloudflare Pages&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>開源專案 docs site&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>GitHub Pages / Netlify / Vercel&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>商品 landing page&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>CDN + S3&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Static-export Next.js / Astro&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>同上&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些場景跟「個人 dev 跑本地 LLM」並列、是教材的合理覆蓋面。&lt;/p>
&lt;h2 id="四種-deployment-方案總覽">四種 deployment 方案總覽&lt;/h2>





&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"> embedding vector LLM call
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> 搜尋 DB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">方案 1 純前端 browser browser browser（WebLLM）或 user-key 直 call
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">方案 2 edge serverless edge fn edge DB edge fn → LLM API
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">方案 3 RAG SaaS SaaS SaaS SaaS（或自 call）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">方案 4 純文字 search N/A static idx N/A（不是 RAG）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>四方案快速對比：&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG</a> 跟 <a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">4.12 embedding model</a> 寫的是「RAG 在做什麼、embedding 怎麼選」、預設「有 backend server」可跑 embedding 跟 LLM。但實際大量場景是<strong>沒 backend</strong> — 個人 blog（Hugo / Jekyll / Astro）想加智能搜尋、docs site 想做 LLM 對話、demo 想離線跑。本章把這條「靜態 / serverless RAG」路線拆成四個方案、配合靜態場景<strong>特有的資安議題</strong>（這些議題模組六沒覆蓋、屬本章新增）。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>區分四種 RAG deployment 方案（純前端 / edge serverless / RAG SaaS / 純文字 search）。</li>
<li>對自己場景判斷該選哪個方案、看資料量 / 隱私 / 預算。</li>
<li>認識靜態場景特有的資安議題：API key 暴露、CORS、abuse、第三方 SaaS 供應鏈、client-side 模型完整性。</li>
<li>知道哪些資安議題在 <a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六</a> 已覆蓋、哪些是本章獨有。</li>
</ol>
<h2 id="為什麼這個議題重要">為什麼這個議題重要</h2>
<p>傳統 RAG 教材預設架構：</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">User → backend server → embedding API → vector DB → LLM API → response</span></span></code></pre></div><p>需要 backend 可執行 server-side code、藏 API key、控制 rate limit。但個人開發者場景常見的 deployment：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Backend？</th>
          <th>部署方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>個人 Hugo blog</td>
          <td>無</td>
          <td>GitHub Pages / Cloudflare Pages</td>
      </tr>
      <tr>
          <td>開源專案 docs site</td>
          <td>無</td>
          <td>GitHub Pages / Netlify / Vercel</td>
      </tr>
      <tr>
          <td>商品 landing page</td>
          <td>無</td>
          <td>CDN + S3</td>
      </tr>
      <tr>
          <td>Static-export Next.js / Astro</td>
          <td>無</td>
          <td>同上</td>
      </tr>
  </tbody>
</table>
<p>這些場景跟「個人 dev 跑本地 LLM」並列、是教材的合理覆蓋面。</p>
<h2 id="四種-deployment-方案總覽">四種 deployment 方案總覽</h2>





<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">                          embedding   vector       LLM call
</span></span><span class="line"><span class="ln">2</span><span class="cl">                          搜尋          DB
</span></span><span class="line"><span class="ln">3</span><span class="cl">方案 1 純前端            browser       browser     browser（WebLLM）或 user-key 直 call
</span></span><span class="line"><span class="ln">4</span><span class="cl">方案 2 edge serverless   edge fn       edge DB     edge fn → LLM API
</span></span><span class="line"><span class="ln">5</span><span class="cl">方案 3 RAG SaaS          SaaS          SaaS        SaaS（或自 call）
</span></span><span class="line"><span class="ln">6</span><span class="cl">方案 4 純文字 search     N/A           static idx  N/A（不是 RAG）</span></span></code></pre></div><p>四方案快速對比：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>1 純前端</th>
          <th>2 edge serverless</th>
          <th>3 SaaS</th>
          <th>4 純文字 search</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>是否「真 RAG」</td>
          <td>是</td>
          <td>是</td>
          <td>是</td>
          <td><strong>否</strong>（無 LLM）</td>
      </tr>
      <tr>
          <td>隱私</td>
          <td>最強（不離 browser）</td>
          <td>中（信 edge provider）</td>
          <td>弱（信 SaaS）</td>
          <td>最強</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>完全 zero（build 一次）</td>
          <td>每 query 付 edge + LLM</td>
          <td>免費 tier / 按量計費</td>
          <td>Zero</td>
      </tr>
      <tr>
          <td>規模上限</td>
          <td>&lt; 10K chunks</td>
          <td>1M+</td>
          <td>視服務</td>
          <td>視工具</td>
      </tr>
      <tr>
          <td>開發複雜度</td>
          <td>中（要 build pipeline）</td>
          <td>中高（要寫 edge fn）</td>
          <td>低（API 直接用）</td>
          <td>低</td>
      </tr>
      <tr>
          <td>主要資安議題</td>
          <td>模型完整性、user-key 暴露</td>
          <td>edge provider 信任</td>
          <td>SaaS 信任 + 供應鏈</td>
          <td>較少（無 LLM）</td>
      </tr>
  </tbody>
</table>
<h2 id="方案-1純前端-ragbrowser-side-everything">方案 1：純前端 RAG（browser-side everything）</h2>
<p>整個 RAG pipeline 都跑在使用者瀏覽器：</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">Build time（Hugo build / CI pipeline）：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  content/*.md
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    ↓ 抽段、chunk
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    ↓ embedding model（Node.js 版 sentence-transformers）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  embeddings.json（每個 chunk 一個 vector）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    ↓ 跟 HTML 一起 deploy
</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">Runtime（user browser）：
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  User query
</span></span><span class="line"><span class="ln">10</span><span class="cl">    ↓ load @xenova/transformers + embeddings.json（首訪載 ~50MB）
</span></span><span class="line"><span class="ln">11</span><span class="cl">    ↓ embed query in browser
</span></span><span class="line"><span class="ln">12</span><span class="cl">    ↓ cosine similarity vs embeddings.json
</span></span><span class="line"><span class="ln">13</span><span class="cl">  top-K chunks
</span></span><span class="line"><span class="ln">14</span><span class="cl">    ↓ LLM call（兩條子路線、見下）
</span></span><span class="line"><span class="ln">15</span><span class="cl">  Response in browser</span></span></code></pre></div><p>LLM 的兩條子路線：</p>
<table>
  <thead>
      <tr>
          <th>子路線</th>
          <th>機制</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong><a href="/blog/llm/knowledge-cards/client-side-llm/" data-link-title="Client-Side LLM / Embedding" data-link-desc="在 browser 內直接跑 LLM 或 embedding model 的 paradigm、靜態網站做 RAG 的關鍵基底">Client-side LLM</a></strong></td>
          <td>WebLLM / wllama 跑 &lt; 4B model</td>
          <td>完全離線、首訪載 1-3GB 模型、隱私最強</td>
      </tr>
      <tr>
          <td><strong>User 自帶 API key</strong></td>
          <td>前端讀 localStorage 的 key、直 call API</td>
          <td>高品質（雲端旗艦）、key 暴露、需要使用者授信</td>
      </tr>
  </tbody>
</table>
<p>實作概要：</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"># Build time（Node.js script）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npx @xenova/transformers-cli embed content/*.md &gt; static/embeddings.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"># Frontend（簡化版）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">import <span class="o">{</span> pipeline <span class="o">}</span> from <span class="s1">&#39;@xenova/transformers&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">const <span class="nv">embedder</span> <span class="o">=</span> await pipeline<span class="o">(</span><span class="s1">&#39;feature-extraction&#39;</span>, <span class="s1">&#39;nomic-embed-text-v1.5&#39;</span><span class="o">)</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">const <span class="nv">queryVec</span> <span class="o">=</span> await embedder<span class="o">(</span>userQuery, <span class="o">{</span> pooling: <span class="s1">&#39;mean&#39;</span> <span class="o">})</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">const <span class="nv">ranked</span> <span class="o">=</span> embeddings.map<span class="o">(</span><span class="nv">c</span> <span class="o">=</span>&gt; <span class="o">({</span> ...c, score: cosineSim<span class="o">(</span>c.vec, queryVec.data<span class="o">)</span> <span class="o">}))</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">                          .sort<span class="o">((</span>a,b<span class="o">)</span> <span class="o">=</span>&gt; b.score - a.score<span class="o">)</span>.slice<span class="o">(</span>0, 5<span class="o">)</span><span class="p">;</span></span></span></code></pre></div><p>規模上限：</p>
<ul>
<li>&lt; 1000 chunks：embeddings.json ~ 4MB（1024-dim float32）、輕鬆</li>
<li>1K-10K：~40MB、首訪載入慢但可接受</li>
<li>10K+：純前端開始勉強、考慮方案 2</li>
</ul>
<p><strong>適合場景</strong>：個人 blog、docs site、demo、隱私敏感、規模 &lt; 10K chunks。</p>
<h2 id="方案-2靜態--edge-serverless">方案 2：靜態 + edge serverless</h2>
<p>「靜態主站 + edge function 處理動態請求」：</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">靜態前端（HTML / JS、Hugo / Astro）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   ↓ fetch /api/rag
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Edge function（Cloudflare Workers / Vercel Edge / Netlify Functions）
</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">Embedding API（OpenAI / Voyage）
</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">Vector DB（Cloudflare Vectorize / Pinecone / Turso vector / Upstash Vector）
</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">LLM API（OpenAI / Anthropic / Cloudflare AI Gateway）
</span></span><span class="line"><span class="ln">10</span><span class="cl">   ↓ response
</span></span><span class="line"><span class="ln">11</span><span class="cl">靜態前端</span></span></code></pre></div><p>對使用者體感跟「有 backend」一樣、但你不用維護 server / 不用 sysadmin。</p>
<p>主流元件搭配：</p>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>Cloudflare 全家桶</th>
          <th>Vercel / 其他</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Edge runtime</td>
          <td>Workers</td>
          <td>Vercel Edge / Netlify Functions</td>
      </tr>
      <tr>
          <td>Vector DB</td>
          <td>Cloudflare Vectorize</td>
          <td>Pinecone / Turso / Upstash</td>
      </tr>
      <tr>
          <td>Embedding</td>
          <td>Workers AI 內建模型 / OpenAI</td>
          <td>OpenAI / Voyage</td>
      </tr>
      <tr>
          <td>LLM</td>
          <td>Workers AI / AI Gateway 轉發</td>
          <td>OpenAI / Anthropic</td>
      </tr>
  </tbody>
</table>
<p>關鍵特性：</p>
<ol>
<li><strong>API key 不暴露在 browser</strong>：edge function 內讀環境變數、安全</li>
<li><strong>可加 rate limit</strong>：edge function 內判斷 client IP / user agent、避免 abuse</li>
<li><strong>Build-time index 仍重要</strong>：embedding ingestion 通常在 build 階段、不在 runtime</li>
<li><strong>Edge cold start</strong>：第一次 query latency 略高（~100ms 額外）、後續 hot 路徑快</li>
</ol>
<p><strong>適合場景</strong>：規模 1K-100K chunks、想保留近 backend 體驗、可接受少量 cost。這條路線一旦升級到有 backend 的 vector DB、storage 選型（index 結構、維度、成本）就回到 <a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 RAG storage 工程</a> 的判讀。</p>
<h2 id="方案-3靜態--rag-saas">方案 3：靜態 + RAG SaaS</h2>
<p>把整個 RAG stack 外包：</p>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>角色</th>
          <th>免費 tier 上限</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Algolia</td>
          <td>搜尋 + 向量檢索一條龍、build time 同步</td>
          <td>10K records、10K search / month</td>
      </tr>
      <tr>
          <td>Pinecone Cloud</td>
          <td>純 vector DB、自己 call embedding + LLM</td>
          <td>100K vectors（starter）</td>
      </tr>
      <tr>
          <td>Weaviate Cloud</td>
          <td>同上、hybrid search 內建</td>
          <td>14 天 trial</td>
      </tr>
      <tr>
          <td>MeiliSearch Cloud</td>
          <td>BM25 + vector hybrid</td>
          <td>試用</td>
      </tr>
  </tbody>
</table>
<p>API key 設計：</p>
<ul>
<li><strong>search-only key</strong>：只能查詢、無寫入權限、<strong>可安全暴露在 browser</strong>（這是設計支援的）</li>
<li><strong>admin key</strong>：build time CI 用、有寫入權限、必須藏 server-side</li>
</ul>
<p>前端範例（Algolia）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="nx">algoliasearch</span><span class="p">(</span><span class="s1">&#39;APP_ID&#39;</span><span class="p">,</span> <span class="s1">&#39;SEARCH_ONLY_KEY&#39;</span><span class="p">);</span>  <span class="c1">// 可公開
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">index</span> <span class="o">=</span> <span class="nx">client</span><span class="p">.</span><span class="nx">initIndex</span><span class="p">(</span><span class="s1">&#39;my-blog&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">hits</span> <span class="p">}</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">index</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">userQuery</span><span class="p">,</span> <span class="p">{</span> <span class="nx">hitsPerPage</span><span class="o">:</span> <span class="mi">5</span> <span class="p">});</span></span></span></code></pre></div><p><strong>適合場景</strong>：想最快上線、不在乎 vendor lock-in、規模中小、retrieval-only（不需要 LLM 對話）。</p>
<h2 id="方案-4靜態--純文字-search不是真-rag">方案 4：靜態 + 純文字 search（不是真 RAG）</h2>
<p>Pagefind、Stork、lunr.js、FlexSearch — build time 產靜態 search index、純前端查詢。</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pagefind</td>
          <td>static-first、自動 chunking、CJK 友善</td>
      </tr>
      <tr>
          <td>Stork</td>
          <td>Rust 寫的 keyword search、輕量</td>
      </tr>
      <tr>
          <td>lunr.js</td>
          <td>純 JS、tf-idf BM25 風格</td>
      </tr>
      <tr>
          <td>FlexSearch</td>
          <td>同上、體積更小</td>
      </tr>
  </tbody>
</table>
<p><strong>這不是 RAG</strong>：</p>
<ol>
<li><strong>無 embedding similarity</strong>：keyword / fuzzy match、不是語意相似</li>
<li><strong>無 LLM augmentation</strong>：只列文章連結、不生成回答</li>
<li><strong>算 retrieval 的「字面」變體</strong>：見 <a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG</a> 的「語意 vs 字面」段</li>
</ol>
<p><strong>適合場景</strong>：blog 內搜尋只需要找文章、不需要對話、極致 zero-cost。</p>
<h2 id="規模門檻什麼時候該升級方案">規模門檻：什麼時候該升級方案</h2>





<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">&lt; 1K chunks                    → 方案 1 純前端、最簡單
</span></span><span class="line"><span class="ln">2</span><span class="cl">1K - 10K chunks                → 方案 1 或 方案 4
</span></span><span class="line"><span class="ln">3</span><span class="cl">10K - 100K chunks              → 方案 2 edge serverless
</span></span><span class="line"><span class="ln">4</span><span class="cl">100K+ chunks                   → 完整 backend RAG（不再是「靜態」場景）
</span></span><span class="line"><span class="ln">5</span><span class="cl">非 RAG、只要找文章             → 方案 4（Pagefind 等）</span></span></code></pre></div><h2 id="靜態場景特有的資安議題">靜態場景特有的資安議題</h2>
<p>本章節最重要的部分。靜態 / serverless RAG 有些議題模組六沒覆蓋、要在本章補。</p>
<h3 id="1-api-key-暴露--靜態場景的根本問題">1. API key 暴露 — 靜態場景的根本問題</h3>
<p><strong>核心衝突</strong>：靜態網站沒 server-side runtime、藏不了 secret。任何寫在前端 JS / 編進 HTML 的東西、使用者按 F12 都看得到。</p>
<p>對應到 RAG：</p>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>能否前端持有 key</th>
          <th>緩解</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Embedding API（生成方）</td>
          <td>否（admin key 不該暴露）</td>
          <td>build time 用、不放前端</td>
      </tr>
      <tr>
          <td>LLM API（生成方）</td>
          <td>否</td>
          <td>改方案 2 用 edge、或讓使用者自帶 key</td>
      </tr>
      <tr>
          <td>Vector DB（read）</td>
          <td><strong>可</strong>（search-only key 設計支援）</td>
          <td>API 設計時就分權、search-only 可公開</td>
      </tr>
      <tr>
          <td>完整 LLM 跑在前端</td>
          <td>N/A（無 server-side key）</td>
          <td>方案 1 的 Client-side LLM 子路線</td>
      </tr>
  </tbody>
</table>
<p>如果要 LLM 對話功能、三條合法路線：</p>
<ol>
<li><strong>使用者自帶 API key</strong>（如 Anthropic / OpenAI）、存 localStorage、前端直接 call API — 適合 power user、需要使用者授信</li>
<li><strong>WebLLM / wllama 跑前端 LLM</strong> — 模型在 browser、不需 server-side key</li>
<li><strong>方案 2 edge serverless</strong> — key 藏在 edge function、就不是純靜態了</li>
</ol>
<p>寫死 API key 在前端 JS 等於把 key 公開、會被 scraper 撿走燒爆 quota — 這是 <strong>anti-pattern</strong>、跟 <a href="/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4 跨雲端 / 本地資料邊界</a> 提到「API key 寫死 config」的延伸版（前端更嚴重、所有訪客都看得到）。</p>
<h3 id="2-user-query-隱私">2. User query 隱私</h3>
<p>靜態場景的 query 走向：</p>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>Query 走向</th>
          <th>誰能看到</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 純前端 + WebLLM</td>
          <td>從不離 browser</td>
          <td>只有使用者本人</td>
      </tr>
      <tr>
          <td>1 + user API key</td>
          <td>Browser → 雲端 vendor</td>
          <td>該 vendor（依政策）</td>
      </tr>
      <tr>
          <td>2 edge serverless</td>
          <td>Browser → edge → 雲端 API</td>
          <td>Edge provider + LLM vendor</td>
      </tr>
      <tr>
          <td>3 SaaS</td>
          <td>Browser → SaaS</td>
          <td>SaaS provider</td>
      </tr>
  </tbody>
</table>
<p>對應 framing 跟 <a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流</a> 同源 — 但靜態場景的特殊性是「<strong>前端直接出去</strong>」、不像 backend 場景可以加一層中介控制。</p>
<p>特別注意：</p>
<ol>
<li><strong>方案 3 SaaS 的 query 隱私</strong>：Algolia / Pinecone 都會 log query、依政策可能用於改進服務；對隱私敏感場景不適合</li>
<li><strong>Edge provider 的 region</strong>：Cloudflare Workers 的 edge node 可能在跟使用者不同 region 處理、跨境資料法規（GDPR 等）要考慮</li>
<li><strong>Browser extension 偷 query</strong>：使用者裝的 plugin 可能 access 整個頁面、包含 RAG 介面內的 query</li>
</ol>
<h3 id="3-cors--同源策略--browser-特有的安全模型">3. CORS / 同源策略 — Browser 特有的安全模型</h3>
<p>靜態前端 call 任意 API 會撞 CORS（Cross-Origin Resource Sharing）：</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">靜態網站：https://my-blog.com
</span></span><span class="line"><span class="ln">2</span><span class="cl">要 call：https://api.openai.com/v1/...
</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">Browser 檢查 OpenAI 是否在 Access-Control-Allow-Origin 含 my-blog.com
</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">OpenAI 預設允許所有 origin（為了讓前端 SDK 能用）→ 通過
</span></span><span class="line"><span class="ln">7</span><span class="cl">某些 API（Anthropic 早期版本）不允許 browser 直 call → 失敗、必須走 edge</span></span></code></pre></div><p>判讀：</p>
<ul>
<li><strong>能在 browser 直 call 的 API</strong>：OpenAI、Voyage、Algolia（search-only）等明確設計 browser-friendly 的服務</li>
<li><strong>不能 browser 直 call、要 edge proxy</strong>：許多企業 LLM API、私有 vector DB、需要 server-only credentials 的服務</li>
</ul>
<p>CORS 不是「資安漏洞」、是 browser 對「JS 從一個網站 call 另一個網站」的設計約束、用來保護使用者。要繞 CORS 要嗎服務商配合（設 ACAO）、要嗎用 edge function proxy。</p>
<h3 id="4-第三方-saas-信任--跟-60-同源對象換">4. 第三方 SaaS 信任 — 跟 6.0 同源、對象換</h3>
<p><a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0 模型供應鏈與信任邊界</a> 處理的是「<strong>模型權重的信任</strong>」。靜態 RAG SaaS（Algolia / Pinecone / Weaviate Cloud）引入另一條供應鏈：</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">模型供應鏈（6.0 覆蓋）：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  原作者 → quantizer → registry → 你機器
</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">RAG SaaS 供應鏈（本章新增）：
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  你的 content → SaaS embedding service → SaaS vector DB → SaaS retrieval
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    └──────── 全程在 SaaS 內、你信任 SaaS 沒做以下事 ────────┘
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">              - 把你 index 用於訓練他們自己的模型
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">              - 把你 query log 賣給第三方
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">              - 沒做適當 isolation（你跟其他客戶的資料）
</span></span><span class="line"><span class="ln">10</span><span class="cl">              - 沒處理好 supply chain（他們用的 base embedding model）</span></span></code></pre></div><p>判讀類似 <a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 物理 vs 合約保證</a>：本地方案是物理保證（資料不離 browser）、SaaS 方案是合約保證（信 SaaS 的 ToS）。</p>
<h3 id="5-rate-limit--abuse--前端被-scrape-後濫用">5. Rate limit / abuse — 前端被 scrape 後濫用</h3>
<p>靜態 RAG 的特殊 abuse 路徑：</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">攻擊者掃到你的 demo blog
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓ 找到前端載入的 embedding endpoint / LLM endpoint
</span></span><span class="line"><span class="ln">3</span><span class="cl">   ↓ 直接從攻擊者 server 重複 call（不經 browser）
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓ 你的 LLM API quota 燒爆 / SaaS 配額耗光</span></span></code></pre></div><p>緩解：</p>
<ol>
<li><strong>方案 2 edge</strong> + 加 rate limit by IP / token bucket：edge function 內 reject 過量請求</li>
<li><strong>方案 1 純前端 + WebLLM</strong>：根本沒 server-side endpoint 可被 abuse、最安全</li>
<li><strong>方案 3 SaaS</strong> + 用 search-only key 並設 query 上限：SaaS 通常內建 quota</li>
<li><strong>CAPTCHA / Turnstile</strong>：邊緣防護</li>
</ol>
<p>絕對不該做：把 OpenAI / Anthropic API key 寫在前端 JS、想用 rate limit 阻擋 — 攻擊者拿到 key 後不會經過你的 rate limit。</p>
<h3 id="6-client-side-llm-的模型完整性">6. Client-side LLM 的模型完整性</h3>
<p><a href="/blog/llm/knowledge-cards/client-side-llm/" data-link-title="Client-Side LLM / Embedding" data-link-desc="在 browser 內直接跑 LLM 或 embedding model 的 paradigm、靜態網站做 RAG 的關鍵基底">Client-side LLM</a> 把幾 GB 模型權重下載到 browser、引入新的供應鏈面：</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">你的網站
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓ &lt;script&gt; 載入 WebLLM runtime（CDN）
</span></span><span class="line"><span class="ln">3</span><span class="cl">   ↓ runtime 從 HuggingFace CDN 抓 model weights
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓ 使用者 browser 跑模型</span></span></code></pre></div><p>風險：</p>
<ol>
<li><strong>CDN 被 compromise</strong>：WebLLM runtime 或 model weights 在 CDN 上被換、注入 backdoor</li>
<li><strong>HTTPS 之外無額外驗證</strong>：不像本地 <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">GGUF + hash 比對</a>、browser 載模型純信 CDN + HTTPS</li>
<li><strong>使用者本機沒 inventory 記錄</strong>：跟 <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0</a> 推薦的「下載後記 hash」對比、browser 沒這機制</li>
</ol>
<p>緩解：</p>
<ol>
<li><strong>Subresource Integrity（SRI）</strong>：HTML 的 <code>&lt;script integrity=&quot;sha384-...&quot;&gt;</code> 屬性、browser 自動驗證 hash</li>
<li><strong>CSP（Content Security Policy）</strong>：限制可載入的 script / image source、減少 supply chain attack 面</li>
<li><strong>挑大廠 CDN</strong>：Cloudflare / jsdelivr / unpkg 等被 compromise 的歷史紀錄較少</li>
</ol>
<p>跟 <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0</a> 的關係：6.0 講「本機跑的 GGUF 模型供應鏈」、本章補「browser 跑的 client-side 模型供應鏈」— 兩種場景的 framing 一致、但具體威脅面跟工具不同。</p>
<h2 id="跟模組六的-routing">跟模組六的 routing</h2>
<p>本章資安段跟既有 <a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六</a> 的對應：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>06 對應章節</th>
          <th>本章補的角度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模型 / 供應鏈信任</td>
          <td><a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0</a></td>
          <td>client-side 模型分發新形態</td>
      </tr>
      <tr>
          <td>Server 綁定</td>
          <td><a href="/blog/llm/06-security/inference-server-binding/" data-link-title="6.1 推論伺服器的綁定與暴露範圍" data-link-desc="個人 dev 場景下 llama-server / Ollama / LM Studio 的 bind address 判讀：127.0.0.1 vs LAN vs 反代、預設安全、誤開放給內網的後果">6.1</a></td>
          <td>靜態場景無 server、議題消失</td>
      </tr>
      <tr>
          <td>Tool use 權限</td>
          <td><a href="/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">6.2</a></td>
          <td>browser-side tool use（少數場景）</td>
      </tr>
      <tr>
          <td>Prompt injection</td>
          <td><a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3</a></td>
          <td>靜態 RAG 仍適用、source 變 web fetched</td>
      </tr>
      <tr>
          <td>跨雲端 / 本地資料邊界</td>
          <td><a href="/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4</a></td>
          <td>靜態場景 query 走向跟 backend 場景不同</td>
      </tr>
      <tr>
          <td>Production routing</td>
          <td><a href="/blog/llm/06-security/routing-to-production-security/" data-link-title="6.5 跨進 production 的 routing 中樞" data-link-desc="個人 dev → 團隊 → production LLM 服務的三層演化、跟 backend/07 對應卡片的 routing 清單">6.5</a></td>
          <td>從個人靜態 RAG 升級到 production</td>
      </tr>
      <tr>
          <td><strong>API key 暴露 / browser</strong></td>
          <td>（無）</td>
          <td><strong>本章獨有</strong></td>
      </tr>
      <tr>
          <td><strong>CORS / 同源策略</strong></td>
          <td>（無）</td>
          <td><strong>本章獨有</strong></td>
      </tr>
      <tr>
          <td><strong>靜態場景 abuse / rate limit</strong></td>
          <td>（無、跟 6.1 server 議題不同）</td>
          <td><strong>本章獨有</strong></td>
      </tr>
  </tbody>
</table>
<h2 id="判讀流程">判讀流程</h2>





<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">你的場景：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  ├─ 有 backend？
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  │    └─ 是 → 用 4.0 RAG + 4.8 embedding 主章節、本章不適用
</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">  ├─ 規模？
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  │    ├─ &lt; 1K chunks → 方案 1 純前端
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  │    ├─ 1K-10K → 方案 1（embeddings.json ~ 40MB 仍可接受）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  │    ├─ 10K-100K → 方案 2 edge serverless
</span></span><span class="line"><span class="ln">10</span><span class="cl">  │    └─ 100K+ → 不再是靜態場景、回 backend
</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">  ├─ 需要 LLM 對話、不只 retrieval？
</span></span><span class="line"><span class="ln">13</span><span class="cl">  │    ├─ 是 + 隱私第一 → 方案 1 + WebLLM
</span></span><span class="line"><span class="ln">14</span><span class="cl">  │    ├─ 是 + 品質第一 → 方案 1 + user-key 或 方案 2
</span></span><span class="line"><span class="ln">15</span><span class="cl">  │    └─ 否（只要找文章） → 方案 4 純文字 search
</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">  └─ 預算 / vendor lock-in 容忍度？
</span></span><span class="line"><span class="ln">18</span><span class="cl">       ├─ 完全 zero-cost、無 vendor → 方案 1 純前端
</span></span><span class="line"><span class="ln">19</span><span class="cl">       ├─ 接受少量 cost、不想自己寫太多 → 方案 3 SaaS
</span></span><span class="line"><span class="ln">20</span><span class="cl">       └─ 接受少量 cost、想自己控 → 方案 2 edge</span></span></code></pre></div><h2 id="不在本章內的主題">不在本章內的主題</h2>
<ol>
<li><strong>完整 backend RAG</strong>：see <a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG 原理</a> 跟 <a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">4.12 embedding model</a></li>
<li><strong>具體 SaaS API 教學</strong>：Algolia / Pinecone 等 API 細節隨版本變、見各 SaaS 文件</li>
<li><strong>WebGPU 內部細節</strong>：GPU shader、WebGPU API 設計屬 web platform 議題、不在 LLM 教材範圍</li>
<li><strong>Production 多租戶 RAG 服務</strong>：屬 backend/07、本章 framing 是「個人 / 小團隊靜態網站」</li>
<li><strong>企業合規 deployment</strong>：HIPAA / GDPR / SOC 2 跟具體 SaaS / cloud provider 強相關、見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend/07 合規卡片</a> 跟 <a href="/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4 跨雲端</a></li>
</ol>
<h2 id="何時過時--何時不過時">何時過時 / 何時不過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>四方案分類（純前端 / edge / SaaS / 純文字 search）</li>
<li>「靜態場景藏不了 secret」這個根本特性</li>
<li>API key 暴露 / CORS / abuse / 供應鏈 / 模型完整性 五大資安議題分類</li>
<li>跟 <a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六</a> 的 routing 關係</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 SaaS / edge provider（Cloudflare Vectorize / Pinecone / Algolia 等持續演化）</li>
<li>Client-side LLM runtime（WebLLM / wllama / transformers.js）的能力上限</li>
<li>WebGPU 支援度跟 browser 標準</li>
<li>哪些 LLM vendor 允許 browser 直 call（CORS 政策會變）</li>
<li>純文字 search 工具（Pagefind 等持續改進）</li>
</ul>
<h2 id="下一步">下一步</h2>
<p>本章是 <a href="/blog/llm/04-applications/" data-link-title="模組四：LLM 應用層原理" data-link-desc="Prompt 技術光譜、RAG、tool use、agent、應用層協議、人機協作、multi-agent、workflow 編排、eval 設計：跨工具不變的概念地圖">模組四</a> 最後一章。讀完整個模組四、完整覆蓋 LLM 作為系統元件的設計取捨。下一步可進入 <a href="/blog/llm/05-discrete-gpu/" data-link-title="模組五：Windows / Linux &#43; 獨立 GPU" data-link-desc="消費級 PC（Windows / Linux &#43; NVIDIA / AMD 獨立 GPU）跑本地 LLM 的硬體判讀、MoE CPU 卸載、KV cache 量化與 llama.cpp 調參">模組五 PC 獨立 GPU</a> 或 <a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六 安全</a> 補本地 dev 視角的安全議題。</p>
]]></content:encoded></item><item><title>LLM 寫 code 工程實務指南：從心智模型到應用架構</title><link>https://tarrragon.github.io/blog/llm/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/</guid><description>&lt;p>本指南的核心目標是把「LLM 在寫 code 工作流的完整工程地圖」拆成可決策、可實作、可期望管理的工程問題。範圍覆蓋四條讀者旅程：(1) 在自己機器跑本地 LLM 寫 code 的最短可行路徑（Mac 或 PC）、(2) 想懂 LLM 內部運作機制（數學 + 理論基礎）、(3) 想做 LLM 應用開發（RAG / agent / tool use / VLM / benchmarking / 靜態 deployment）、(4) 關心 LLM 工作流的安全議題（本地 dev 視角 + 靜態網站視角）。網路上的 LLM 文章常把推論框架、加速技巧、應用模式、安全議題混為一談；本指南先把這些名詞放回正確的層級、再回答各層的具體取捨。&lt;/p>
&lt;p>本指南預設讀者已經會用過雲端 LLM（ChatGPT、Claude）、熟悉終端機操作、想以工程視角理解 LLM。&lt;strong>寫 code 場景是主要使用例、但模組二 / 三 / 四 / 六多數章節跨場景通用&lt;/strong>：想懂 reasoning model / RAG / embedding model 內部、即使不裝本地 LLM 也能讀。硬體前提分兩條路線：Apple Silicon Mac（M1 ~ M4、統一記憶體）走模組一；Windows / Linux + 獨立 GPU（NVIDIA / AMD、獨立 VRAM + 系統 RAM）走模組五。文章不販賣 LLM 焦慮、也不誇大本地能取代雲端的程度；它的責任是給每條讀者旅程的最短可行路徑、並標出每個階段的取捨。&lt;/p>
&lt;p>模組零（心智模型）是所有讀者旅程的共同前置。模組一跟模組五是「裝本地 LLM」的兩條硬體路線、依平台選一條；想懂底層走模組二跟模組三（跟硬體無關、含 reasoning model / speculative decoding 等推論細節）；想看 LLM 作為系統元件走模組四（12 章涵蓋 RAG、tool use、agent、應用層協議、workflow、production resource、long context、embedding model、benchmarking、vision、靜態 deployment）；本地工作流跑穩想看安全議題走模組六（個人 dev 視角的供應鏈、伺服器綁定、tool use 權限、prompt injection、跨雲端邊界、production routing）。&lt;/p>
&lt;h2 id="教材邊界">教材邊界&lt;/h2>
&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>本地 vs 雲端的差異、為何 LLM 生字慢、三層架構（介面 / 伺服器 / 模型）、&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/openai-compatible-api/" data-link-title="0.3 OpenAI 相容 API" data-link-desc="為什麼幾乎所有本地 LLM 工具不用改就能切到本地：背後是同一套 API 形狀">OpenAI 相容 API&lt;/a>&lt;/td>
 &lt;td>雲端 GPU 租用、AGI 預測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>術語澄清&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MLX&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MTP&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">oMLX&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/speculative-decoding/" data-link-title="Speculative Decoding" data-link-desc="用小模型猜未來 token、大模型並行驗證的加速技巧">speculative decoding&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/moe-cpu-offload/" data-link-title="MoE CPU 卸載" data-link-desc="把 Mixture-of-Experts 模型不活躍的專家層權重放在系統 RAM、用到再走 PCIe 拉回 GPU、讓有限 VRAM 跑得了更大模型">MoE CPU 卸載&lt;/a>&lt;/td>
 &lt;td>post-training fine-tuning 細節&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mac 硬體現實&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">記憶體預算與模型大小&lt;/a>、量化選擇、首字延遲、風扇與功耗&lt;/td>
 &lt;td>雲端 GPU 租用、資料中心訓練&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PC 硬體現實&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/05-discrete-gpu/vram-ram-budget/" data-link-title="5.0 VRAM &amp;#43; RAM 分層預算" data-link-desc="PC 獨立 GPU 場景的記憶體預算判讀：VRAM 是快的世界、RAM 是大的世界、PCIe 把兩個世界連起來">VRAM + RAM 分層預算&lt;/a>、MoE 專家層 CPU 卸載、KV cache 量化、PCIe 頻寬限制&lt;/td>
 &lt;td>多卡 NVLink、資料中心級分散式推論&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本地推論伺服器&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/ollama/" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">Ollama&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/lm-studio/" data-link-title="1.1 LM Studio：GUI 探索模型" data-link-desc="GUI 取向的本地推論伺服器：內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型">LM Studio&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/llama-cpp/" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">llama.cpp&lt;/a>（Mac + PC 通用）&lt;/td>
 &lt;td>vLLM、TGI、Triton 等資料中心級 inference server&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編輯器整合&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &amp;#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&amp;#43;L 對話 / Cmd&amp;#43;I 行內編輯快捷鍵">Continue.dev + VS Code&lt;/a>、Cursor 對應關係&lt;/td>
 &lt;td>JetBrains 全套整合、Vim / Emacs 進階 plugin&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>模型挑選&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">coding 場景的模型優先順序&lt;/a>、量化等級對體感影響&lt;/td>
 &lt;td>benchmark 跑分方法論的完整推導&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>期望管理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/expectation-management/" data-link-title="1.5 期望管理：本地 LLM 的擅長領域與分工" data-link-desc="本地 LLM 是免費的初階 pair programmer：辨識它的擅長領域、跟雲端旗艦做結構性分工">本地 LLM 的擅長領域與分工&lt;/a>、混用雲端的時機&lt;/td>
 &lt;td>LLM 通用能力評估、AGI 預測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>數學基礎&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/linear-algebra-for-llm/" data-link-title="2.0 線性代數：向量、矩陣、空間" data-link-desc="LLM 內部運算的基底：向量、矩陣、向量空間、內積、norm、矩陣乘法的角色">線性代數&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/probability-and-information/" data-link-title="2.1 機率與資訊論" data-link-desc="LLM 輸出的本質是機率分佈：softmax、cross-entropy、KL divergence、perplexity 在訓練與推論中的角色">機率與資訊論&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/calculus-and-optimization/" data-link-title="2.2 微積分與最佳化" data-link-desc="從 gradient、chain rule 到 SGD / Adam：LLM 訓練如何更新數十億參數">最佳化&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/numerical-precision/" data-link-title="2.3 數值精度與量化的數學依據" data-link-desc="fp32 / bf16 / fp16 / int8 / int4 的差別、量化能省哪些 bits、品質衰減從哪裡來">數值精度&lt;/a> 在 LLM 中的角色&lt;/td>
 &lt;td>完整數學證明、測度論等屬於數學系範圍的主題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>理論基礎&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/neural-network-basics/" data-link-title="3.0 神經網路基礎" data-link-desc="從單一 neuron 到 multi-layer：weights、activation function、forward / backward pass 的角色">神經網路&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/embedding-spaces/" data-link-title="3.1 Embedding 空間" data-link-desc="token 怎麼變成向量、為什麼相似 token 在向量空間中靠近、embedding 是怎麼學出來的">embedding&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/attention-mechanism/" data-link-title="3.2 Attention 機制" data-link-desc="Query / Key / Value、scaled dot-product attention、multi-head attention：Transformer 的核心運算">attention&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/transformer-architecture/" data-link-title="3.3 Transformer 架構細節" data-link-desc="Decoder-only 結構、Transformer block、positional encoding、layer norm、residual stream">Transformer&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/training-pipeline/" data-link-title="3.4 訓練流程：pre-train → SFT → RLHF" data-link-desc="LLM 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">訓練流程&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/sampling-and-decoding/" data-link-title="3.5 Sampling 與 Decoding 策略" data-link-desc="Greedy、beam search、top-k、top-p、temperature、min-p：模型輸出後怎麼挑下一個 token">sampling&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/tokenization-algorithms/" data-link-title="3.6 Tokenization：BPE、SentencePiece、Tiktoken" data-link-desc="把文字切成 token 的算法：為什麼不同模型切出不同 token 數、tokenizer 選擇對能力的影響">tokenization&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/cross-language-tokenization/" data-link-title="3.7 跨語言場景的 tokenizer 與訓練分佈原理" data-link-desc="為什麼模型對不同語言表現不一致：tokenizer &amp;#43; 訓練資料分佈雙因素、語言選擇取捨">跨語言原理&lt;/a>&lt;/td>
 &lt;td>多模態擴展、最新研究細節交給 Stanford CS25&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>應用層原理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &amp;#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">RAG&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">Tool use&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">Agent 架構&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">應用層協議&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">Workflow 編排&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">Production resource&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/artifact-management/" data-link-title="4.10 衍生產物管理原理：什麼進 git、什麼不該" data-link-desc="LLM 應用的 source / derived / external 三類產物對應 git / build cache / registry、與 production 部署的 reproducibility / cost / share 取捨">Artifact 管理&lt;/a>&lt;/td>
 &lt;td>具體 framework 教學（LangChain / LlamaIndex）、prompt engineering&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>進階理論&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/reasoning-models/" data-link-title="3.8 Reasoning models：test-time compute paradigm" data-link-desc="Chain-of-thought 從 prompting 技巧演化成訓練 paradigm、reasoning model 的內部運作、本地可跑的選項與適用任務">Reasoning models&lt;/a>（o1 / R1 / QwQ 風格）、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/speculative-decoding-internals/" data-link-title="3.9 Speculative decoding 內部：drafter / 驗證 / 加速上限" data-link-desc="speculative decoding 的演算法細節、drafter 跟 target 怎麼配對、acceptance rate 怎麼決定實際加速、MTP 跟 EAGLE 等變體">Speculative decoding 內部&lt;/a>（drafter / MTP / EAGLE）&lt;/td>
 &lt;td>完整 paper 推導、最新研究 frontier&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>進階應用&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/long-context-engineering/" data-link-title="4.11 Long context engineering" data-link-desc="128K / 1M context 模型怎麼用：claimed vs effective context、lost-in-the-middle、context 設計策略、Long context vs RAG 取捨">Long context engineering&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &amp;#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">Embedding model 內部&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">Benchmarking&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/vision-in-coding-workflow/" data-link-title="4.15 Vision in coding workflow：本地 VLM 怎麼接寫 code" data-link-desc="VLM 在 coding 工作流的 use cases、本地 VLM 選型、跟雲端 VLM 的分工、Continue.dev / Ollama 整合現狀">Vision in coding&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">靜態 / serverless RAG deployment&lt;/a>&lt;/td>
 &lt;td>完整 LangChain / LlamaIndex 教學&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Fine-tuning&lt;/td>
 &lt;td>原理（&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/lora/" data-link-title="LoRA" data-link-desc="Low-Rank Adaptation：凍住原模型權重、只訓兩個小矩陣的 parameter-efficient fine-tuning">LoRA&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/qlora/" data-link-title="QLoRA" data-link-desc="把 base model 量化到 4-bit &amp;#43; LoRA fine-tune 的組合、消費級 GPU 也能 fine-tune 大模型">QLoRA&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/catastrophic-forgetting/" data-link-title="Catastrophic Forgetting" data-link-desc="Fine-tune 模型時、新訓練資料覆蓋掉原本學到的能力的現象、LoRA / 資料 mixing 是主要緩解">catastrophic forgetting&lt;/a>）+ &lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/local-fine-tuning/" data-link-title="Hands-on：用 QLoRA 在本機 fine-tune coding 模型" data-link-desc="Apple Silicon Mac / PC 獨立 GPU 上跑 QLoRA fine-tune 的完整流程：環境、資料、訓練、evaluation、合併、部署到 Ollama">本機 hands-on&lt;/a>&lt;/td>
 &lt;td>完整資料工程、large-scale distributed fine-tune&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>隱私 / 安全&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">隱私資料流&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">本地 dev 安全模組&lt;/a>（供應鏈 / 伺服器綁定 / tool use / prompt injection / 跨雲端邊界 / production routing）、&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">靜態網站 RAG 資安&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">排錯方法論&lt;/a>&lt;/td>
 &lt;td>企業合規逐條檢核、SOC 2 / HIPAA 流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>進一步學習&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/going-deeper-math/" data-link-title="2.4 想學更深：推薦公開課程" data-link-desc="MIT、Stanford、Harvard 等公開課程：數學基礎跟 LLM 預備知識的完整學習路線">數學公開課推薦&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">LLM 理論公開課推薦&lt;/a>&lt;/td>
 &lt;td>（交給推薦的課程跟書籍）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;p>本指南分成七個模組加一組前置卡片（111 張）。讀者依目的選讀、不需要從頭到尾全讀：&lt;/p></description><content:encoded><![CDATA[<p>本指南的核心目標是把「LLM 在寫 code 工作流的完整工程地圖」拆成可決策、可實作、可期望管理的工程問題。範圍覆蓋四條讀者旅程：(1) 在自己機器跑本地 LLM 寫 code 的最短可行路徑（Mac 或 PC）、(2) 想懂 LLM 內部運作機制（數學 + 理論基礎）、(3) 想做 LLM 應用開發（RAG / agent / tool use / VLM / benchmarking / 靜態 deployment）、(4) 關心 LLM 工作流的安全議題（本地 dev 視角 + 靜態網站視角）。網路上的 LLM 文章常把推論框架、加速技巧、應用模式、安全議題混為一談；本指南先把這些名詞放回正確的層級、再回答各層的具體取捨。</p>
<p>本指南預設讀者已經會用過雲端 LLM（ChatGPT、Claude）、熟悉終端機操作、想以工程視角理解 LLM。<strong>寫 code 場景是主要使用例、但模組二 / 三 / 四 / 六多數章節跨場景通用</strong>：想懂 reasoning model / RAG / embedding model 內部、即使不裝本地 LLM 也能讀。硬體前提分兩條路線：Apple Silicon Mac（M1 ~ M4、統一記憶體）走模組一；Windows / Linux + 獨立 GPU（NVIDIA / AMD、獨立 VRAM + 系統 RAM）走模組五。文章不販賣 LLM 焦慮、也不誇大本地能取代雲端的程度；它的責任是給每條讀者旅程的最短可行路徑、並標出每個階段的取捨。</p>
<p>模組零（心智模型）是所有讀者旅程的共同前置。模組一跟模組五是「裝本地 LLM」的兩條硬體路線、依平台選一條；想懂底層走模組二跟模組三（跟硬體無關、含 reasoning model / speculative decoding 等推論細節）；想看 LLM 作為系統元件走模組四（12 章涵蓋 RAG、tool use、agent、應用層協議、workflow、production resource、long context、embedding model、benchmarking、vision、靜態 deployment）；本地工作流跑穩想看安全議題走模組六（個人 dev 視角的供應鏈、伺服器綁定、tool use 權限、prompt injection、跨雲端邊界、production routing）。</p>
<h2 id="教材邊界">教材邊界</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>放在本指南</th>
          <th>不放在本指南</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>心智模型</td>
          <td>本地 vs 雲端的差異、為何 LLM 生字慢、三層架構（介面 / 伺服器 / 模型）、<a href="/blog/llm/00-foundations/openai-compatible-api/" data-link-title="0.3 OpenAI 相容 API" data-link-desc="為什麼幾乎所有本地 LLM 工具不用改就能切到本地：背後是同一套 API 形狀">OpenAI 相容 API</a></td>
          <td>雲端 GPU 租用、AGI 預測</td>
      </tr>
      <tr>
          <td>術語澄清</td>
          <td><a href="/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MLX</a>、<a href="/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MTP</a>、<a href="/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">oMLX</a>、<a href="/blog/llm/knowledge-cards/speculative-decoding/" data-link-title="Speculative Decoding" data-link-desc="用小模型猜未來 token、大模型並行驗證的加速技巧">speculative decoding</a>、<a href="/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化</a>、<a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a>、<a href="/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT</a>、<a href="/blog/llm/knowledge-cards/moe-cpu-offload/" data-link-title="MoE CPU 卸載" data-link-desc="把 Mixture-of-Experts 模型不活躍的專家層權重放在系統 RAM、用到再走 PCIe 拉回 GPU、讓有限 VRAM 跑得了更大模型">MoE CPU 卸載</a></td>
          <td>post-training fine-tuning 細節</td>
      </tr>
      <tr>
          <td>Mac 硬體現實</td>
          <td><a href="/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">記憶體預算與模型大小</a>、量化選擇、首字延遲、風扇與功耗</td>
          <td>雲端 GPU 租用、資料中心訓練</td>
      </tr>
      <tr>
          <td>PC 硬體現實</td>
          <td><a href="/blog/llm/05-discrete-gpu/vram-ram-budget/" data-link-title="5.0 VRAM &#43; RAM 分層預算" data-link-desc="PC 獨立 GPU 場景的記憶體預算判讀：VRAM 是快的世界、RAM 是大的世界、PCIe 把兩個世界連起來">VRAM + RAM 分層預算</a>、MoE 專家層 CPU 卸載、KV cache 量化、PCIe 頻寬限制</td>
          <td>多卡 NVLink、資料中心級分散式推論</td>
      </tr>
      <tr>
          <td>本地推論伺服器</td>
          <td><a href="/blog/llm/01-local-llm-services/ollama/" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">Ollama</a>、<a href="/blog/llm/01-local-llm-services/lm-studio/" data-link-title="1.1 LM Studio：GUI 探索模型" data-link-desc="GUI 取向的本地推論伺服器：內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型">LM Studio</a>、<a href="/blog/llm/01-local-llm-services/llama-cpp/" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">llama.cpp</a>（Mac + PC 通用）</td>
          <td>vLLM、TGI、Triton 等資料中心級 inference server</td>
      </tr>
      <tr>
          <td>編輯器整合</td>
          <td><a href="/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&#43;L 對話 / Cmd&#43;I 行內編輯快捷鍵">Continue.dev + VS Code</a>、Cursor 對應關係</td>
          <td>JetBrains 全套整合、Vim / Emacs 進階 plugin</td>
      </tr>
      <tr>
          <td>模型挑選</td>
          <td><a href="/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">coding 場景的模型優先順序</a>、量化等級對體感影響</td>
          <td>benchmark 跑分方法論的完整推導</td>
      </tr>
      <tr>
          <td>期望管理</td>
          <td><a href="/blog/llm/01-local-llm-services/expectation-management/" data-link-title="1.5 期望管理：本地 LLM 的擅長領域與分工" data-link-desc="本地 LLM 是免費的初階 pair programmer：辨識它的擅長領域、跟雲端旗艦做結構性分工">本地 LLM 的擅長領域與分工</a>、混用雲端的時機</td>
          <td>LLM 通用能力評估、AGI 預測</td>
      </tr>
      <tr>
          <td>數學基礎</td>
          <td><a href="/blog/llm/02-math-foundations/linear-algebra-for-llm/" data-link-title="2.0 線性代數：向量、矩陣、空間" data-link-desc="LLM 內部運算的基底：向量、矩陣、向量空間、內積、norm、矩陣乘法的角色">線性代數</a>、<a href="/blog/llm/02-math-foundations/probability-and-information/" data-link-title="2.1 機率與資訊論" data-link-desc="LLM 輸出的本質是機率分佈：softmax、cross-entropy、KL divergence、perplexity 在訓練與推論中的角色">機率與資訊論</a>、<a href="/blog/llm/02-math-foundations/calculus-and-optimization/" data-link-title="2.2 微積分與最佳化" data-link-desc="從 gradient、chain rule 到 SGD / Adam：LLM 訓練如何更新數十億參數">最佳化</a>、<a href="/blog/llm/02-math-foundations/numerical-precision/" data-link-title="2.3 數值精度與量化的數學依據" data-link-desc="fp32 / bf16 / fp16 / int8 / int4 的差別、量化能省哪些 bits、品質衰減從哪裡來">數值精度</a> 在 LLM 中的角色</td>
          <td>完整數學證明、測度論等屬於數學系範圍的主題</td>
      </tr>
      <tr>
          <td>理論基礎</td>
          <td><a href="/blog/llm/03-theoretical-foundations/neural-network-basics/" data-link-title="3.0 神經網路基礎" data-link-desc="從單一 neuron 到 multi-layer：weights、activation function、forward / backward pass 的角色">神經網路</a>、<a href="/blog/llm/03-theoretical-foundations/embedding-spaces/" data-link-title="3.1 Embedding 空間" data-link-desc="token 怎麼變成向量、為什麼相似 token 在向量空間中靠近、embedding 是怎麼學出來的">embedding</a>、<a href="/blog/llm/03-theoretical-foundations/attention-mechanism/" data-link-title="3.2 Attention 機制" data-link-desc="Query / Key / Value、scaled dot-product attention、multi-head attention：Transformer 的核心運算">attention</a>、<a href="/blog/llm/03-theoretical-foundations/transformer-architecture/" data-link-title="3.3 Transformer 架構細節" data-link-desc="Decoder-only 結構、Transformer block、positional encoding、layer norm、residual stream">Transformer</a>、<a href="/blog/llm/03-theoretical-foundations/training-pipeline/" data-link-title="3.4 訓練流程：pre-train → SFT → RLHF" data-link-desc="LLM 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">訓練流程</a>、<a href="/blog/llm/03-theoretical-foundations/sampling-and-decoding/" data-link-title="3.5 Sampling 與 Decoding 策略" data-link-desc="Greedy、beam search、top-k、top-p、temperature、min-p：模型輸出後怎麼挑下一個 token">sampling</a>、<a href="/blog/llm/03-theoretical-foundations/tokenization-algorithms/" data-link-title="3.6 Tokenization：BPE、SentencePiece、Tiktoken" data-link-desc="把文字切成 token 的算法：為什麼不同模型切出不同 token 數、tokenizer 選擇對能力的影響">tokenization</a>、<a href="/blog/llm/03-theoretical-foundations/cross-language-tokenization/" data-link-title="3.7 跨語言場景的 tokenizer 與訓練分佈原理" data-link-desc="為什麼模型對不同語言表現不一致：tokenizer &#43; 訓練資料分佈雙因素、語言選擇取捨">跨語言原理</a></td>
          <td>多模態擴展、最新研究細節交給 Stanford CS25</td>
      </tr>
      <tr>
          <td>應用層原理</td>
          <td><a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">RAG</a>、<a href="/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">Tool use</a>、<a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">Agent 架構</a>、<a href="/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">應用層協議</a>、<a href="/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">Workflow 編排</a>、<a href="/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">Production resource</a>、<a href="/blog/llm/04-applications/artifact-management/" data-link-title="4.10 衍生產物管理原理：什麼進 git、什麼不該" data-link-desc="LLM 應用的 source / derived / external 三類產物對應 git / build cache / registry、與 production 部署的 reproducibility / cost / share 取捨">Artifact 管理</a></td>
          <td>具體 framework 教學（LangChain / LlamaIndex）、prompt engineering</td>
      </tr>
      <tr>
          <td>進階理論</td>
          <td><a href="/blog/llm/03-theoretical-foundations/reasoning-models/" data-link-title="3.8 Reasoning models：test-time compute paradigm" data-link-desc="Chain-of-thought 從 prompting 技巧演化成訓練 paradigm、reasoning model 的內部運作、本地可跑的選項與適用任務">Reasoning models</a>（o1 / R1 / QwQ 風格）、<a href="/blog/llm/03-theoretical-foundations/speculative-decoding-internals/" data-link-title="3.9 Speculative decoding 內部：drafter / 驗證 / 加速上限" data-link-desc="speculative decoding 的演算法細節、drafter 跟 target 怎麼配對、acceptance rate 怎麼決定實際加速、MTP 跟 EAGLE 等變體">Speculative decoding 內部</a>（drafter / MTP / EAGLE）</td>
          <td>完整 paper 推導、最新研究 frontier</td>
      </tr>
      <tr>
          <td>進階應用</td>
          <td><a href="/blog/llm/04-applications/long-context-engineering/" data-link-title="4.11 Long context engineering" data-link-desc="128K / 1M context 模型怎麼用：claimed vs effective context、lost-in-the-middle、context 設計策略、Long context vs RAG 取捨">Long context engineering</a>、<a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">Embedding model 內部</a>、<a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">Benchmarking</a>、<a href="/blog/llm/04-applications/vision-in-coding-workflow/" data-link-title="4.15 Vision in coding workflow：本地 VLM 怎麼接寫 code" data-link-desc="VLM 在 coding 工作流的 use cases、本地 VLM 選型、跟雲端 VLM 的分工、Continue.dev / Ollama 整合現狀">Vision in coding</a>、<a href="/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">靜態 / serverless RAG deployment</a></td>
          <td>完整 LangChain / LlamaIndex 教學</td>
      </tr>
      <tr>
          <td>Fine-tuning</td>
          <td>原理（<a href="/blog/llm/knowledge-cards/lora/" data-link-title="LoRA" data-link-desc="Low-Rank Adaptation：凍住原模型權重、只訓兩個小矩陣的 parameter-efficient fine-tuning">LoRA</a> / <a href="/blog/llm/knowledge-cards/qlora/" data-link-title="QLoRA" data-link-desc="把 base model 量化到 4-bit &#43; LoRA fine-tune 的組合、消費級 GPU 也能 fine-tune 大模型">QLoRA</a> / <a href="/blog/llm/knowledge-cards/catastrophic-forgetting/" data-link-title="Catastrophic Forgetting" data-link-desc="Fine-tune 模型時、新訓練資料覆蓋掉原本學到的能力的現象、LoRA / 資料 mixing 是主要緩解">catastrophic forgetting</a>）+ <a href="/blog/llm/01-local-llm-services/hands-on/local-fine-tuning/" data-link-title="Hands-on：用 QLoRA 在本機 fine-tune coding 模型" data-link-desc="Apple Silicon Mac / PC 獨立 GPU 上跑 QLoRA fine-tune 的完整流程：環境、資料、訓練、evaluation、合併、部署到 Ollama">本機 hands-on</a></td>
          <td>完整資料工程、large-scale distributed fine-tune</td>
      </tr>
      <tr>
          <td>隱私 / 安全</td>
          <td><a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">隱私資料流</a>、<a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">本地 dev 安全模組</a>（供應鏈 / 伺服器綁定 / tool use / prompt injection / 跨雲端邊界 / production routing）、<a href="/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">靜態網站 RAG 資安</a>、<a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">排錯方法論</a></td>
          <td>企業合規逐條檢核、SOC 2 / HIPAA 流程</td>
      </tr>
      <tr>
          <td>進一步學習</td>
          <td><a href="/blog/llm/02-math-foundations/going-deeper-math/" data-link-title="2.4 想學更深：推薦公開課程" data-link-desc="MIT、Stanford、Harvard 等公開課程：數學基礎跟 LLM 預備知識的完整學習路線">數學公開課推薦</a>、<a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">LLM 理論公開課推薦</a></td>
          <td>（交給推薦的課程跟書籍）</td>
      </tr>
  </tbody>
</table>
<h2 id="學習路線">學習路線</h2>
<p>本指南分成七個模組加一組前置卡片（111 張）。讀者依目的選讀、不需要從頭到尾全讀：</p>
<ul>
<li><strong>想用 Apple Silicon Mac 裝本地 LLM 寫 code</strong>：讀模組零 + 模組一（最短路徑）</li>
<li><strong>想用 Windows / Linux + 獨立 GPU 裝</strong>：讀模組零 + 模組五</li>
<li><strong>想懂 LLM 內部原理</strong>：模組二（數學） + 模組三（理論、含 reasoning models / speculative decoding）— 跟硬體無關</li>
<li><strong>想做 LLM 應用開發（含 RAG / agent / VLM / 靜態 deployment）</strong>：模組四（12 章、跨工具世代不變的原理）— 跟硬體無關</li>
<li><strong>想懂本地工作流的安全議題</strong>：模組一 / 五跑穩後接模組六（個人 dev 視角）</li>
<li><strong>想選 RAG 的 storage 方案（pickle / vector DB / hosted SaaS）</strong>：直接看 <a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 RAG storage 工程</a></li>
<li><strong>想在靜態網站加 RAG / 智能搜尋</strong>：直接看 <a href="/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">4.16 靜態 / serverless RAG deployment</a></li>
<li><strong>想在本機 fine-tune 模型</strong>：模組三 3.4 訓練流程原理 → <a href="/blog/llm/01-local-llm-services/hands-on/local-fine-tuning/" data-link-title="Hands-on：用 QLoRA 在本機 fine-tune coding 模型" data-link-desc="Apple Silicon Mac / PC 獨立 GPU 上跑 QLoRA fine-tune 的完整流程：環境、資料、訓練、evaluation、合併、部署到 Ollama">本機 QLoRA hands-on</a></li>
<li><strong>想跟最新進展接軌</strong>：讀完模組後進推薦的公開課程跟 paper（模組二 2.4 + 模組三 3.10）</li>
</ul>
<h3 id="前置知識卡片"><a href="/blog/llm/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理本地 LLM 寫 code 場景所需的概念詞彙">前置知識卡片</a></h3>
<p>用原子化卡片整理 <a href="/blog/llm/knowledge-cards/token/" data-link-title="Token" data-link-desc="LLM 處理文字時的最小單位：介於字元與單字之間">token</a>、<a href="/blog/llm/knowledge-cards/autoregressive/" data-link-title="Autoregressive" data-link-desc="LLM 一次生成一個 token、把已生成內容作為下一次輸入的架構">自回歸</a>、<a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a>、<a href="/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化</a>、<a href="/blog/llm/knowledge-cards/speculative-decoding/" data-link-title="Speculative Decoding" data-link-desc="用小模型猜未來 token、大模型並行驗證的加速技巧">speculative decoding</a>、<a href="/blog/llm/knowledge-cards/mtp/" data-link-title="Multi-Token Prediction (MTP)" data-link-desc="Google 為 Gemma 系列釋出的 speculative decoding 工程化實作">MTP</a>、<a href="/blog/llm/knowledge-cards/mlx/" data-link-title="MLX" data-link-desc="Apple 釋出的 Apple Silicon 數值運算 framework：類似 PyTorch / JAX 的 Mac 對應物">MLX</a>、<a href="/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器</a>、<a href="/blog/llm/knowledge-cards/openai-compatible-api/" data-link-title="OpenAI 相容 API" data-link-desc="本地推論伺服器跟雲端 OpenAI 共用的 API 形狀標準">OpenAI 相容 API</a>、<a href="/blog/llm/knowledge-cards/memory-bandwidth/" data-link-title="Memory Bandwidth" data-link-desc="記憶體每秒能讀寫多少 bytes：決定本地 LLM 生字速度的真正瓶頸">memory bandwidth</a>、<a href="/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體</a>、<a href="/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT</a>、<a href="/blog/llm/knowledge-cards/prefill/" data-link-title="Prefill" data-link-desc="Prompt 首次處理時的計算階段：把整段輸入跑過模型、產生 KV cache">prefill</a>、<a href="/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">context window</a>、<a href="/blog/llm/knowledge-cards/transformer/" data-link-title="Transformer" data-link-desc="寫 code 用的 LLM 神經網路架構：基於 attention 機制、自回歸生成 token">Transformer</a>、<a href="/blog/llm/knowledge-cards/diffusion/" data-link-title="Diffusion" data-link-desc="產圖用的生成式 AI 架構：跟寫 code 用的 Transformer 是不同路線">Diffusion</a> 等核心概念。章節文章專注情境推導、術語背景交由卡片維持一致。</p>
<h3 id="模組零基礎知識與心智模型"><a href="/blog/llm/00-foundations/" data-link-title="模組零：基礎知識與心智模型" data-link-desc="建立本地 LLM 的心智模型、釐清 MLX / MTP / oMLX 等常被混淆的術語、Apple Silicon 記憶體現實">模組零：基礎知識與心智模型</a></h3>
<p>整理本地 vs 雲端 LLM 的差異、自回歸架構與記憶體頻寬瓶頸、介面 / 伺服器 / 模型三層心智模型、OpenAI 相容 API 為何重要、MLX / MTP / oMLX 三個容易搞混的術語、Apple Silicon Mac 記憶體與模型大小的對應關係、判讀本地 LLM 資訊的五個框架。</p>
<h3 id="模組一本地-llm-服務的安裝與應用"><a href="/blog/llm/01-local-llm-services/" data-link-title="模組一：本地 LLM 服務的安裝與應用" data-link-desc="Ollama、LM Studio、llama.cpp 的安裝與差異、VS Code &#43; Continue.dev 整合、模型選型與期望管理">模組一：本地 LLM 服務的安裝與應用</a></h3>
<p>整理 Ollama、LM Studio、llama.cpp 三個主流推論伺服器的現況差異與安裝路徑、用 Continue.dev 把本地 LLM 接到 VS Code 的完整步驟、寫 code 場景下模型選型的優先順序、本地模型的期望管理、想進一步玩 coding agent、Web UI、產圖時的延伸方向。</p>
<h3 id="模組二llm-的數學基礎"><a href="/blog/llm/02-math-foundations/" data-link-title="模組二：LLM 的數學基礎" data-link-desc="整理 LLM 推論背後需要理解的線性代數、機率與資訊論、最佳化、數值精度等數學概念">模組二：LLM 的數學基礎</a></h3>
<p>整理 LLM 推論背後的數學工具：<a href="/blog/llm/02-math-foundations/linear-algebra-for-llm/" data-link-title="2.0 線性代數：向量、矩陣、空間" data-link-desc="LLM 內部運算的基底：向量、矩陣、向量空間、內積、norm、矩陣乘法的角色">線性代數</a>（向量、矩陣、空間）、<a href="/blog/llm/02-math-foundations/probability-and-information/" data-link-title="2.1 機率與資訊論" data-link-desc="LLM 輸出的本質是機率分佈：softmax、cross-entropy、KL divergence、perplexity 在訓練與推論中的角色">機率與資訊論</a>（softmax、cross-entropy、KL、perplexity）、<a href="/blog/llm/02-math-foundations/calculus-and-optimization/" data-link-title="2.2 微積分與最佳化" data-link-desc="從 gradient、chain rule 到 SGD / Adam：LLM 訓練如何更新數十億參數">微積分與最佳化</a>（gradient、SGD / Adam）、<a href="/blog/llm/02-math-foundations/numerical-precision/" data-link-title="2.3 數值精度與量化的數學依據" data-link-desc="fp32 / bf16 / fp16 / int8 / int4 的差別、量化能省哪些 bits、品質衰減從哪裡來">數值精度</a>（fp32 / bf16 / Q4 / Q8 的取捨）。每章末尾接到<a href="/blog/llm/02-math-foundations/going-deeper-math/" data-link-title="2.4 想學更深：推薦公開課程" data-link-desc="MIT、Stanford、Harvard 等公開課程：數學基礎跟 LLM 預備知識的完整學習路線">公開課推薦</a>。</p>
<h3 id="模組三llm-的理論基礎"><a href="/blog/llm/03-theoretical-foundations/" data-link-title="模組三：LLM 的理論基礎" data-link-desc="從神經網路、embedding、attention、Transformer 架構、訓練到 sampling：LLM 內部運作的完整理論圖像">模組三：LLM 的理論基礎</a></h3>
<p>整理 LLM 內部運作機制、共 11 章：<a href="/blog/llm/03-theoretical-foundations/neural-network-basics/" data-link-title="3.0 神經網路基礎" data-link-desc="從單一 neuron 到 multi-layer：weights、activation function、forward / backward pass 的角色">神經網路基礎</a>、<a href="/blog/llm/03-theoretical-foundations/embedding-spaces/" data-link-title="3.1 Embedding 空間" data-link-desc="token 怎麼變成向量、為什麼相似 token 在向量空間中靠近、embedding 是怎麼學出來的">embedding 空間</a>、<a href="/blog/llm/03-theoretical-foundations/attention-mechanism/" data-link-title="3.2 Attention 機制" data-link-desc="Query / Key / Value、scaled dot-product attention、multi-head attention：Transformer 的核心運算">attention 機制</a>、<a href="/blog/llm/03-theoretical-foundations/transformer-architecture/" data-link-title="3.3 Transformer 架構細節" data-link-desc="Decoder-only 結構、Transformer block、positional encoding、layer norm、residual stream">Transformer 架構</a>、<a href="/blog/llm/03-theoretical-foundations/training-pipeline/" data-link-title="3.4 訓練流程：pre-train → SFT → RLHF" data-link-desc="LLM 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">訓練流程</a>（pre-train → SFT → RLHF / DPO）、<a href="/blog/llm/03-theoretical-foundations/sampling-and-decoding/" data-link-title="3.5 Sampling 與 Decoding 策略" data-link-desc="Greedy、beam search、top-k、top-p、temperature、min-p：模型輸出後怎麼挑下一個 token">sampling 策略</a>、<a href="/blog/llm/03-theoretical-foundations/tokenization-algorithms/" data-link-title="3.6 Tokenization：BPE、SentencePiece、Tiktoken" data-link-desc="把文字切成 token 的算法：為什麼不同模型切出不同 token 數、tokenizer 選擇對能力的影響">tokenization 算法</a>、<a href="/blog/llm/03-theoretical-foundations/cross-language-tokenization/" data-link-title="3.7 跨語言場景的 tokenizer 與訓練分佈原理" data-link-desc="為什麼模型對不同語言表現不一致：tokenizer &#43; 訓練資料分佈雙因素、語言選擇取捨">跨語言場景原理</a>、<a href="/blog/llm/03-theoretical-foundations/reasoning-models/" data-link-title="3.8 Reasoning models：test-time compute paradigm" data-link-desc="Chain-of-thought 從 prompting 技巧演化成訓練 paradigm、reasoning model 的內部運作、本地可跑的選項與適用任務">Reasoning models</a>（o1 / R1 / QwQ 等 test-time compute paradigm）、<a href="/blog/llm/03-theoretical-foundations/speculative-decoding-internals/" data-link-title="3.9 Speculative decoding 內部：drafter / 驗證 / 加速上限" data-link-desc="speculative decoding 的演算法細節、drafter 跟 target 怎麼配對、acceptance rate 怎麼決定實際加速、MTP 跟 EAGLE 等變體">Speculative decoding 內部</a>（drafter / MTP / EAGLE）。每章末尾接到<a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">公開課推薦</a>（Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI）。</p>
<h3 id="模組四llm-應用層原理"><a href="/blog/llm/04-applications/" data-link-title="模組四：LLM 應用層原理" data-link-desc="Prompt 技術光譜、RAG、tool use、agent、應用層協議、人機協作、multi-agent、workflow 編排、eval 設計：跨工具不變的概念地圖">模組四：LLM 應用層原理</a></h3>
<p>整理 LLM 作為系統元件的設計原理、共 12 章：<a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">RAG</a>、<a href="/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">tool use</a>、<a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">agent 架構</a>、<a href="/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">應用層協議</a>、<a href="/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">workflow 編排模式</a>、<a href="/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">Production resource planning</a>、<a href="/blog/llm/04-applications/artifact-management/" data-link-title="4.10 衍生產物管理原理：什麼進 git、什麼不該" data-link-desc="LLM 應用的 source / derived / external 三類產物對應 git / build cache / registry、與 production 部署的 reproducibility / cost / share 取捨">衍生產物管理</a>、<a href="/blog/llm/04-applications/long-context-engineering/" data-link-title="4.11 Long context engineering" data-link-desc="128K / 1M context 模型怎麼用：claimed vs effective context、lost-in-the-middle、context 設計策略、Long context vs RAG 取捨">Long context engineering</a>、<a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">Embedding model 內部</a>、<a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">Benchmarking 方法論</a>、<a href="/blog/llm/04-applications/vision-in-coding-workflow/" data-link-title="4.15 Vision in coding workflow：本地 VLM 怎麼接寫 code" data-link-desc="VLM 在 coding 工作流的 use cases、本地 VLM 選型、跟雲端 VLM 的分工、Continue.dev / Ollama 整合現狀">Vision in coding workflow</a>（本地 VLM 接 IDE）、<a href="/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">靜態 / serverless RAG deployment</a>（沒 backend 場景）。本模組刻意只寫跨工具世代不變的原理、避開 LangChain / LlamaIndex 等具體 framework 教學。</p>
<h3 id="模組五windows--linux--獨立-gpu"><a href="/blog/llm/05-discrete-gpu/" data-link-title="模組五：Windows / Linux &#43; 獨立 GPU" data-link-desc="消費級 PC（Windows / Linux &#43; NVIDIA / AMD 獨立 GPU）跑本地 LLM 的硬體判讀、MoE CPU 卸載、KV cache 量化與 llama.cpp 調參">模組五：Windows / Linux + 獨立 GPU</a></h3>
<p>整理消費級 PC（Windows / Linux + NVIDIA / AMD 獨立 GPU）跑本地 LLM 的硬體判讀模型與工程選項：<a href="/blog/llm/05-discrete-gpu/vram-ram-budget/" data-link-title="5.0 VRAM &#43; RAM 分層預算" data-link-desc="PC 獨立 GPU 場景的記憶體預算判讀：VRAM 是快的世界、RAM 是大的世界、PCIe 把兩個世界連起來">VRAM + RAM 分層預算</a>、MoE 模型的 <a href="/blog/llm/knowledge-cards/moe-cpu-offload/" data-link-title="MoE CPU 卸載" data-link-desc="把 Mixture-of-Experts 模型不活躍的專家層權重放在系統 RAM、用到再走 PCIe 拉回 GPU、讓有限 VRAM 跑得了更大模型">CPU 卸載策略</a>（<code>--n-cpu-moe</code>）、KV cache 量化（K=Q8 / V=Q4）跟 context 長度的權衡、llama.cpp 在 PC 上的調參空間。本模組跟模組一是平行的硬體路線、共用模組零的心智模型跟卡片。</p>
<h3 id="模組六本地-llm-的安全與權限"><a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六：本地 LLM 的安全與權限</a></h3>
<p>整理個人 dev 在自己機器上跑本地 LLM 的安全議題：<a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">模型供應鏈與信任邊界</a>、<a href="/blog/llm/06-security/inference-server-binding/" data-link-title="6.1 推論伺服器的綁定與暴露範圍" data-link-desc="個人 dev 場景下 llama-server / Ollama / LM Studio 的 bind address 判讀：127.0.0.1 vs LAN vs 反代、預設安全、誤開放給內網的後果">推論伺服器的綁定與暴露範圍</a>、<a href="/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">tool use 與 MCP server 的權限模型</a>、<a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">IDE 場景的 prompt injection</a>、<a href="/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">跨雲端 / 本地的資料邊界</a>、<a href="/blog/llm/06-security/routing-to-production-security/" data-link-title="6.5 跨進 production 的 routing 中樞" data-link-desc="個人 dev → 團隊 → production LLM 服務的三層演化、跟 backend/07 對應卡片的 routing 清單">跨進 production 的 routing 中樞</a>。framing 是個人 dev 視角、不是 enterprise 資安管理；production / 多租戶 LLM 服務的特殊資安議題見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">Backend 模組七 資安與資料保護</a> 的 LLM 相關章節。</p>
<h2 id="模組之間怎麼配合">模組之間怎麼配合</h2>
<table>
  <thead>
      <tr>
          <th>模組</th>
          <th>角度</th>
          <th>跟其他模組的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模組零</td>
          <td>操作層心智模型</td>
          <td>是模組一跟模組五的共同前置</td>
      </tr>
      <tr>
          <td>模組一</td>
          <td>工具層、Mac 實際安裝</td>
          <td>用模組零的詞彙、跟模組三的理論互補</td>
      </tr>
      <tr>
          <td>模組二</td>
          <td>數學工具</td>
          <td>提供模組三需要的數學詞彙、跟硬體平台無關</td>
      </tr>
      <tr>
          <td>模組三</td>
          <td>理論機制</td>
          <td>用模組二的工具拼出完整 LLM、跟硬體平台無關</td>
      </tr>
      <tr>
          <td>模組四</td>
          <td>應用層原理</td>
          <td>用前面模組建的詞彙、看 LLM 作為系統元件</td>
      </tr>
      <tr>
          <td>模組五</td>
          <td>工具層、PC 獨立 GPU</td>
          <td>跟模組一平行、用模組零的詞彙、處理 VRAM 場景</td>
      </tr>
      <tr>
          <td>模組六</td>
          <td>安全層、個人 dev 視角</td>
          <td>在模組一 / 五的工作流上加安全判讀、cross-link backend/07 通用資安卡片</td>
      </tr>
  </tbody>
</table>
<p>模組二跟模組三可並讀。閱讀模組三遇到陌生數學詞時跳回模組二補完、再回模組三繼續。模組四在前面模組之上、但讀者熟悉 LLM 應用詞彙也可直接從這裡讀起。模組一跟模組五依硬體選一條主路線、共用模組零的心智模型與 <a href="/blog/llm/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理本地 LLM 寫 code 場景所需的概念詞彙">knowledge-cards</a>。模組六在模組一 / 五跑穩後接、處理「跑起來後該注意什麼」。</p>
<h2 id="適合的讀者">適合的讀者</h2>
<table>
  <thead>
      <tr>
          <th>背景</th>
          <th>適合程度</th>
          <th>建議起點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用過 ChatGPT / Claude、沒碰過本地模型</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/00-foundations/" data-link-title="模組零：基礎知識與心智模型" data-link-desc="建立本地 LLM 的心智模型、釐清 MLX / MTP / oMLX 等常被混淆的術語、Apple Silicon 記憶體現實">模組零</a> 從頭讀</td>
      </tr>
      <tr>
          <td>裝過 Ollama 但被網路上的術語混淆</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MLX / MTP / oMLX 區分</a> + <a href="/blog/llm/00-foundations/info-judgment-frames/" data-link-title="0.6 判讀本地 LLM 資訊的五個框架" data-link-desc="本地 LLM 資訊更新快，學會用版本、層級、變數、能力、資料流五個框架評估文章與宣稱">判讀框架</a></td>
      </tr>
      <tr>
          <td>想知道 24GB / 32GB Mac 該選哪個模型</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">硬體記憶體預算</a> + <a href="/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">模型選型</a></td>
      </tr>
      <tr>
          <td>想用本地 LLM 完全取代 Claude / GPT-5</td>
          <td>部分適合</td>
          <td><a href="/blog/llm/01-local-llm-services/expectation-management/" data-link-title="1.5 期望管理：本地 LLM 的擅長領域與分工" data-link-desc="本地 LLM 是免費的初階 pair programmer：辨識它的擅長領域、跟雲端旗艦做結構性分工">期望管理</a> 先看完再決定</td>
      </tr>
      <tr>
          <td>想懂 LLM 內部運作機制</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/03-theoretical-foundations/" data-link-title="模組三：LLM 的理論基礎" data-link-desc="從神經網路、embedding、attention、Transformer 架構、訓練到 sampling：LLM 內部運作的完整理論圖像">模組三 理論基礎</a> 從頭讀（含 reasoning models / speculative decoding）</td>
      </tr>
      <tr>
          <td>想懂背後的數學</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/02-math-foundations/" data-link-title="模組二：LLM 的數學基礎" data-link-desc="整理 LLM 推論背後需要理解的線性代數、機率與資訊論、最佳化、數值精度等數學概念">模組二 數學基礎</a> 從頭讀</td>
      </tr>
      <tr>
          <td>想懂 o1 / DeepSeek-R1 等 reasoning model 怎麼運作</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/03-theoretical-foundations/reasoning-models/" data-link-title="3.8 Reasoning models：test-time compute paradigm" data-link-desc="Chain-of-thought 從 prompting 技巧演化成訓練 paradigm、reasoning model 的內部運作、本地可跑的選項與適用任務">3.8 Reasoning models</a> 從頭讀</td>
      </tr>
      <tr>
          <td>想做 LLM 應用開發（RAG / agent / tool use）</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/04-applications/" data-link-title="模組四：LLM 應用層原理" data-link-desc="Prompt 技術光譜、RAG、tool use、agent、應用層協議、人機協作、multi-agent、workflow 編排、eval 設計：跨工具不變的概念地圖">模組四</a> 從 4.0 RAG 依序讀</td>
      </tr>
      <tr>
          <td>想在自家 Hugo / Astro 等靜態網站加 RAG</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">4.16 靜態 / serverless RAG deployment</a>（含資安取捨）</td>
      </tr>
      <tr>
          <td>想用 VLM 看截圖 / 設計稿輔助寫 code</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/04-applications/vision-in-coding-workflow/" data-link-title="4.15 Vision in coding workflow：本地 VLM 怎麼接寫 code" data-link-desc="VLM 在 coding 工作流的 use cases、本地 VLM 選型、跟雲端 VLM 的分工、Continue.dev / Ollama 整合現狀">4.15 Vision in coding workflow</a></td>
      </tr>
      <tr>
          <td>想評估 LLM benchmark 數字、做 in-house eval</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14 Benchmarking 方法論</a></td>
      </tr>
      <tr>
          <td>想在本機 fine-tune 模型懂自家 codebase 慣例</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/03-theoretical-foundations/training-pipeline/" data-link-title="3.4 訓練流程：pre-train → SFT → RLHF" data-link-desc="LLM 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">3.4 訓練流程</a> 原理 + <a href="/blog/llm/01-local-llm-services/hands-on/local-fine-tuning/" data-link-title="Hands-on：用 QLoRA 在本機 fine-tune coding 模型" data-link-desc="Apple Silicon Mac / PC 獨立 GPU 上跑 QLoRA fine-tune 的完整流程：環境、資料、訓練、evaluation、合併、部署到 Ollama">QLoRA hands-on</a></td>
      </tr>
      <tr>
          <td>想做 large-scale fine-tune / 從頭訓練</td>
          <td>部分適合</td>
          <td>讀完模組三後進入 <a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">推薦的公開課程</a> 跟 Stanford CS336</td>
      </tr>
      <tr>
          <td>用 Windows / Linux + NVIDIA / AMD 獨立 GPU 跑本地 LLM</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/00-foundations/" data-link-title="模組零：基礎知識與心智模型" data-link-desc="建立本地 LLM 的心智模型、釐清 MLX / MTP / oMLX 等常被混淆的術語、Apple Silicon 記憶體現實">模組零</a> 建心智模型 + <a href="/blog/llm/05-discrete-gpu/" data-link-title="模組五：Windows / Linux &#43; 獨立 GPU" data-link-desc="消費級 PC（Windows / Linux &#43; NVIDIA / AMD 獨立 GPU）跑本地 LLM 的硬體判讀、MoE CPU 卸載、KV cache 量化與 llama.cpp 調參">模組五</a> 處理 VRAM 預算、MoE 卸載、KV cache 量化</td>
      </tr>
      <tr>
          <td>想知道本地 LLM 跑起來後的安全議題</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六</a> 個人 dev 視角的安全與權限</td>
      </tr>
      <tr>
          <td>想把 LLM 部署成 production 服務、處理服務化資安</td>
          <td>部分適合</td>
          <td>個人視角見 <a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六</a>；production 場景見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">Backend 模組七 資安</a> 的 LLM 相關章節</td>
      </tr>
      <tr>
          <td>想在資料中心級 GPU（H100 / H200 / B200）部署</td>
          <td>部分適合</td>
          <td>心智模型跟 <a href="/blog/llm/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理本地 LLM 寫 code 場景所需的概念詞彙">knowledge-cards</a> 通用；vLLM / TGI / Triton 等資料中心 inference server 另尋專門教材</td>
      </tr>
      <tr>
          <td>想跑 Stable Diffusion / Midjourney 等產圖</td>
          <td>跟主題不同</td>
          <td>產圖是 Diffusion 架構、見 <a href="/blog/llm/knowledge-cards/diffusion/" data-link-title="Diffusion" data-link-desc="產圖用的生成式 AI 架構：跟寫 code 用的 Transformer 是不同路線">Diffusion 卡片</a>、另尋 ComfyUI / Draw Things 教材</td>
      </tr>
  </tbody>
</table>
<h2 id="用語約定">用語約定</h2>
<p>本指南使用的關鍵術語在第一次出現時都附原文。為避免歧義，下列詞彙在本指南內固定指涉：</p>
<ol>
<li><strong>本地 LLM</strong>：跑在使用者自己機器（Mac 或 PC）上的大型語言模型推論、prompt 留在本機。</li>
<li><strong>推論伺服器</strong>（inference server）：負責載入模型權重、處理 prompt、產生 token 的常駐程式、例如 Ollama、LM Studio 內建 server、llama.cpp <code>server</code>。</li>
<li><strong>介面層</strong>：使用者實際打字互動的工具、例如 VS Code + Continue.dev、CLI、Web UI。介面層透過 API 跟推論伺服器溝通。</li>
<li><strong>模型</strong>（model）：權重檔本身、例如 <code>gemma4:31b</code>、<code>qwen3-coder:30b</code>。模型可以在不同推論伺服器之間共用、前提是格式相容。</li>
<li><strong>量化</strong>（quantization）：把模型權重從高精度（如 bf16）壓成低精度（如 Q4）以減少記憶體佔用、代價是少許品質下降。</li>
</ol>
<h2 id="不在本指南內的主題">不在本指南內的主題</h2>
<p>本指南不討論：</p>
<ul>
<li><strong>Speech / audio LLM</strong>：跟核心文字 LLM 是不同方向、本指南不涵蓋。Vision（VLM）原本不放、但因 coding 工作流的 vision use case 進入主流、補上 <a href="/blog/llm/04-applications/vision-in-coding-workflow/" data-link-title="4.15 Vision in coding workflow：本地 VLM 怎麼接寫 code" data-link-desc="VLM 在 coding 工作流的 use cases、本地 VLM 選型、跟雲端 VLM 的分工、Continue.dev / Ollama 整合現狀">4.15 Vision in coding workflow</a>；video LLM 仍不放。</li>
<li><strong>資料中心訓練的工程細節</strong>：data parallelism、ZeRO、tensor parallelism 等屬於專門課程的範圍。</li>
<li><strong>向量資料庫的 vendor 比較</strong>（Pinecone vs Weaviate vs Chroma 等）：vendor 格局半年一變、不適合寫入教材。RAG 的 storage 工程原理（升級判讀、index 生命週期、dependency 約束）見 <a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 RAG storage 工程</a>。</li>
<li><strong>Kubernetes / 資料中心級分散式推論</strong>：跟個人機器本地 LLM 方向不同、需另尋專門教材。</li>
<li><strong>多卡 NVLink、tensor parallelism</strong>：消費級 PC 場景通常單卡、本指南不涵蓋多卡分散式推論。</li>
</ul>
<p>若讀完本指南後想往這些方向走：</p>
<ol>
<li><strong>想做 <a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a> 應用</strong>：先把 Ollama + Continue.dev 跑穩、再讀 <a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">模組四 4.1 RAG 原理</a> 建立設計取捨判讀、或 <a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">模組三 3.8 推薦</a> 的 DeepLearning.AI short courses。</li>
<li><strong>想跑 coding <a href="/blog/llm/knowledge-cards/agent/" data-link-title="LLM Agent" data-link-desc="把控制流交給 LLM 的應用模式：自主決策、跨多步呼叫工具、人類角色從主導變監督">agent</a></strong>：先讀 <a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent 架構原理</a> 建立判讀、再看 <a href="/blog/llm/01-local-llm-services/extension-paths/" data-link-title="1.6 延伸方向：Web UI、coding agent、產圖" data-link-desc="日常路徑跑穩後可以玩的延伸：Open WebUI、aider、ComfyUI；先把基底跑穩再進階">1.6 延伸方向</a> 了解 aider、Cline 等工具的定位差異。</li>
<li><strong>想跑產圖模型</strong>：<a href="/blog/llm/knowledge-cards/diffusion/" data-link-title="Diffusion" data-link-desc="產圖用的生成式 AI 架構：跟寫 code 用的 Transformer 是不同路線">Diffusion</a> 跟 Transformer 是不同架構、請另尋 ComfyUI / Draw Things / Diffusers 教材。</li>
<li><strong>想自己訓練 / fine-tune</strong>：讀完模組三、進入 Karpathy zero-to-hero、Stanford CS336、Hugging Face NLP Course 等<a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">推薦資源</a>。</li>
</ol>
<hr>
<p><em>文件版本：v0.7.0</em>
<em>最後更新：2026-05-12</em>
<em>系列狀態：七個模組 + 125 張知識卡片。模組零（9 章）/ 一（10 章 + hands-on、含 QLoRA + judge harness）/ 二（5 章）/ 三（12 章、含 reasoning / speculative / constrained decoding）/ 四（17 章、含 long context / embedding / benchmarking / VLM / 靜態 deployment / coding agent harness / prompt caching / agent memory / tracing / LLM-as-judge）/ 五（7 章）/ 六（7 章、含 OWASP 對照）。</em></p>
]]></content:encoded></item><item><title>模組五案例正文</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/</guid><description>&lt;p>這個資料夾的核心責任是把平台遷移案例轉成部署策略、切流策略與回退策略的可操作內容。&lt;/p>
&lt;h2 id="案例列表">案例列表&lt;/h2>
&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;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1&lt;/a>&lt;/td>
 &lt;td>Tradeshift：self-managed K8s -&amp;gt; EKS&lt;/td>
 &lt;td>把零停機平台遷移拆成可執行階段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2&lt;/a>&lt;/td>
 &lt;td>Condé Nast：EKS 平台整併&lt;/td>
 &lt;td>把多團隊異質集群整併成單一治理面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3&lt;/a>&lt;/td>
 &lt;td>Orbitera：遷移到 managed Kubernetes&lt;/td>
 &lt;td>把平台重置與產品不中斷目標對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4&lt;/a>&lt;/td>
 &lt;td>Mobileye：workloads -&amp;gt; EKS&lt;/td>
 &lt;td>把工作負載搬遷策略做成可回退階段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5&lt;/a>&lt;/td>
 &lt;td>Miro：managed EKS 遷移&lt;/td>
 &lt;td>把平台託管化與團隊維運模型對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6&lt;/a>&lt;/td>
 &lt;td>Airbnb K8s 叢集演進&lt;/td>
 &lt;td>把手動擴縮轉成自動化容量治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7&lt;/a>&lt;/td>
 &lt;td>Airbnb Istio 升級治理&lt;/td>
 &lt;td>把 service mesh 升級變成可重播流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9&lt;/a>&lt;/td>
 &lt;td>反例：切流未先 drain&lt;/td>
 &lt;td>平台切換忽略連線清退造成錯誤暴增&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10&lt;/a>&lt;/td>
 &lt;td>對照：規模差異下平台遷移&lt;/td>
 &lt;td>小中大型組織的平台遷移風險邊界不同&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把平台遷移案例轉成部署策略、切流策略與回退策略的可操作內容。</p>
<h2 id="案例列表">案例列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1</a></td>
          <td>Tradeshift：self-managed K8s -&gt; EKS</td>
          <td>把零停機平台遷移拆成可執行階段</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2</a></td>
          <td>Condé Nast：EKS 平台整併</td>
          <td>把多團隊異質集群整併成單一治理面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3</a></td>
          <td>Orbitera：遷移到 managed Kubernetes</td>
          <td>把平台重置與產品不中斷目標對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4</a></td>
          <td>Mobileye：workloads -&gt; EKS</td>
          <td>把工作負載搬遷策略做成可回退階段</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5</a></td>
          <td>Miro：managed EKS 遷移</td>
          <td>把平台託管化與團隊維運模型對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6</a></td>
          <td>Airbnb K8s 叢集演進</td>
          <td>把手動擴縮轉成自動化容量治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7</a></td>
          <td>Airbnb Istio 升級治理</td>
          <td>把 service mesh 升級變成可重播流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9</a></td>
          <td>反例：切流未先 drain</td>
          <td>平台切換忽略連線清退造成錯誤暴增</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10</a></td>
          <td>對照：規模差異下平台遷移</td>
          <td>小中大型組織的平台遷移風險邊界不同</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>部署平台 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/</guid><description>&lt;p>部署平台 Vendor 清單的核心責任是把平台名稱放回 runtime contract、lifecycle contract、traffic contract、control plane 與 rollout governance 的判斷。每個服務頁先回答它承擔啟動、調度、入口、設定、基礎設施狀態或 service discovery 的哪一段，再討論操作成本與案例回寫。部署這塊能力的買 vs 建是一條深度光譜：自管 Kubernetes、用 managed K8s（EKS、GKE、AKS）、用 PaaS（Fly、Render、Railway），到完全 serverless — 越往右維運越少、客製與可攜性也越受限，取捨見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建&lt;/a>。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>部署服務要從服務生命週期進入。讀者如果要處理 container 與 runtime，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime&lt;/a>；如果要處理 rollout 與 probe，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes deployment&lt;/a>；如果要處理入口與 drain，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>部署平台服務頁的教學順序是先建立 workload runtime，再進入 orchestration、traffic entry、infra state 與 discovery。這個順序對齊 checkout E4：讀者先理解服務如何啟動、接流量、drain 與 rollback，再比較 Kubernetes、systemd、Docker、load balancer、proxy、Terraform 與 Consul 分別承擔哪一層平台責任。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&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;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes&lt;/a>&lt;/td>
 &lt;td>Orchestration&lt;/td>
 &lt;td>pod lifecycle、probe、rolling update 與 resource limit 如何成為平台契約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker&lt;/a>&lt;/td>
 &lt;td>Container runtime&lt;/td>
 &lt;td>image、entrypoint、runtime config 與 local / prod parity 如何管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd&lt;/a>&lt;/td>
 &lt;td>Process supervisor&lt;/td>
 &lt;td>unit、restart policy、signal 與 journal 如何支援單機服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx&lt;/a>&lt;/td>
 &lt;td>Reverse proxy / LB&lt;/td>
 &lt;td>reverse proxy、timeout、buffering、TLS 與 ingress 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy&lt;/a>&lt;/td>
 &lt;td>Service proxy&lt;/td>
 &lt;td>xDS、dynamic config、mesh data plane 與 traffic policy 如何治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB&lt;/a>&lt;/td>
 &lt;td>Managed LB&lt;/td>
 &lt;td>ALB / NLB、health check、draining 與 target group 如何支援 AWS 入口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform / OpenTofu&lt;/a>&lt;/td>
 &lt;td>IaC&lt;/td>
 &lt;td>state、plan、provider、drift 與 review gate 如何管理 infra 變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik&lt;/a>&lt;/td>
 &lt;td>Ingress / proxy&lt;/td>
 &lt;td>auto-discovery、dynamic routing 與 cloud-native ingress 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/consul/" data-link-title="Consul" data-link-desc="Service registry / mesh / KV / DNS">Consul&lt;/a>&lt;/td>
 &lt;td>Registry / mesh&lt;/td>
 &lt;td>service registry、DNS、health check、KV 與 mesh 邊界如何取捨&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入。&lt;/p></description><content:encoded><![CDATA[<p>部署平台 Vendor 清單的核心責任是把平台名稱放回 runtime contract、lifecycle contract、traffic contract、control plane 與 rollout governance 的判斷。每個服務頁先回答它承擔啟動、調度、入口、設定、基礎設施狀態或 service discovery 的哪一段，再討論操作成本與案例回寫。部署這塊能力的買 vs 建是一條深度光譜：自管 Kubernetes、用 managed K8s（EKS、GKE、AKS）、用 PaaS（Fly、Render、Railway），到完全 serverless — 越往右維運越少、客製與可攜性也越受限，取捨見 <a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建</a>。</p>
<h2 id="讀法">讀法</h2>
<p>部署服務要從服務生命週期進入。讀者如果要處理 container 與 runtime，先回到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime</a>；如果要處理 rollout 與 probe，先回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes deployment</a>；如果要處理入口與 drain，先回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>部署平台服務頁的教學順序是先建立 workload runtime，再進入 orchestration、traffic entry、infra state 與 discovery。這個順序對齊 checkout E4：讀者先理解服務如何啟動、接流量、drain 與 rollback，再比較 Kubernetes、systemd、Docker、load balancer、proxy、Terraform 與 Consul 分別承擔哪一層平台責任。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a></td>
          <td>Orchestration</td>
          <td>pod lifecycle、probe、rolling update 與 resource limit 如何成為平台契約</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a></td>
          <td>Container runtime</td>
          <td>image、entrypoint、runtime config 與 local / prod parity 如何管理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd</a></td>
          <td>Process supervisor</td>
          <td>unit、restart policy、signal 與 journal 如何支援單機服務</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a></td>
          <td>Reverse proxy / LB</td>
          <td>reverse proxy、timeout、buffering、TLS 與 ingress 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></td>
          <td>Service proxy</td>
          <td>xDS、dynamic config、mesh data plane 與 traffic policy 如何治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a></td>
          <td>Managed LB</td>
          <td>ALB / NLB、health check、draining 與 target group 如何支援 AWS 入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform / OpenTofu</a></td>
          <td>IaC</td>
          <td>state、plan、provider、drift 與 review gate 如何管理 infra 變更</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></td>
          <td>Ingress / proxy</td>
          <td>auto-discovery、dynamic routing 與 cloud-native ingress 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/consul/" data-link-title="Consul" data-link-desc="Service registry / mesh / KV / DNS">Consul</a></td>
          <td>Registry / mesh</td>
          <td>service registry、DNS、health check、KV 與 mesh 邊界如何取捨</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="kubernetes/">Kubernetes</a></td>
          <td><a href="kubernetes/graceful-shutdown/">graceful-shutdown</a></td>
          <td><a href="kubernetes/migrate-from-docker-swarm/">← Docker Swarm</a></td>
      </tr>
      <tr>
          <td><a href="terraform/">Terraform</a></td>
          <td>—</td>
          <td><a href="terraform/migrate-to-opentofu/">→ OpenTofu</a></td>
      </tr>
      <tr>
          <td><a href="consul/">Consul</a></td>
          <td>—</td>
          <td><a href="consul/migrate-from-etcd/">← etcd</a></td>
      </tr>
  </tbody>
</table>
<p>其他 T1 vendor（Docker / systemd / nginx / Envoy / AWS ELB / Traefik）尚未開始。對應的 backlog 議題見上方「T1 服務頁大綱」段每個服務頁要回答的核心問題、跟各 vendor <code>_index.md</code> 的「預計實作話題」段。</p>
<h2 id="服務頁撰寫欄位">服務頁撰寫欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>部署服務頁要保留的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務責任</td>
          <td>它承擔 runtime、orchestration、traffic entry、IaC、registry 還是 mesh</td>
      </tr>
      <tr>
          <td>適用壓力</td>
          <td>rollout frequency、instance count、long connection、multi-region、team ownership 哪個壓力最明顯</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>VM、container、Kubernetes、managed platform、service mesh、simple proxy 的機會成本</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>upgrade、config drift、certificate、health check、drain、state、rollback</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>deploy marker、per-version SLI、health check、drain completion、plan diff、registry freshness</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>Tradeshift、Condé Nast、Orbitera 與平台切換案例如何提供回退判準</td>
      </tr>
  </tbody>
</table>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>部署服務頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>它是 runtime、process supervisor、orchestrator、proxy、LB、IaC 還是 registry</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷 lifecycle、traffic、config、resource 與 rollback contract</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「服務如何啟動、接流量、擴容、停止、回退」快速定位平台層</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>image、unit、deployment、health check、drain、TLS、plan、registry</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>systemd、Docker、Kubernetes、managed runtime、proxy、service mesh 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>multi-cluster、service mesh、dynamic config、IaC drift、managed runtime</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>readiness、liveness、drain timeout、target health、config drift、state lock</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>單機服務回 systemd、多服務平台上 Kubernetes、簡單入口用 managed LB</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>完整 YAML / HCL 語法百科、雲端平台所有產品線、語言 framework deployment</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 5.C migration cases、6 release gate、8 decision log</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>本模組 9 個 vendor 跨 6 個 category（orchestrator / container / process / proxy / LB / IaC / registry）、不是同類產品的多個選項。對照表用「橫向工程議題」標明每個議題在哪些 vendor 是核心責任、哪些不適用。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>K8s</th>
          <th>Docker</th>
          <th>systemd</th>
          <th>nginx</th>
          <th>Envoy</th>
          <th>AWS ELB</th>
          <th>Terraform</th>
          <th>Traefik</th>
          <th>Consul</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主責任</td>
          <td>orchestration</td>
          <td>container build/run</td>
          <td>process supervisor</td>
          <td>reverse proxy</td>
          <td>service proxy</td>
          <td>managed LB</td>
          <td>IaC state</td>
          <td>ingress proxy</td>
          <td>registry / mesh</td>
      </tr>
      <tr>
          <td>服務生命週期</td>
          <td>pod lifecycle</td>
          <td>container run</td>
          <td>service unit</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>target health</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>health check</td>
      </tr>
      <tr>
          <td>流量入口</td>
          <td>Service/Ingress</td>
          <td>port mapping</td>
          <td>listen socket</td>
          <td>HTTP server</td>
          <td>listener</td>
          <td>listener</td>
          <td>N/A</td>
          <td>entrypoint</td>
          <td>N/A</td>
      </tr>
      <tr>
          <td>配置模式</td>
          <td>declarative</td>
          <td>imperative</td>
          <td>declarative</td>
          <td>static config</td>
          <td>xDS dynamic</td>
          <td>API / IaC</td>
          <td>declarative</td>
          <td>dynamic provider</td>
          <td>KV + watch</td>
      </tr>
      <tr>
          <td>Service discovery</td>
          <td>K8s DNS</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>manual upstream</td>
          <td>xDS EDS</td>
          <td>target group</td>
          <td>provider data</td>
          <td>provider 自動</td>
          <td>registry 原生</td>
      </tr>
      <tr>
          <td>Health check</td>
          <td>probe</td>
          <td>healthcheck</td>
          <td>restart policy</td>
          <td>upstream check</td>
          <td>active/passive</td>
          <td>health check</td>
          <td>N/A</td>
          <td>health check</td>
          <td>health check</td>
      </tr>
      <tr>
          <td>TLS / mTLS</td>
          <td>cert-manager</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>ssl module</td>
          <td>filter chain</td>
          <td>ACM</td>
          <td>provider data</td>
          <td>ACME 自動</td>
          <td>Connect mTLS</td>
      </tr>
      <tr>
          <td>Multi-cluster</td>
          <td>federation</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>manual</td>
          <td>mesh control plane</td>
          <td>cross-region</td>
          <td>provider chain</td>
          <td>per cluster</td>
          <td>DC federation</td>
      </tr>
      <tr>
          <td>授權模式</td>
          <td>Apache 2</td>
          <td>Apache 2 / Desktop license</td>
          <td>LGPL</td>
          <td>BSD-2 / Plus 商業</td>
          <td>Apache 2</td>
          <td>AWS managed</td>
          <td>BSL / OpenTofu MPL</td>
          <td>MIT / Hub 商業</td>
          <td>BSL</td>
      </tr>
      <tr>
          <td>主討論案例</td>
          <td>C1/C2/C3/C4/C8</td>
          <td>待補</td>
          <td>待補</td>
          <td>待補</td>
          <td>C5</td>
          <td>C9</td>
          <td>待補</td>
          <td>待補</td>
          <td>待補</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>寫某 vendor 頁時、檢查橫向議題該怎麼定位（不該強塞跟它無關的議題）</li>
<li>讀者理解「9 vendor 不是同類選一個、是不同 layer 各自一個」</li>
<li>評估部署 stack：選 orchestrator + container + proxy + LB + IaC + registry 各 1-2 個組合</li>
</ul>
<p>下面 5 段把對照表的關鍵橫向議題展開（不是每行都展開 — 部分行如「主責任」「授權模式」直接看表即可）。</p>
<h3 id="配置模式">配置模式</h3>
<p>配置模式跨 vendor 差異大、影響 dev workflow 跟 GitOps 整合度。<strong>K8s</strong> declarative（kubectl apply / YAML）；<strong>Terraform</strong> declarative（HCL）；<strong>systemd</strong> declarative（unit file）；<strong>Docker</strong> imperative（CLI）+ Compose declarative；<strong>nginx</strong> static config + reload；<strong>Envoy</strong> xDS dynamic（control plane push）；<strong>Traefik</strong> dynamic（provider 自動 sync）；<strong>AWS ELB</strong> API + IaC；<strong>Consul</strong> KV + watch。</p>
<p>選型判讀：要 GitOps → declarative（K8s + Terraform + systemd unit）；要 zero-reload → dynamic config（Envoy / Traefik）；要 manual control → imperative（Docker / 純 CLI）。</p>
<h3 id="service-discovery--health-check">Service discovery + Health check</h3>
<p>Service discovery 是 5 模組多個 vendor 共同關心的議題、但實作差異大。<strong>K8s</strong> 內建（Service + DNS + kube-proxy）；<strong>Consul</strong> registry first + DNS interface + health check 內建；<strong>Envoy</strong> EDS（xDS endpoint discovery）；<strong>Traefik</strong> provider 自動發現；<strong>nginx / AWS ELB</strong> 配置 upstream target；<strong>Docker / systemd</strong> N/A（單機 / 不負責 discovery）。</p>
<p>選型判讀：K8s-only → 內建；非 K8s 多平台 → Consul；K8s + service mesh → Istio + Envoy；單機 → nginx + manual config。</p>
<h3 id="multi-cluster--跨-dc">Multi-cluster / 跨 DC</h3>
<p>跨多 cluster / DC 拓樸差異大。<strong>K8s</strong> federation（v2 / Cluster API multi-cluster）；<strong>Consul</strong> 一級公民跨 DC（WAN federation）；<strong>Envoy + Istio</strong> multi-cluster mesh；<strong>Terraform</strong> 用 provider chain 管多 cloud / 多 cluster；<strong>AWS ELB</strong> cross-region replication；<strong>nginx / Traefik</strong> 一般 per cluster；<strong>systemd / Docker</strong> N/A。</p>
<p>選型判讀：跨 DC 為核心需求 → Consul / Istio；單一 cluster + cross-region LB → ELB / Global LB；多 cluster K8s → Cluster API + federation。</p>
<h3 id="tls--mtls">TLS / mTLS</h3>
<p>TLS / mTLS 在不同 vendor 由不同 layer 負責。<strong>K8s</strong> cert-manager（Let&rsquo;s Encrypt / 內部 CA）；<strong>AWS ELB</strong> ACM 自動憑證；<strong>Traefik</strong> ACME 自動 TLS；<strong>nginx</strong> ssl module + manual cert / cert-manager；<strong>Envoy</strong> filter chain（SDS 動態 cert）；<strong>Consul Connect</strong> mTLS 自動 sidecar；<strong>Terraform</strong> 不負責 TLS、提供 provider；<strong>Docker / systemd</strong> 不負責（交給 application 或上游 proxy）。</p>
<p>選型判讀：cluster 內 mTLS → cert-manager / Consul Connect；外部 TLS → ACME（Traefik / 自管 cert-manager）；managed → AWS ELB / Cloudflare。</p>
<h3 id="授權模式2023-2024-bsl-變動">授權模式（2023-2024 BSL 變動）</h3>
<p>2023-2024 多個 HashiCorp 產品改 BSL（Terraform / Vault / Consul / Boundary / Vagrant）— 影響採用決策。<strong>Terraform</strong> → OpenTofu fork（Linux Foundation、MPL 2.0）；<strong>Consul</strong> → 暫無大型 fork；<strong>Docker Desktop</strong> → 商業 license（員工 &gt; 250 / 收入 &gt; $10M）→ Podman Desktop 替代；<strong>nginx</strong> → F5 後 OSS 不滿 → Freenginx / angie fork；<strong>K8s / Envoy / Traefik</strong> → 仍 OSI 開源。</p>
<p>選型判讀：商業 SaaS 提供類似服務 → 避 BSL（用 OpenTofu / 自評）；企業內部使用 → BSL 多數無影響；公部門 / 嚴格合規 → 仍要 OSI 認可 license。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>D1</td>
          <td>Docker / systemd</td>
          <td>建立 runtime、entrypoint、process supervisor 與單機服務 baseline</td>
      </tr>
      <tr>
          <td>D2</td>
          <td>Kubernetes</td>
          <td>建立 workload lifecycle、orchestration、probe 與 rollout contract</td>
      </tr>
      <tr>
          <td>D3</td>
          <td>nginx / AWS ELB / Envoy / Traefik</td>
          <td>建立 traffic entry、drain、timeout 與 proxy policy 對照</td>
      </tr>
      <tr>
          <td>D4</td>
          <td>Terraform / OpenTofu / Consul</td>
          <td>建立 infra state、service registry 與 control-plane boundary</td>
      </tr>
      <tr>
          <td>D5</td>
          <td>ECS / Fargate / Cloud Run / Nomad</td>
          <td>補 managed runtime、platform abstraction 與自管調度對照</td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選">後續候選</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GitOps / package</td>
          <td>Argo CD、Flux、Helm、Kustomize</td>
          <td>desired state、release review、config drift、environment promotion</td>
      </tr>
      <tr>
          <td>Ingress / Gateway</td>
          <td>ingress-nginx、Envoy Gateway、Gateway API、HAProxy</td>
          <td>routing contract、TLS、cross-namespace policy、drain</td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>Istio、Linkerd、Cilium Service Mesh</td>
          <td>mTLS、traffic split、sidecar / ambient、control-plane cost</td>
      </tr>
      <tr>
          <td>Managed runtime</td>
          <td>ECS、Fargate、Cloud Run、Azure Container Apps、Fly.io</td>
          <td>managed scaling、deployment contract、platform limit</td>
      </tr>
      <tr>
          <td>Alternative orchestrator</td>
          <td>Nomad、OpenShift、Rancher</td>
          <td>operations model、multi-cluster governance、enterprise support</td>
      </tr>
      <tr>
          <td>IaC / PaaS</td>
          <td>Pulumi、Heroku、Railway、Vercel</td>
          <td>developer workflow、state ownership、backend suitability</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是分開 runtime、orchestration、ingress / gateway、GitOps、IaC 與 mesh。Kubernetes 是 orchestration baseline；Argo CD / Flux / Helm / Kustomize 解 desired state delivery；ingress-nginx / Envoy Gateway / HAProxy 解 traffic entry；Istio / Linkerd / Cilium 解 service-to-service policy；ECS / Fargate / Cloud Run 解 managed runtime。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes deployment</a></li>
<li>上游：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract</a></li>
<li>案例：<a href="/blog/backend/05-deployment-platform/cases/" data-link-title="模組五案例正文" data-link-desc="部署平台轉換案例入口。">5.C 部署平台案例正文</a></li>
<li>服務路徑：<a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout with Drain and Rollback</a></li>
</ul>
]]></content:encoded></item><item><title>LLM Deployment 供應鏈完整性</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-deployment-supply-chain/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-deployment-supply-chain/</guid><description>&lt;p>本章的責任是把 LLM 服務的模型權重、推論伺服器、第三方 plugin / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP&lt;/a> server 三條供應鏈、納入 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈與產物信任&lt;/a> 的既有框架。模型來源信任的判讀依據見 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/model-card/" data-link-title="Model Card" data-link-desc="Hugging Face 等平台上模型的 metadata 文件、列出模型來源、訓練資料、能力、限制、授權">model card&lt;/a> 卡；通用 artifact 信任機制見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">artifact-provenance&lt;/a> 卡。LLM 場景的特殊性在於模型權重既是「資料」又是「程式邏輯」、第三方 MCP 是可執行程式碼、跟一般 software artifact 的信任模型有部分差異、但 build provenance / signature / dependency isolation 等控制原則沿用同一套。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦 production LLM 服務的供應鏈完整性問題節點。個人 dev 視角的模型來源信任見 &lt;a href="https://tarrragon.github.io/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">llm/6.0 模型供應鏈與信任邊界&lt;/a>；本章不重複個人 dev 場景的判讀、聚焦 production 場景下的特殊議題（規模化下載、跨 region 鏡像、retry 策略、模型 release 流程）。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：模型權重 build provenance（HF organization / 量化者 / Ollama registry）、GGUF / safetensors artifact 完整性、production 下載與鏡像策略、第三方 MCP / plugin 的 deployment 供應鏈、模型版本回退機制。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>一般 software artifact 信任 → &lt;a href="../supply-chain-integrity-and-artifact-trust/">7.4 supply-chain-integrity-and-artifact-trust&lt;/a>&lt;/li>
&lt;li>機器憑證 → &lt;a href="../secrets-and-machine-credential-governance/">7.6 secrets-and-machine-credential-governance&lt;/a>&lt;/li>
&lt;li>入口治理 → &lt;a href="../entrypoint-and-server-protection/">7.3 entrypoint-and-server-protection&lt;/a>&lt;/li>
&lt;li>個人 dev 模型來源信任 → &lt;a href="https://tarrragon.github.io/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">llm/6.0 model-supply-chain-trust&lt;/a>&lt;/li>
&lt;li>部署平台 → &lt;code>05-deployment-platform&lt;/code>、可靠性 → &lt;code>06-reliability&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer、沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 control link 進 knowledge-card、看具體機制與 LLM 場景的差異。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;h2 id="llm-供應鏈的三條-chain">LLM 供應鏈的三條 chain&lt;/h2>
&lt;p>LLM 服務的供應鏈跟一般 software 服務的差異在「同時管三條 chain」：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>模型權重 chain&lt;/strong>：原始作者 → 官方 release → 量化者 → registry → production 鏡像&lt;/li>
&lt;li>&lt;strong>推論伺服器 chain&lt;/strong>：llama.cpp / vLLM / Ollama 等 server software 的一般 software artifact chain&lt;/li>
&lt;li>&lt;strong>第三方 plugin / MCP chain&lt;/strong>：MCP server / Continue.dev 等的程式碼供應鏈&lt;/li>
&lt;/ol>
&lt;p>三條 chain 在 production 階段都需要 build provenance、簽署驗證、依賴隔離跟回退機制。差異主要在模型權重 chain 的特殊性：權重是大型 binary（GB 級）、難以靜態 audit、且權重本身會影響推論行為。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把 LLM 服務的模型權重、推論伺服器、第三方 plugin / <a href="/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP</a> server 三條供應鏈、納入 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈與產物信任</a> 的既有框架。模型來源信任的判讀依據見 <a href="/blog/llm/knowledge-cards/model-card/" data-link-title="Model Card" data-link-desc="Hugging Face 等平台上模型的 metadata 文件、列出模型來源、訓練資料、能力、限制、授權">model card</a> 卡；通用 artifact 信任機制見 <a href="/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">artifact-provenance</a> 卡。LLM 場景的特殊性在於模型權重既是「資料」又是「程式邏輯」、第三方 MCP 是可執行程式碼、跟一般 software artifact 的信任模型有部分差異、但 build provenance / signature / dependency isolation 等控制原則沿用同一套。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦 production LLM 服務的供應鏈完整性問題節點。個人 dev 視角的模型來源信任見 <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">llm/6.0 模型供應鏈與信任邊界</a>；本章不重複個人 dev 場景的判讀、聚焦 production 場景下的特殊議題（規模化下載、跨 region 鏡像、retry 策略、模型 release 流程）。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：模型權重 build provenance（HF organization / 量化者 / Ollama registry）、GGUF / safetensors artifact 完整性、production 下載與鏡像策略、第三方 MCP / plugin 的 deployment 供應鏈、模型版本回退機制。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>一般 software artifact 信任 → <a href="../supply-chain-integrity-and-artifact-trust/">7.4 supply-chain-integrity-and-artifact-trust</a></li>
<li>機器憑證 → <a href="../secrets-and-machine-credential-governance/">7.6 secrets-and-machine-credential-governance</a></li>
<li>入口治理 → <a href="../entrypoint-and-server-protection/">7.3 entrypoint-and-server-protection</a></li>
<li>個人 dev 模型來源信任 → <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">llm/6.0 model-supply-chain-trust</a></li>
<li>部署平台 → <code>05-deployment-platform</code>、可靠性 → <code>06-reliability</code></li>
</ul>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer、沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 control link 進 knowledge-card、看具體機制與 LLM 場景的差異。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>05-deployment-platform / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<h2 id="llm-供應鏈的三條-chain">LLM 供應鏈的三條 chain</h2>
<p>LLM 服務的供應鏈跟一般 software 服務的差異在「同時管三條 chain」：</p>
<ol>
<li><strong>模型權重 chain</strong>：原始作者 → 官方 release → 量化者 → registry → production 鏡像</li>
<li><strong>推論伺服器 chain</strong>：llama.cpp / vLLM / Ollama 等 server software 的一般 software artifact chain</li>
<li><strong>第三方 plugin / MCP chain</strong>：MCP server / Continue.dev 等的程式碼供應鏈</li>
</ol>
<p>三條 chain 在 production 階段都需要 build provenance、簽署驗證、依賴隔離跟回退機制。差異主要在模型權重 chain 的特殊性：權重是大型 binary（GB 級）、難以靜態 audit、且權重本身會影響推論行為。</p>
<h2 id="分析模型">分析模型</h2>
<p>production LLM 供應鏈的分析依五個層次拆解、跟 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4</a> 的層次模型保持一致：</p>
<ol>
<li><strong>來源層</strong>：模型 build provenance 是否可回溯（哪個 base model、用哪個 dataset、由誰量化）。</li>
<li><strong>產物層</strong>：GGUF / safetensors 在傳遞過程的完整性（hash / 簽署）。</li>
<li><strong>依賴層</strong>：MCP server / inference framework / model 各自獨立信任、影響面隔離。</li>
<li><strong>節奏層</strong>：模型版本切換、回退、freeze 流程。</li>
<li><strong>收斂層</strong>：供應鏈事件能否路由到 IR 流程。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「可部署的 LLM 服務」轉成「可信的 LLM 服務」。</p>
<ol>
<li>先確認模型來源 organization、量化版本、build provenance 可關聯。</li>
<li>再確認 GGUF / safetensors 的完整性證據（hash、size、metadata）。</li>
<li>接著確認模型 + server + plugin 三條 chain 的依賴隔離。</li>
<li>最後交接到可靠性與 incident 流程、追蹤回退能力。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模型來源不可追溯</td>
          <td>HF organization 不明、量化者沒公開 build script</td>
          <td>模型可信度下降、無法 audit、合規問題</td>
          <td><a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">ci-pipeline</a></td>
      </tr>
      <tr>
          <td>GGUF artifact 完整性斷點</td>
          <td>缺 hash 比對、CDN 鏡像未驗證、未簽署</td>
          <td>模型權重被替換、影響推論行為</td>
          <td><a href="/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">deployment-contract</a></td>
      </tr>
      <tr>
          <td>第三方 MCP / plugin 風險放大</td>
          <td>多服務共用同一 MCP server、依賴版本固定</td>
          <td>單一 MCP server 漏洞波及多 service</td>
          <td><a href="/blog/backend/knowledge-cards/dependency-isolation/" data-link-title="Dependency Isolation" data-link-desc="說明如何隔離下游依賴，避免單一依賴耗盡共享資源">dependency-isolation</a></td>
      </tr>
      <tr>
          <td>模型版本切換節奏混亂</td>
          <td>版本切換條件不一致、回退測試缺失</td>
          <td>切換時行為差異未測、production incident</td>
          <td><a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release-gate</a></td>
      </tr>
      <tr>
          <td>量化版本污染</td>
          <td>信任未知量化者、未做 behavior regression</td>
          <td>量化過程引入後門或非預期行為</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract-test</a></td>
      </tr>
      <tr>
          <td>跨 region 鏡像不一致</td>
          <td>不同 region 跑不同版本權重、cache 政策衝突</td>
          <td>一致性議題、debug 困難</td>
          <td><a href="/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">deployment-contract</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時 LLM 供應鏈風險已進入高壓狀態。</p>
<ul>
<li>模型來源（base + dataset + 量化者）長期無法回溯時、代表 provenance 模型失效。</li>
<li>模型 artifact 在 CDN / 鏡像層沒有簽署驗證時、代表完整性邊界不足。</li>
<li>MCP server / plugin 跟 inference framework 共用單一信任域時、代表依賴隔離不足。</li>
<li>模型版本切換沒有 behavior regression test 時、代表 release 流程不收斂。</li>
</ul>
<h2 id="llm-場景的特殊判讀">LLM 場景的特殊判讀</h2>
<p>LLM 供應鏈相對一般 software 供應鏈有幾個特殊點：</p>
<ol>
<li><strong>權重是大型 binary、難以靜態 audit</strong>：跟 source code 不同、權重檔案無法用 grep / diff / linter 找後門；只能用 behavior testing 跟 hash 比對。</li>
<li><strong>量化過程可能改變推論行為</strong>：同一 base model 不同量化版本、回答品質有差；量化者的可信度影響整體可信度、需 case-by-case 信任。</li>
<li><strong>模型 supply chain 跟 production deployment 解耦</strong>：模型釋出方（如 Meta、Qwen 團隊）跟 production 部署方通常不同單位、責任邊界要明確。</li>
<li><strong>「license」議題</strong>：模型權重的 license（如 Llama Community License）跟一般 software license 不同、production 使用需 legal review、不只是技術議題。</li>
<li><strong>MCP server 多為 Node / Python 程式</strong>：跟一般 dependency 一樣有 supply chain 風險、但 LLM 場景下、MCP 對主機資源的副作用面比一般 dependency 大、需更嚴格的 isolation。</li>
</ol>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>LLM 場景的供應鏈事件案例尚在累積中、本章先沿用 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4</a> 的通用案例。LLM-specific 案例累積後會補入 <code>red-team/cases/llm-supply-chain/</code>：</p>
<ul>
<li>開源組件滲透與下游衝擊：<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a>（同類威脅在 MCP server / inference framework 也適用）</li>
<li>平台級供應鏈事件：<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020</a>（模型釋出方平台級事件適用）</li>
</ul>
<blockquote>
<p><strong>事實查核註</strong>：LLM 供應鏈的公開事件案例累積還在早期、本章列舉的通用案例提供 mechanism 對照、不代表 LLM 場景已有等同規模的事件記錄。建議引用前以最新的 <a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">OWASP LLM Top 10</a> 跟社群 incident 報告為準。</p></blockquote>
<h2 id="引用標準">引用標準</h2>
<p>LLM 場景的供應鏈標準在發展中、本章沿用 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈與產物信任</a> 的標準作為 mechanism 層 anchor、補上 LLM-specific 參考：</p>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>版本 / 年份</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLSA</td>
          <td>v1.0 (2023)</td>
          <td>套用於 inference server + MCP build provenance</td>
      </tr>
      <tr>
          <td>Sigstore（cosign / Rekor / Fulcio）</td>
          <td>continuous</td>
          <td>模型 artifact 簽署實驗階段</td>
      </tr>
      <tr>
          <td>OWASP LLM Top 10</td>
          <td>2025</td>
          <td>LLM application security 通用 reference</td>
      </tr>
      <tr>
          <td>Hugging Face Model Card spec</td>
          <td>continuous</td>
          <td>模型來源 metadata</td>
      </tr>
  </tbody>
</table>
<p>引用版本與 cadence 規則見 <a href="/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &#43; sync owner（內部）。">security-citation-currency-and-precision</a>。Last reviewed: 2026-05-12。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>交付平台與部署治理：<code>05-deployment-platform</code></li>
<li>發佈驗證與回退演練：<code>06-reliability</code></li>
<li>多租戶 LLM 推論隔離：<a href="/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/" data-link-title="LLM 多租戶推論隔離" data-link-desc="production LLM 服務的多租戶隔離：KV cache 不共享、log / model artifact 隔離、跨用戶 prompt 洩漏面">llm-multi-tenant-isolation</a></li>
<li>偵測訊號設計：<a href="/blog/backend/07-security-data-protection/llm-as-service-detection-coverage/" data-link-title="LLM Service 偵測訊號覆蓋" data-link-desc="production LLM 服務的 detection 訊號設計：tool call 異常模式、prompt injection 觸發徵兆、abuse 跟濫用模式、跟既有 detection-coverage 框架的接合">llm-as-service-detection-coverage</a></li>
<li>分級與跨部門收斂：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>Startup Probe</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/startup-probe/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/startup-probe/</guid><description>&lt;p>Startup probe 的核心概念是「在服務啟動期間持續探測、確認初始化完成後再交棒給 liveness 與 readiness &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe&lt;/a>」。它保護啟動時間長的服務（JVM warmup、大量依賴連線建立）不被 liveness 在初始化期間判定失敗而反覆重啟。可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">Probe&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Startup probe 位在 container 啟動與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> / liveness 之間。startup probe 成功前，liveness 和 readiness 不會啟動。startup probe 一旦成功就永久停用，由 liveness 和 readiness 接手。可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Graceful Shutdown&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 startup probe 的訊號是「服務啟動時間超過 liveness 的預設容忍窗口」。典型場景：JVM 服務 warmup 需 30-60 秒、依賴多的服務需要等資料庫連線池和 cache 連線建立。沒有 startup probe 時，liveness 會在初始化期間把健康的服務判定為壞掉，觸發 restart loop。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>startup probe 的總容忍時間 = &lt;code>failureThreshold × periodSeconds&lt;/code>。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線），再加 headroom。startup probe 跟 &lt;code>initialDelaySeconds&lt;/code> 解決同一個問題，但 startup probe 在啟動期間持續探測（能偵測啟動失敗），&lt;code>initialDelaySeconds&lt;/code> 是盲等（無法觀測啟動進度）。&lt;/p></description><content:encoded><![CDATA[<p>Startup probe 的核心概念是「在服務啟動期間持續探測、確認初始化完成後再交棒給 liveness 與 readiness <a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>」。它保護啟動時間長的服務（JVM warmup、大量依賴連線建立）不被 liveness 在初始化期間判定失敗而反覆重啟。可先對照 <a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">Probe</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Startup probe 位在 container 啟動與 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> / liveness 之間。startup probe 成功前，liveness 和 readiness 不會啟動。startup probe 一旦成功就永久停用，由 liveness 和 readiness 接手。可先對照 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Graceful Shutdown</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 startup probe 的訊號是「服務啟動時間超過 liveness 的預設容忍窗口」。典型場景：JVM 服務 warmup 需 30-60 秒、依賴多的服務需要等資料庫連線池和 cache 連線建立。沒有 startup probe 時，liveness 會在初始化期間把健康的服務判定為壞掉，觸發 restart loop。</p>
<h2 id="設計責任">設計責任</h2>
<p>startup probe 的總容忍時間 = <code>failureThreshold × periodSeconds</code>。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線），再加 headroom。startup probe 跟 <code>initialDelaySeconds</code> 解決同一個問題，但 startup probe 在啟動期間持續探測（能偵測啟動失敗），<code>initialDelaySeconds</code> 是盲等（無法觀測啟動進度）。</p>
]]></content:encoded></item><item><title>Canary Release</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/canary-release/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/canary-release/</guid><description>&lt;p>Canary release 的核心概念是「先把小比例流量導向新版本、觀察行為、再決定是否擴大」。它把版本切換從一次性決策變成連續多批決策，每批都有明確的觀察窗口與停損條件。可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">Rolling Update&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Canary release 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a> 之間。Rolling update 是逐批替換實例的機制，canary 是在替換過程中加入「先驗證再擴批」的決策層。Release gate 是每批擴大前的放行條件。可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary Perf Check&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 canary release 的訊號是「版本切換需要控制 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>」。判讀要維持 per-version 視角——只看整體平均值會掩蓋新版本的局部退化。常見 stop condition 包含 per-version error rate 偏離、p95/p99 latency 惡化、依賴 timeout 連續超門檻、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining&lt;/a> 未完成。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Canary release 要定義三件事：切換單位（比例 / 區域 / 租戶 / 路由規則）、每批觀察窗口與停損條件、回退路徑（舊版本是否仍能承接回退流量）。效能退化的檢查見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary Perf Check&lt;/a>。Canary 決策的 evidence 格式見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">Evidence Package&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Canary release 的核心概念是「先把小比例流量導向新版本、觀察行為、再決定是否擴大」。它把版本切換從一次性決策變成連續多批決策，每批都有明確的觀察窗口與停損條件。可先對照 <a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">Rolling Update</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Canary release 位在 <a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update</a> 與 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 之間。Rolling update 是逐批替換實例的機制，canary 是在替換過程中加入「先驗證再擴批」的決策層。Release gate 是每批擴大前的放行條件。可先對照 <a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary Perf Check</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 canary release 的訊號是「版本切換需要控制 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>」。判讀要維持 per-version 視角——只看整體平均值會掩蓋新版本的局部退化。常見 stop condition 包含 per-version error rate 偏離、p95/p99 latency 惡化、依賴 timeout 連續超門檻、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 未完成。</p>
<h2 id="設計責任">設計責任</h2>
<p>Canary release 要定義三件事：切換單位（比例 / 區域 / 租戶 / 路由規則）、每批觀察窗口與停損條件、回退路徑（舊版本是否仍能承接回退流量）。效能退化的檢查見 <a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary Perf Check</a>。Canary 決策的 evidence 格式見 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">Evidence Package</a>。</p>
]]></content:encoded></item><item><title>Outbound Tunnel</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/outbound-tunnel/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/outbound-tunnel/</guid><description>&lt;p>Outbound tunnel 是一種入口形態：本機進程主動對外連到邊緣節點，把流量沿反向隧道帶回來，路由器零開 port、對公網零入站面。跟傳統 port-forward（從外往內開 port）的責任方向相反 — 連線由內部發起、外部只能沿已建立的隧道回來。與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer&lt;/a> 的責任方向不同：LB 假設 instance 有公開可達位址，tunnel 由內部主動外連。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Outbound tunnel 位在本機進程與公網之間，取代傳統的 port-forward 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer&lt;/a> 入口。常與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS&lt;/a> 搭配保護隧道內的傳輸安全，認證則疊在 tunnel 之後由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authentication-middleware/" data-link-title="Authentication Middleware" data-link-desc="說明請求進入 handler 前如何完成身份驗證">authentication middleware&lt;/a> 處理。&lt;/p>
&lt;p>常見實作包括 cloudflared（綁 Cloudflare 邊緣）和 Tailscale（WireGuard mesh VPN）。隧道網址是位址、不是密碼 — 認證必須疊在 tunnel 之後。&lt;/p>
&lt;p>深入：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口與生命週期&lt;/a>。選型案例：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/remote-shell-access-tailscale-vs-cloudflare-tunnel/" data-link-title="7.C11 選型：單人遠端 Shell — Tailscale vs Cloudflare Tunnel" data-link-desc="以「手機遠端操作本機 shell」為情境，比較 Tailscale mesh VPN 與 Cloudflare Tunnel &amp;#43; Access 兩種存取模型的選型判讀。">7.C11 Tailscale vs Cloudflare Tunnel&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Outbound tunnel 是一種入口形態：本機進程主動對外連到邊緣節點，把流量沿反向隧道帶回來，路由器零開 port、對公網零入站面。跟傳統 port-forward（從外往內開 port）的責任方向相反 — 連線由內部發起、外部只能沿已建立的隧道回來。與 <a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 的責任方向不同：LB 假設 instance 有公開可達位址，tunnel 由內部主動外連。</p>
<h2 id="概念位置">概念位置</h2>
<p>Outbound tunnel 位在本機進程與公網之間，取代傳統的 port-forward 或 <a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 入口。常與 <a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a> 搭配保護隧道內的傳輸安全，認證則疊在 tunnel 之後由 <a href="/blog/backend/knowledge-cards/authentication-middleware/" data-link-title="Authentication Middleware" data-link-desc="說明請求進入 handler 前如何完成身份驗證">authentication middleware</a> 處理。</p>
<p>常見實作包括 cloudflared（綁 Cloudflare 邊緣）和 Tailscale（WireGuard mesh VPN）。隧道網址是位址、不是密碼 — 認證必須疊在 tunnel 之後。</p>
<p>深入：<a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口與生命週期</a>。選型案例：<a href="/blog/backend/07-security-data-protection/cases/remote-shell-access-tailscale-vs-cloudflare-tunnel/" data-link-title="7.C11 選型：單人遠端 Shell — Tailscale vs Cloudflare Tunnel" data-link-desc="以「手機遠端操作本機 shell」為情境，比較 Tailscale mesh VPN 與 Cloudflare Tunnel &#43; Access 兩種存取模型的選型判讀。">7.C11 Tailscale vs Cloudflare Tunnel</a>。</p>
]]></content:encoded></item><item><title>7.22 資安風險如何進入 Release Gate</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-risk-in-release-gate/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-risk-in-release-gate/</guid><description>&lt;p>本篇的責任是把資安風險接到 release gate。讀者讀完後，能把控制驗證、例外條件與風險判讀轉成放行判準。&lt;/p>
&lt;h2 id="核心論點">核心論點&lt;/h2>
&lt;p>資安進入 release gate 的核心概念是讓放行決策可回查。放行條件一旦包含風險與證據，變更速度與風險控制可以共同優化。&lt;/p>
&lt;h2 id="gate-欄位">Gate 欄位&lt;/h2>
&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>Risk classification&lt;/td>
 &lt;td>定義變更風險等級&lt;/td>
 &lt;td>risk label&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Required controls&lt;/td>
 &lt;td>定義必備控制驗證&lt;/td>
 &lt;td>control checklist&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence bundle&lt;/td>
 &lt;td>定義放行證據集合&lt;/td>
 &lt;td>evidence package&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Exception window&lt;/td>
 &lt;td>定義例外期間與補償措施&lt;/td>
 &lt;td>exception record&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Decision owner&lt;/td>
 &lt;td>定義放行決策責任&lt;/td>
 &lt;td>approval route&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Re-evaluation trigger&lt;/td>
 &lt;td>定義重評估條件&lt;/td>
 &lt;td>tripwire link&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="高風險變更流程">高風險變更流程&lt;/h2>
&lt;p>高風險變更流程的責任是讓放行有階段節奏。流程可分成預檢、驗證、審查、放行、回寫五步，並固定記錄風險假設與驗證結果。&lt;/p>
&lt;h2 id="例外治理">例外治理&lt;/h2>
&lt;p>例外治理的責任是讓例外成為受控狀態。例外紀錄至少包含期限、補償控制、回收條件與 owner，並接到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tripwire/" data-link-title="Tripwire" data-link-desc="說明風險決策在條件變化時如何自動回到評估流程">tripwire&lt;/a>。&lt;/p>
&lt;h2 id="與部署與可靠性交接">與部署與可靠性交接&lt;/h2>
&lt;p>與部署與可靠性交接的責任是把 gate 決策接到執行層。放行結果可直接交接到部署流程、回退策略與 incident readiness。&lt;/p>
&lt;h2 id="判讀訊號與路由">判讀訊號與路由&lt;/h2>
&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>需要補資安證據欄位&lt;/td>
 &lt;td>7.22 → 7.B3&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>例外到期後仍持續放行&lt;/td>
 &lt;td>需要補 re-evaluation trigger&lt;/td>
 &lt;td>7.22 → 7.14&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高風險變更缺少 owner 決策&lt;/td>
 &lt;td>需要補 decision owner&lt;/td>
 &lt;td>7.22 → 05&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>放行後事故率上升&lt;/td>
 &lt;td>需要補 gate 迭代回寫&lt;/td>
 &lt;td>7.22 → 7.24&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="從-gate-通過到-control-實際驗證">從 Gate 通過到 control 實際驗證&lt;/h2>
&lt;p>Gate 通過代表流程跑完（risk classification + controls + evidence + exception 全填）；control 是否真在生產驗過、要靠兩條 chain：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Evidence chain&lt;/strong>：evidence package 列的證據要對應到 control 實際 mechanism、不只填欄位。例：「TLS 已啟用」要附 cipher suite + cert valid + HSTS preload 證據、不只 prod 連得上 https。Mechanism 細節見 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任&lt;/a> 跟對應 knowledge-card。&lt;/li>
&lt;li>&lt;strong>Re-evaluation chain&lt;/strong>：tripwire 觸發 / 例外到期 / 事件 trigger 接到 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14 例外治理&lt;/a> 跟 7.x 主章節再評估。&lt;/li>
&lt;/ul>
&lt;p>Gate 通過 + 兩條 chain 跑通、放行才是 risk reduce 決策。Gate 跟 control 是流程層 vs 實作層、由 evidence 內容對應。&lt;/p>
&lt;h2 id="必連章節">必連章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14 資安治理例外與 Tripwire&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/security-control-validation/" data-link-title="7.B3 資安控制驗證" data-link-desc="建立資安控制面如何用證據、演練與 release gate 驗證的大綱">7.B3 資安控制驗證&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-control-handoff-to-delivery-and-incident/" data-link-title="7.18 資安控制面如何交接到部署與事故流程" data-link-desc="建立資安控制面交接到部署、可靠性與事故流程的大綱">7.18 資安控制面如何交接到部署與事故流程&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-and-reliability-shared-controls/" data-link-title="7.23 資安與可靠性的共同控制面" data-link-desc="建立資安與可靠性共同控制面的交集，整合 rollback、containment、degradation 與 evidence">7.23 資安與可靠性的共同控制面&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="完稿判準">完稿判準&lt;/h2>
&lt;p>完稿時要讓讀者能為高風險變更建立資安 gate。輸出至少包含風險等級、控制驗證、證據包、例外條件與重評估觸發器。&lt;/p></description><content:encoded><![CDATA[<p>本篇的責任是把資安風險接到 release gate。讀者讀完後，能把控制驗證、例外條件與風險判讀轉成放行判準。</p>
<h2 id="核心論點">核心論點</h2>
<p>資安進入 release gate 的核心概念是讓放行決策可回查。放行條件一旦包含風險與證據，變更速度與風險控制可以共同優化。</p>
<h2 id="gate-欄位">Gate 欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>產出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Risk classification</td>
          <td>定義變更風險等級</td>
          <td>risk label</td>
      </tr>
      <tr>
          <td>Required controls</td>
          <td>定義必備控制驗證</td>
          <td>control checklist</td>
      </tr>
      <tr>
          <td>Evidence bundle</td>
          <td>定義放行證據集合</td>
          <td>evidence package</td>
      </tr>
      <tr>
          <td>Exception window</td>
          <td>定義例外期間與補償措施</td>
          <td>exception record</td>
      </tr>
      <tr>
          <td>Decision owner</td>
          <td>定義放行決策責任</td>
          <td>approval route</td>
      </tr>
      <tr>
          <td>Re-evaluation trigger</td>
          <td>定義重評估條件</td>
          <td>tripwire link</td>
      </tr>
  </tbody>
</table>
<h2 id="高風險變更流程">高風險變更流程</h2>
<p>高風險變更流程的責任是讓放行有階段節奏。流程可分成預檢、驗證、審查、放行、回寫五步，並固定記錄風險假設與驗證結果。</p>
<h2 id="例外治理">例外治理</h2>
<p>例外治理的責任是讓例外成為受控狀態。例外紀錄至少包含期限、補償控制、回收條件與 owner，並接到 <a href="/blog/backend/knowledge-cards/tripwire/" data-link-title="Tripwire" data-link-desc="說明風險決策在條件變化時如何自動回到評估流程">tripwire</a>。</p>
<h2 id="與部署與可靠性交接">與部署與可靠性交接</h2>
<p>與部署與可靠性交接的責任是把 gate 決策接到執行層。放行結果可直接交接到部署流程、回退策略與 incident readiness。</p>
<h2 id="判讀訊號與路由">判讀訊號與路由</h2>
<table>
  <thead>
      <tr>
          <th>判讀訊號</th>
          <th>代表需求</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>發版條件只看功能測試</td>
          <td>需要補資安證據欄位</td>
          <td>7.22 → 7.B3</td>
      </tr>
      <tr>
          <td>例外到期後仍持續放行</td>
          <td>需要補 re-evaluation trigger</td>
          <td>7.22 → 7.14</td>
      </tr>
      <tr>
          <td>高風險變更缺少 owner 決策</td>
          <td>需要補 decision owner</td>
          <td>7.22 → 05</td>
      </tr>
      <tr>
          <td>放行後事故率上升</td>
          <td>需要補 gate 迭代回寫</td>
          <td>7.22 → 7.24</td>
      </tr>
  </tbody>
</table>
<h2 id="從-gate-通過到-control-實際驗證">從 Gate 通過到 control 實際驗證</h2>
<p>Gate 通過代表流程跑完（risk classification + controls + evidence + exception 全填）；control 是否真在生產驗過、要靠兩條 chain：</p>
<ul>
<li><strong>Evidence chain</strong>：evidence package 列的證據要對應到 control 實際 mechanism、不只填欄位。例：「TLS 已啟用」要附 cipher suite + cert valid + HSTS preload 證據、不只 prod 連得上 https。Mechanism 細節見 <a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任</a> 跟對應 knowledge-card。</li>
<li><strong>Re-evaluation chain</strong>：tripwire 觸發 / 例外到期 / 事件 trigger 接到 <a href="/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14 例外治理</a> 跟 7.x 主章節再評估。</li>
</ul>
<p>Gate 通過 + 兩條 chain 跑通、放行才是 risk reduce 決策。Gate 跟 control 是流程層 vs 實作層、由 evidence 內容對應。</p>
<h2 id="必連章節">必連章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14 資安治理例外與 Tripwire</a></li>
<li><a href="/blog/backend/07-security-data-protection/blue-team/security-control-validation/" data-link-title="7.B3 資安控制驗證" data-link-desc="建立資安控制面如何用證據、演練與 release gate 驗證的大綱">7.B3 資安控制驗證</a></li>
<li><a href="/blog/backend/07-security-data-protection/security-control-handoff-to-delivery-and-incident/" data-link-title="7.18 資安控制面如何交接到部署與事故流程" data-link-desc="建立資安控制面交接到部署、可靠性與事故流程的大綱">7.18 資安控制面如何交接到部署與事故流程</a></li>
<li><a href="/blog/backend/07-security-data-protection/security-and-reliability-shared-controls/" data-link-title="7.23 資安與可靠性的共同控制面" data-link-desc="建立資安與可靠性共同控制面的交集，整合 rollback、containment、degradation 與 evidence">7.23 資安與可靠性的共同控制面</a></li>
</ul>
<h2 id="完稿判準">完稿判準</h2>
<p>完稿時要讓讀者能為高風險變更建立資安 gate。輸出至少包含風險等級、控制驗證、證據包、例外條件與重評估觸發器。</p>
]]></content:encoded></item></channel></rss>