<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Lou Stack Base</title>
    <link>https://loustack.dev/zh-tw/</link>
    <description>Lou&#39;s knowledge base for software engineering, DevOps, and topics I&#39;m exploring.</description>
    <generator>Hugo 0.161.1 &amp; FixIt v0.4.5</generator>
    <language>zh-TW</language>
    <managingEditor>louChang.tw@gmail.com (Lou Chang)</managingEditor>
    <webMaster>louChang.tw@gmail.com (Lou Chang)</webMaster>
    <lastBuildDate>Sun, 17 May 2026 22:00:00 -0400</lastBuildDate>
    <atom:link href="https://loustack.dev/zh-tw/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>把同一個工具同時做成 CLI 和 MCP 之後, 我學到的事</title>
      <link>https://loustack.dev/zh-tw/content-i18n-cli-mcp-system-design/</link>
      <pubDate>Sun, 17 May 2026 22:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/content-i18n-cli-mcp-system-design/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/tools/">Tools</category>
      <description>&lt;p&gt;這篇記錄 &lt;a href=&#34;https://github.com/loustack17/content-i18n&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;&lt;code&gt;content-i18n&lt;/code&gt;&lt;/a&gt; 同時支援 CLI 和 MCP server 時的 system design&lt;/p&gt;&#xA;&lt;p&gt;重點不是 MCP protocol 本身, 而是同一個 workflow 要怎麼被兩種 entrypoint 共用&lt;/p&gt;&#xA;&lt;p&gt;核心問題:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;CLI 是什麼&lt;/li&gt;&#xA;&lt;li&gt;MCP 是什麼&lt;/li&gt;&#xA;&lt;li&gt;它們各自應該負責什麼&lt;/li&gt;&#xA;&lt;li&gt;為什麼不能各自長一套 workflow&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;content-i18n&lt;/code&gt; 最後怎麼切 core, CLI, MCP&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;什麼是-cli&#34;&gt;&lt;span&gt;什麼是 CLI&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%bb%80%e9%ba%bc%e6%98%af-cli&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;CLI 是 command-line interface, 也就是人透過 terminal 下 command 操作程式&lt;/p&gt;&#xA;&lt;p&gt;例如:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;content-i18n review --config ./content-i18n.yaml --file target.md --source source.md&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這個 command 裡有幾個部分:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;content-i18n&lt;/code&gt;: 要執行的程式&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;review&lt;/code&gt;: 要執行的 action&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--config&lt;/code&gt;: config file path&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--file&lt;/code&gt;: target file&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--source&lt;/code&gt;: source file&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;CLI 的使用者通常是人, shell script, CI job, 或其他 automation&lt;/p&gt;&#xA;&lt;p&gt;一個好的 CLI 通常要處理:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;command name 清楚&lt;/li&gt;&#xA;&lt;li&gt;flag 好理解&lt;/li&gt;&#xA;&lt;li&gt;error message 看得懂&lt;/li&gt;&#xA;&lt;li&gt;exit code 可以給 script 判斷&lt;/li&gt;&#xA;&lt;li&gt;output 可以給人讀, 也可以給 shell 接&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;CLI 是 human-facing entrypoint&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;cli-怎麼運作&#34;&gt;&lt;span&gt;CLI 怎麼運作&lt;/span&gt;&#xA;  &lt;a href=&#34;#cli-%e6%80%8e%e9%ba%bc%e9%81%8b%e4%bd%9c&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;CLI 大概是這條 flow:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;command&#xA;  -&amp;gt; argument parsing&#xA;  -&amp;gt; config loading&#xA;  -&amp;gt; core workflow call&#xA;  -&amp;gt; human-facing output&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;以 &lt;code&gt;content-i18n review&lt;/code&gt; 來說:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;讀 command 和 flags&lt;/li&gt;&#xA;&lt;li&gt;載入 &lt;code&gt;content-i18n.yaml&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;找到 source 和 target file&lt;/li&gt;&#xA;&lt;li&gt;呼叫真正的 review workflow&lt;/li&gt;&#xA;&lt;li&gt;把結果印給使用者&lt;/li&gt;&#xA;&lt;li&gt;根據結果回傳 exit code&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;CLI 可以決定 output 怎麼印, 錯誤訊息怎麼呈現, command 怎麼命名&lt;/p&gt;&#xA;&lt;p&gt;但 review 檢查什麼, queue state 怎麼算, sync-status 什麼時候能更新, 不應該放在 CLI layer&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;cli-layer-要怎麼設計&#34;&gt;&lt;span&gt;CLI layer 要怎麼設計&lt;/span&gt;&#xA;  &lt;a href=&#34;#cli-layer-%e8%a6%81%e6%80%8e%e9%ba%bc%e8%a8%ad%e8%a8%88&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;CLI layer 適合負責:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;command tree&lt;/li&gt;&#xA;&lt;li&gt;flag parsing&lt;/li&gt;&#xA;&lt;li&gt;config path loading&lt;/li&gt;&#xA;&lt;li&gt;shell-friendly output&lt;/li&gt;&#xA;&lt;li&gt;human-readable error&lt;/li&gt;&#xA;&lt;li&gt;exit code&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;CLI layer 不適合負責:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;prepare 的 business rule&lt;/li&gt;&#xA;&lt;li&gt;review 的 business rule&lt;/li&gt;&#xA;&lt;li&gt;queue state 推導&lt;/li&gt;&#xA;&lt;li&gt;sync-status 判斷&lt;/li&gt;&#xA;&lt;li&gt;batch orchestration&lt;/li&gt;&#xA;&lt;li&gt;source / target mapping rule&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;CLI 是 adapter:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;human&#xA;  -&amp;gt; CLI&#xA;  -&amp;gt; internal/core&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;它把人的 command 轉成 core workflow 可以吃的 input, 再把 core workflow 的 result 轉成人看得懂的 output&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;什麼是-mcp&#34;&gt;&lt;span&gt;什麼是 MCP&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%bb%80%e9%ba%bc%e6%98%af-mcp&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;MCP 是 Model Context Protocol&lt;/p&gt;&#xA;&lt;p&gt;在這個專案裡, MCP 讓 agent runtime 可以用 structured tool call 操作本機工具&lt;/p&gt;&#xA;&lt;p&gt;如果 CLI 是 human 跟 local program 之間的 contract, MCP 就是 agent runtime 跟 local program 之間的 contract&lt;/p&gt;&#xA;&lt;p&gt;CLI call:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;content-i18n review --config ./content-i18n.yaml --file target.md --source source.md&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MCP tool call:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;{&#xA;  &amp;#34;tool&amp;#34;: &amp;#34;content_i18n_review_translation&amp;#34;,&#xA;  &amp;#34;file&amp;#34;: &amp;#34;target.md&amp;#34;,&#xA;  &amp;#34;source&amp;#34;: &amp;#34;source.md&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;CLI 是 command + flags&lt;/p&gt;&#xA;&lt;p&gt;MCP 是 tool name + structured arguments&lt;/p&gt;&#xA;&lt;p&gt;形式不同, 但最後要做的事情可以是同一件事&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;mcp-怎麼運作&#34;&gt;&lt;span&gt;MCP 怎麼運作&lt;/span&gt;&#xA;  &lt;a href=&#34;#mcp-%e6%80%8e%e9%ba%bc%e9%81%8b%e4%bd%9c&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;MCP layer 大概是這條 flow:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;tool definition&#xA;  -&amp;gt; request decoding&#xA;  -&amp;gt; core workflow call&#xA;  -&amp;gt; structured response&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;一支 MCP tool 通常要定義:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;tool name&lt;/li&gt;&#xA;&lt;li&gt;description&lt;/li&gt;&#xA;&lt;li&gt;input schema&lt;/li&gt;&#xA;&lt;li&gt;handler&lt;/li&gt;&#xA;&lt;li&gt;response shape&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以 &lt;code&gt;content_i18n_review_translation&lt;/code&gt; 來說:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;宣告這支 tool 存在&lt;/li&gt;&#xA;&lt;li&gt;定義它需要哪些 arguments&lt;/li&gt;&#xA;&lt;li&gt;接 agent runtime 傳進來的 request&lt;/li&gt;&#xA;&lt;li&gt;decode 成 core workflow 可以用的 input&lt;/li&gt;&#xA;&lt;li&gt;呼叫 review workflow&lt;/li&gt;&#xA;&lt;li&gt;回傳 structured response 給 agent&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;MCP 的 output 要讓 agent 可以接著做下一步&lt;/p&gt;&#xA;&lt;p&gt;所以 response 通常要包含:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;issue list&lt;/li&gt;&#xA;&lt;li&gt;file path&lt;/li&gt;&#xA;&lt;li&gt;source path&lt;/li&gt;&#xA;&lt;li&gt;sync readiness&lt;/li&gt;&#xA;&lt;li&gt;next action hint&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;但 MCP 不該自己實作 review&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;mcp-layer-要怎麼設計&#34;&gt;&lt;span&gt;MCP layer 要怎麼設計&lt;/span&gt;&#xA;  &lt;a href=&#34;#mcp-layer-%e8%a6%81%e6%80%8e%e9%ba%bc%e8%a8%ad%e8%a8%88&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;MCP layer 適合負責:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;public tool contract&lt;/li&gt;&#xA;&lt;li&gt;argument schema&lt;/li&gt;&#xA;&lt;li&gt;request decoding&lt;/li&gt;&#xA;&lt;li&gt;handler registration&lt;/li&gt;&#xA;&lt;li&gt;structured response&lt;/li&gt;&#xA;&lt;li&gt;agent-friendly result shape&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;MCP layer 不適合負責:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;自己決定 review rule&lt;/li&gt;&#xA;&lt;li&gt;自己推導 queue state&lt;/li&gt;&#xA;&lt;li&gt;自己判斷 completion&lt;/li&gt;&#xA;&lt;li&gt;自己處理 source / target mapping&lt;/li&gt;&#xA;&lt;li&gt;自己補 batch orchestration rule&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;code&gt;content-i18n&lt;/code&gt; 的 MCP server 後來整理成:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;internal/mcp/&#xA;  server.go&#xA;  tools.go&#xA;  tool_defs.go&#xA;  tool_handlers.go&#xA;  resources.go&#xA;  resource_defs.go&#xA;  resource_handlers.go&#xA;  response.go&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這個結構讓責任比較清楚:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;definition 放 tool contract&lt;/li&gt;&#xA;&lt;li&gt;handler 接 request 並呼叫 core&lt;/li&gt;&#xA;&lt;li&gt;registration 負責把 tool 掛到 server&lt;/li&gt;&#xA;&lt;li&gt;response 負責 agent-facing output shape&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;registration 也改成 registry / spec pattern&lt;/p&gt;&#xA;&lt;p&gt;這樣 public contract 不會藏在一大段手工註冊裡, 新增或移除 tool 時也比較不容易漏掉&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;為什麼-cli-和-mcp-要共用同一個-core-workflow&#34;&gt;&lt;span&gt;為什麼 CLI 和 MCP 要共用同一個 core workflow&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%82%ba%e4%bb%80%e9%ba%bc-cli-%e5%92%8c-mcp-%e8%a6%81%e5%85%b1%e7%94%a8%e5%90%8c%e4%b8%80%e5%80%8b-core-workflow&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;同一個工具支援 CLI 和 MCP 時, 最大風險是兩邊慢慢變成兩個產品&lt;/p&gt;&#xA;&lt;p&gt;如果兩邊各自實作邏輯, 很快就會 drift:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;CLI review pass, MCP review fail&lt;/li&gt;&#xA;&lt;li&gt;MCP sync 成功, CLI queue 還是 stale&lt;/li&gt;&#xA;&lt;li&gt;batch path 和 single-file path 檢查不一致&lt;/li&gt;&#xA;&lt;li&gt;agent 繞過 review, 直接改 target file&lt;/li&gt;&#xA;&lt;li&gt;completion rule 在不同入口有不同解釋&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;所以 workflow 必須只有一個 authority&lt;/p&gt;&#xA;&lt;p&gt;在 &lt;code&gt;content-i18n&lt;/code&gt; 裡, 這個 authority 是 &lt;code&gt;internal/core&lt;/code&gt;&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;CLI / MCP = interface&#xA;internal/core = workflow owner&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;CLI 和 MCP 可以有不同 input / output&lt;/p&gt;&#xA;&lt;p&gt;但 prepare, review, sync, queue, batch 的 meaning 只能有一份&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;content-i18n-的-core-boundary&#34;&gt;&lt;span&gt;&lt;code&gt;content-i18n&lt;/code&gt; 的 core boundary&lt;/span&gt;&#xA;  &lt;a href=&#34;#content-i18n-%e7%9a%84-core-boundary&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;最後比較穩定的分層:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;consumer repo&#xA;  ├─ content roots&#xA;  ├─ content-i18n.yaml&#xA;  ├─ glossary&#xA;  └─ style pack&#xA;        │&#xA;        ▼&#xA;CLI / MCP&#xA;        │&#xA;        ▼&#xA;internal/core&#xA;  ├─ init&#xA;  ├─ prepare/review/sync&#xA;  ├─ queue&#xA;  ├─ batch orchestration&#xA;  └─ site validation entrypoint&#xA;        │&#xA;        ├─ validator&#xA;        ├─ structure&#xA;        ├─ frontmatter&#xA;        ├─ content&#xA;        └─ providers&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;責任切分:&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Layer&lt;/th&gt;&#xA;          &lt;th&gt;Responsibility&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;CLI&lt;/td&gt;&#xA;          &lt;td&gt;command parsing, flags, config path, exit code, human-readable output&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;MCP&lt;/td&gt;&#xA;          &lt;td&gt;tool definition, request decoding, handler registration, structured response&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;internal/core&lt;/td&gt;&#xA;          &lt;td&gt;prepare, review, sync, queue, batch, validation workflow&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;validator / structure / frontmatter / content&lt;/td&gt;&#xA;          &lt;td&gt;low-level document rules&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;providers&lt;/td&gt;&#xA;          &lt;td&gt;DeepL, Google, ai-harness provider boundary&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;CLI 和 MCP 呼叫 &lt;code&gt;internal/core&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;internal/core&lt;/code&gt; 不需要知道 caller 是 CLI 還是 MCP&lt;/p&gt;&#xA;&lt;p&gt;Review rule 改一次, CLI 和 MCP 都會吃到同一個行為&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;mcp-tool-surface-要怎麼設計&#34;&gt;&lt;span&gt;MCP tool surface 要怎麼設計&lt;/span&gt;&#xA;  &lt;a href=&#34;#mcp-tool-surface-%e8%a6%81%e6%80%8e%e9%ba%bc%e8%a8%ad%e8%a8%88&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;MCP tool surface 不能太碎&lt;/p&gt;&#xA;&lt;p&gt;早期如果把 tool 拆成 low-level primitive, 看起來很有彈性:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;read source&lt;/li&gt;&#xA;&lt;li&gt;create work packet&lt;/li&gt;&#xA;&lt;li&gt;validate translation&lt;/li&gt;&#xA;&lt;li&gt;write translation target&lt;/li&gt;&#xA;&lt;li&gt;next translation&lt;/li&gt;&#xA;&lt;li&gt;repair translation&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;但這對 agent 反而危險&lt;/p&gt;&#xA;&lt;p&gt;因為 tool surface 不只是 API 清單, 它也在暗示 agent 應該怎麼做事&lt;/p&gt;&#xA;&lt;p&gt;raw primitive 太多, agent 很容易自己組 workflow:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;read source&#xA;  -&amp;gt; inspect file&#xA;  -&amp;gt; edit target directly&#xA;  -&amp;gt; run partial validation&#xA;  -&amp;gt; skip sync-status&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這就是 tool bypass&lt;/p&gt;&#xA;&lt;p&gt;所以 &lt;code&gt;content-i18n&lt;/code&gt; 後來把 public MCP surface 收斂成 workflow-level tools:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;content_i18n_status&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;content_i18n_prepare_translation&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;content_i18n_review_translation&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;content_i18n_sync_status&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;content_i18n_translation_queue&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;content_i18n_translate_batch&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;content_i18n_validate_site&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;這 7 支 tool 比 low-level primitive 更適合 agent:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;prepare_translation&lt;/code&gt; 比 &lt;code&gt;read_source&lt;/code&gt; + &lt;code&gt;create_work_packet&lt;/code&gt; 更接近真實 workflow step&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;translation_queue&lt;/code&gt; 已經能帶出 candidate, 不需要額外 &lt;code&gt;next_translation&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;review_translation&lt;/code&gt; 回 structured issue 和 sync readiness, 比單純 validator wrapper 更完整&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;translate_batch&lt;/code&gt; 保留 batch orchestration, 不讓 agent 自己拼 batch loop&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;sync_status&lt;/code&gt; 把 completion 變成正式狀態, 不只是檔案存在&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;比較好的 MCP tool design:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;public tool 少&lt;/li&gt;&#xA;&lt;li&gt;operation 高階&lt;/li&gt;&#xA;&lt;li&gt;response 完整&lt;/li&gt;&#xA;&lt;li&gt;caller 不容易自己補 workflow&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;queue-state-怎麼運作&#34;&gt;&lt;span&gt;Queue state 怎麼運作&lt;/span&gt;&#xA;  &lt;a href=&#34;#queue-state-%e6%80%8e%e9%ba%bc%e9%81%8b%e4%bd%9c&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Queue model 讓 translation workflow 從一次性任務變成可維護系統&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;content-i18n&lt;/code&gt; 用三種 state:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;completed&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;stale&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;missing&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;這些 state 從下面資料推導:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;source discovery&lt;/li&gt;&#xA;&lt;li&gt;expected target path&lt;/li&gt;&#xA;&lt;li&gt;source hash&lt;/li&gt;&#xA;&lt;li&gt;translation status&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;三種 state 的意思:&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;State&lt;/th&gt;&#xA;          &lt;th&gt;Meaning&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;missing&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;source exists, expected target does not exist&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;stale&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;target exists, but source changed after last sync&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;completed&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;review / validation passed, target synced with source&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;翻譯不是 once-and-done&lt;/p&gt;&#xA;&lt;p&gt;今天完成的 target, 只要 source 改了, 明天就可能 stale&lt;/p&gt;&#xA;&lt;p&gt;所以 queue 不只是列出待翻譯檔案, 它是在回答:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;哪些 target 不存在&lt;/li&gt;&#xA;&lt;li&gt;哪些 target 已經跟 source 不同步&lt;/li&gt;&#xA;&lt;li&gt;哪些 target 真的完成&lt;/li&gt;&#xA;&lt;li&gt;下一個該處理哪一篇&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;MCP tool 也要尊重 queue model&lt;/p&gt;&#xA;&lt;p&gt;如果 agent 可以跳過 queue, review, sync, workflow authority 會失效&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;failure-modes-和最後規則&#34;&gt;&lt;span&gt;Failure modes 和最後規則&lt;/span&gt;&#xA;  &lt;a href=&#34;#failure-modes-%e5%92%8c%e6%9c%80%e5%be%8c%e8%a6%8f%e5%89%87&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;這個設計主要避免幾種 failure mode&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;cli--mcp-drift&#34;&gt;&lt;span&gt;CLI / MCP drift&lt;/span&gt;&#xA;  &lt;a href=&#34;#cli--mcp-drift&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;CLI 和 MCP 如果各自實作 review, 很快就會不一致&lt;/p&gt;&#xA;&lt;p&gt;修法:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;CLI 和 MCP 都只呼叫 core&lt;/li&gt;&#xA;&lt;li&gt;review rule 只放一份&lt;/li&gt;&#xA;&lt;li&gt;queue state 只從 core 推導&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;wrapper-logic-leak&#34;&gt;&lt;span&gt;Wrapper logic leak&lt;/span&gt;&#xA;  &lt;a href=&#34;#wrapper-logic-leak&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;MCP wrapper 如果開始補 workflow rule, wrapper 會越來越厚&lt;/p&gt;&#xA;&lt;p&gt;修法:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;MCP handler 只 decode request&lt;/li&gt;&#xA;&lt;li&gt;handler 呼叫 core&lt;/li&gt;&#xA;&lt;li&gt;handler format structured response&lt;/li&gt;&#xA;&lt;li&gt;不在 handler 裡重新判斷 business rule&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;low-level-tool-bypass&#34;&gt;&lt;span&gt;Low-level tool bypass&lt;/span&gt;&#xA;  &lt;a href=&#34;#low-level-tool-bypass&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;low-level tools 太多, agent 會自己組 workflow&lt;/p&gt;&#xA;&lt;p&gt;修法:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;public surface 收斂成 workflow-level tools&lt;/li&gt;&#xA;&lt;li&gt;response 帶足夠資訊&lt;/li&gt;&#xA;&lt;li&gt;review 和 sync-status 成為 official path&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;fuzzy-completion&#34;&gt;&lt;span&gt;Fuzzy completion&lt;/span&gt;&#xA;  &lt;a href=&#34;#fuzzy-completion&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;如果 completion 只是 &lt;code&gt;target file exists&lt;/code&gt;, queue 會失真&lt;/p&gt;&#xA;&lt;p&gt;修法:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;review / validation 要 pass&lt;/li&gt;&#xA;&lt;li&gt;source-language leftover 要清掉&lt;/li&gt;&#xA;&lt;li&gt;sync-status 要成功&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;duplicated-state-logic&#34;&gt;&lt;span&gt;Duplicated state logic&lt;/span&gt;&#xA;  &lt;a href=&#34;#duplicated-state-logic&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;如果 queue state 在 CLI, MCP, batch 各算一次, 結果會不一致&lt;/p&gt;&#xA;&lt;p&gt;修法:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;queue state 推導集中在 core&lt;/li&gt;&#xA;&lt;li&gt;caller 只使用 core result&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;最後規則:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;CLI 和 MCP 是 entrypoint, 不是兩個產品&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;internal/core&lt;/code&gt; 是唯一 workflow owner&lt;/li&gt;&#xA;&lt;li&gt;MCP tool 要偏 workflow-level&lt;/li&gt;&#xA;&lt;li&gt;Public surface 要少, response 要完整&lt;/li&gt;&#xA;&lt;li&gt;Queue state 必須由系統推導&lt;/li&gt;&#xA;&lt;li&gt;Completion 要有正式 path&lt;/li&gt;&#xA;&lt;li&gt;Tool design 要防 agent bypass&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;這樣 CLI 和 MCP 才能共用同一個工具核心, 不會慢慢長成兩套系統&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/loustack17/content-i18n&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;content-i18n GitHub Repository&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://modelcontextprotocol.io/docs/getting-started/intro&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Model Context Protocol — What is MCP?&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://modelcontextprotocol.io/docs/learn/architecture&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Model Context Protocol — Architecture Overview&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://modelcontextprotocol.io/specification/2025-06-18/basic/index&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Model Context Protocol Specification — Basic Overview&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://modelcontextprotocol.io/docs/sdk&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Model Context Protocol — SDKs&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;</description>
    </item>
    <item>
      <title>從即時翻譯到真正的英文內容, 為什麼做 content-i18n</title>
      <link>https://loustack.dev/zh-tw/content-i18n-fidelity-first-translation-workflow/</link>
      <pubDate>Sat, 16 May 2026 22:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/content-i18n-fidelity-first-translation-workflow/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/tools/">Tools</category>
      <description>&lt;p&gt;我的 Blog 原本已經有 &lt;code&gt;cmpt-translate&lt;/code&gt;, 也就是頁面可以即時翻譯成英文&lt;/p&gt;&#xA;&lt;p&gt;但在多倫多 coffee chat 的時候, 有人直接建議我: 如果 Blog 要當作品集的一部分, 預設語言應該是英文&lt;/p&gt;&#xA;&lt;p&gt;這句話點到的不是功能問題, 而是內容主體問題&lt;/p&gt;&#xA;&lt;p&gt;即時翻譯可以把頁面轉成另一種語言, 但它不等於我真的有英文內容, 也不等於我有一套可以長期維護英文文章的 workflow&lt;/p&gt;&#xA;&lt;p&gt;所以我開始做 &lt;a href=&#34;https://github.com/loustack17/content-i18n&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;&lt;code&gt;content-i18n&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;目標很明確: 把 Blog 從 &amp;ldquo;中文為主, 英文靠即時翻譯&amp;rdquo; 變成真的有英文文章可以維護&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;問題不是翻得更快&#34;&gt;&lt;span&gt;問題不是翻得更快&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%95%8f%e9%a1%8c%e4%b8%8d%e6%98%af%e7%bf%bb%e5%be%97%e6%9b%b4%e5%bf%ab&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;如果只是要先拿到英文草稿, 方法很多:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;直接貼到 AI Chat&lt;/li&gt;&#xA;&lt;li&gt;用 Google Translate 或 DeepL&lt;/li&gt;&#xA;&lt;li&gt;call translation API 產生初稿&lt;/li&gt;&#xA;&lt;li&gt;讓 agent 幫忙改成英文&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;這些方法都能用, 速度也快&lt;/p&gt;&#xA;&lt;p&gt;但技術 Blog 不是只有 prose&lt;/p&gt;&#xA;&lt;p&gt;一篇文章通常會混在一起:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;heading 和 section 順序&lt;/li&gt;&#xA;&lt;li&gt;code block&lt;/li&gt;&#xA;&lt;li&gt;inline command&lt;/li&gt;&#xA;&lt;li&gt;config key&lt;/li&gt;&#xA;&lt;li&gt;product name&lt;/li&gt;&#xA;&lt;li&gt;error string&lt;/li&gt;&#xA;&lt;li&gt;table&lt;/li&gt;&#xA;&lt;li&gt;blockquote&lt;/li&gt;&#xA;&lt;li&gt;argument flow&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我要保留的是同一篇文章, 不是只把文字換成另一種語言&lt;/p&gt;&#xA;&lt;p&gt;所以核心規則很簡單:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;same article&lt;/li&gt;&#xA;&lt;li&gt;different language only&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;fidelity-first-的意思&#34;&gt;&lt;span&gt;Fidelity-first 的意思&lt;/span&gt;&#xA;  &lt;a href=&#34;#fidelity-first-%e7%9a%84%e6%84%8f%e6%80%9d&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;fidelity-first 不是逐字翻譯, 也不是讓英文變得很僵&lt;/p&gt;&#xA;&lt;p&gt;它處理的是一個取捨: 當 &amp;ldquo;讀起來更順&amp;rdquo; 和 &amp;ldquo;保住原文&amp;rdquo; 開始衝突時, workflow 要先保哪一邊&lt;/p&gt;&#xA;&lt;p&gt;我的答案是先保原文&lt;/p&gt;&#xA;&lt;p&gt;所以 &lt;code&gt;content-i18n&lt;/code&gt; 會檢查這些東西:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;heading hierarchy&lt;/li&gt;&#xA;&lt;li&gt;section order&lt;/li&gt;&#xA;&lt;li&gt;paragraph coverage&lt;/li&gt;&#xA;&lt;li&gt;list structure&lt;/li&gt;&#xA;&lt;li&gt;table structure&lt;/li&gt;&#xA;&lt;li&gt;code block&lt;/li&gt;&#xA;&lt;li&gt;technical inline literal&lt;/li&gt;&#xA;&lt;li&gt;link 和 reference&lt;/li&gt;&#xA;&lt;li&gt;argument flow&lt;/li&gt;&#xA;&lt;li&gt;conclusion scope&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;如果英文讀起來很順, 但少了一個 caveat, 壓掉一個例子, 或把 command 改成另一種說法, 那就不是我要的結果&lt;/p&gt;&#xA;&lt;p&gt;這也是為什麼我後來把 translation 當成 validation problem, 而不是只看 generation quality&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;即時翻譯不夠的地方&#34;&gt;&lt;span&gt;即時翻譯不夠的地方&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%8d%b3%e6%99%82%e7%bf%bb%e8%ad%af%e4%b8%8d%e5%a4%a0%e7%9a%84%e5%9c%b0%e6%96%b9&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;最麻煩的不是模型翻得完全錯&lt;/p&gt;&#xA;&lt;p&gt;更常見的是看起來差不多, 但內容慢慢 drift&lt;/p&gt;&#xA;&lt;p&gt;我遇過的問題包括:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;opening paragraph 被翻得比原文更曲折, 甚至多出原文沒有的內容&lt;/li&gt;&#xA;&lt;li&gt;heading 變得像文章標題, 但原本要強調的點不見了&lt;/li&gt;&#xA;&lt;li&gt;code block 還在, 但 surrounding sentence 意思偏掉&lt;/li&gt;&#xA;&lt;li&gt;某一段被 AI 覺得重複, 就自己幫我精簡&lt;/li&gt;&#xA;&lt;li&gt;英文稿裡還殘留中文&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;這些問題不一定會讓文章看起來壞掉, 但會讓文章慢慢變成另一篇&lt;/p&gt;&#xA;&lt;p&gt;所以我不想要每次都靠 copy-paste, 再自己從頭檢查一次&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;workflow-設計&#34;&gt;&lt;span&gt;Workflow 設計&lt;/span&gt;&#xA;  &lt;a href=&#34;#workflow-%e8%a8%ad%e8%a8%88&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;最後我需要的是 workflow, 不是一個 prompt&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;prepare&#xA;  -&amp;gt; write target&#xA;  -&amp;gt; review&#xA;  -&amp;gt; fix&#xA;  -&amp;gt; sync-status&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;每一步都有自己的責任&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;prepare&#34;&gt;&lt;span&gt;prepare&lt;/span&gt;&#xA;  &lt;a href=&#34;#prepare&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;prepare&lt;/code&gt; 不只是讀 source file&lt;/p&gt;&#xA;&lt;p&gt;它在定義 translation unit:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;source path&lt;/li&gt;&#xA;&lt;li&gt;target path&lt;/li&gt;&#xA;&lt;li&gt;structure fingerprint&lt;/li&gt;&#xA;&lt;li&gt;glossary&lt;/li&gt;&#xA;&lt;li&gt;style pack&lt;/li&gt;&#xA;&lt;li&gt;prompt context&lt;/li&gt;&#xA;&lt;li&gt;target metadata&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;這一步會影響輸出穩定性&lt;/p&gt;&#xA;&lt;p&gt;如果每次給 AI 或 provider 的 context 都不一樣, target 很難穩定&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;write-target&#34;&gt;&lt;span&gt;write target&lt;/span&gt;&#xA;  &lt;a href=&#34;#write-target&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;write target&lt;/code&gt; 是產生草稿&lt;/p&gt;&#xA;&lt;p&gt;草稿可以來自人, AI model, 或 provider-backed workflow&lt;/p&gt;&#xA;&lt;p&gt;但 target file 存在不代表完成&lt;/p&gt;&#xA;&lt;p&gt;草稿永遠只是草稿&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;review&#34;&gt;&lt;span&gt;review&lt;/span&gt;&#xA;  &lt;a href=&#34;#review&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;review&lt;/code&gt; 是整條 workflow 最重要的一步&lt;/p&gt;&#xA;&lt;p&gt;它不能只看英文順不順或文法對不對&lt;/p&gt;&#xA;&lt;p&gt;它要檢查:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;heading 有沒有對齊&lt;/li&gt;&#xA;&lt;li&gt;blockquote 和 table 還在不在&lt;/li&gt;&#xA;&lt;li&gt;code block 有沒有被改&lt;/li&gt;&#xA;&lt;li&gt;technical inline literal 有沒有 drift&lt;/li&gt;&#xA;&lt;li&gt;應該翻譯的地方還有沒有 source language&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;對知識管理內容來說, 這種 review 比單純看文法更有用&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;sync-status&#34;&gt;&lt;span&gt;sync-status&lt;/span&gt;&#xA;  &lt;a href=&#34;#sync-status&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;sync-status&lt;/code&gt; 會把 completion 寫成正式狀態&lt;/p&gt;&#xA;&lt;p&gt;沒有這一步, queue 很難分辨:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;只是有人改過的草稿&lt;/li&gt;&#xA;&lt;li&gt;還是真的 review 過, 且仍然跟 source 同步的 translation&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;queue-state&#34;&gt;&lt;span&gt;Queue state&lt;/span&gt;&#xA;  &lt;a href=&#34;#queue-state&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Queue state 讓這個工具開始不像一堆 command, 而比較像一個 system&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;content-i18n&lt;/code&gt; 用三種 state:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;completed&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;stale&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;missing&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;這些 state 不是手動標記, 而是從這些資料推導:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;source discovery&lt;/li&gt;&#xA;&lt;li&gt;expected target file&lt;/li&gt;&#xA;&lt;li&gt;source hash&lt;/li&gt;&#xA;&lt;li&gt;translation status&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;翻譯不是一次性任務&lt;/p&gt;&#xA;&lt;p&gt;今天 complete 的 target, 只要 source 改了, 明天就可能 stale&lt;/p&gt;&#xA;&lt;p&gt;我的 Blog 文章本來就會一直修: 改句子, 補段落, 調例子, 修結構&lt;/p&gt;&#xA;&lt;p&gt;所以 workflow 不能只問有沒有翻過&lt;/p&gt;&#xA;&lt;p&gt;它還要問現在還有沒有 match source&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;completion-rule&#34;&gt;&lt;span&gt;Completion rule&lt;/span&gt;&#xA;  &lt;a href=&#34;#completion-rule&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Completion 不能模糊&lt;/p&gt;&#xA;&lt;p&gt;我最後把規則定成這樣:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;review 或 validation 要 pass&lt;/li&gt;&#xA;&lt;li&gt;應該翻譯的地方不能殘留 source language&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;sync-status&lt;/code&gt; 要成功&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;這比 &amp;ldquo;看起來差不多&amp;rdquo; 窄很多&lt;/p&gt;&#xA;&lt;p&gt;我常遇到 structure 看起來沒問題, 但 heading, metadata, inline note 裡還殘留沒翻完整的內容&lt;/p&gt;&#xA;&lt;p&gt;這種東西如果不擋, 很容易混進正式內容&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;sync-status&lt;/code&gt; 的價值就在這裡&lt;/p&gt;&#xA;&lt;p&gt;它把 completion 從主觀感覺, 變成系統可以推導和追蹤的狀態&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;為什麼做成-standalone-tool&#34;&gt;&lt;span&gt;為什麼做成 standalone tool&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%82%ba%e4%bb%80%e9%ba%bc%e5%81%9a%e6%88%90-standalone-tool&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;如果只是翻譯 Blog, 幾支 script 也能用&lt;/p&gt;&#xA;&lt;p&gt;但後來我需要這些東西:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;queue model&lt;/li&gt;&#xA;&lt;li&gt;provider option&lt;/li&gt;&#xA;&lt;li&gt;repeatable prepare / review / sync step&lt;/li&gt;&#xA;&lt;li&gt;stricter validation&lt;/li&gt;&#xA;&lt;li&gt;MCP interface 給 agent 用&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;這些需求一多, script 就不夠清楚&lt;/p&gt;&#xA;&lt;p&gt;做成 standalone tool 之後, boundary 比較乾淨:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;config 留在 consumer repo&lt;/li&gt;&#xA;&lt;li&gt;provider secret 留在 repo 外&lt;/li&gt;&#xA;&lt;li&gt;workflow 邏輯留在工具裡&lt;/li&gt;&#xA;&lt;li&gt;site routing 和 theme 不跟 translation engine 混在一起&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;這樣它才比較像可以長期維護的工具, 不是只在 Blog 裡湊合著用的 script&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;最後整理&#34;&gt;&lt;span&gt;最後整理&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e6%9c%80%e5%be%8c%e6%95%b4%e7%90%86&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;content-i18n&lt;/code&gt; 一開始要解的問題很單純: 讓 Blog 從 runtime translation, 變成真的有英文內容可以維護&lt;/p&gt;&#xA;&lt;p&gt;但實作後, 問題變成:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如何保持同一篇文章&lt;/li&gt;&#xA;&lt;li&gt;哪些東西必須被保住&lt;/li&gt;&#xA;&lt;li&gt;什麼才算真的完成&lt;/li&gt;&#xA;&lt;li&gt;queue 要怎麼追蹤 translation state&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;最後我需要的不是一次產生英文稿, 而是一套可以長期維護英文內容的 workflow&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/loustack17/content-i18n&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;content-i18n GitHub Repository&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://modelcontextprotocol.io/docs/getting-started/intro&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Model Context Protocol — What is MCP?&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://modelcontextprotocol.io/docs/learn/architecture&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Model Context Protocol — Architecture Overview&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://developers.deepl.com/docs/resources/supported-languages&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;DeepL Documentation — Supported Languages&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.github.com/actions/learn-github-actions/reusing-workflows&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GitHub Docs — Reusing Workflows&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;</description>
    </item>
    <item>
      <title>ArgoCD GitOps 實作：從安裝到完整 CD 流程</title>
      <link>https://loustack.dev/zh-tw/argocd-gitops-k3s/</link>
      <pubDate>Tue, 21 Apr 2026 21:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/argocd-gitops-k3s/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/devops/">DevOps</category>
      <category domain="https://loustack.dev/zh-tw/categories/gitops/">GitOps</category>
      <description>&lt;p&gt;這篇記錄把 ArgoCD 裝到 k3s cluster, 建立 ArgoCD Application, 讓整個 CD 流程從「GitHub Actions 直接 kubectl apply」換成「git 是 source of truth, ArgoCD 負責 sync」。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;push-based-vs-pull-based-cd&#34;&gt;&lt;span&gt;Push-based vs Pull-based CD&lt;/span&gt;&#xA;  &lt;a href=&#34;#push-based-vs-pull-based-cd&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;flowchart TD&#xA;    subgraph Push-based&#xA;        P1[Developer push] --&amp;gt; P2[GitHub Actions]&#xA;        P2 --&amp;gt;|kubectl apply &amp;#43; K8s credentials| P3[K8s Cluster]&#xA;    end&#xA;&#xA;    subgraph Pull-based GitOps&#xA;        G1[Developer push] --&amp;gt; G2[GitHub Actions]&#xA;        G2 --&amp;gt;|push image &amp;#43; update YAML| G3[Git Repo]&#xA;        G3 --&amp;gt;|ArgoCD polls every 3min| G4[ArgoCD in Cluster]&#xA;        G4 --&amp;gt;|kubectl apply internal| G5[K8s Cluster]&#xA;    end&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;Push-based&lt;/th&gt;&#xA;          &lt;th&gt;Pull-based (ArgoCD)&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;CI 需要 K8s 權限&lt;/td&gt;&#xA;          &lt;td&gt;是&lt;/td&gt;&#xA;          &lt;td&gt;否&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Drift 偵測&lt;/td&gt;&#xA;          &lt;td&gt;無&lt;/td&gt;&#xA;          &lt;td&gt;自動偵測並修正&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Rollback&lt;/td&gt;&#xA;          &lt;td&gt;手動跑舊 workflow&lt;/td&gt;&#xA;          &lt;td&gt;UI 一鍵或 git revert&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;可視性&lt;/td&gt;&#xA;          &lt;td&gt;CI log&lt;/td&gt;&#xA;          &lt;td&gt;ArgoCD UI 完整 sync 狀態&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Push-based 的根本問題是安全邊界模糊——CI runner 持有 K8s credentials, 一旦 credentials 外洩, 攻擊者可以直接操作 cluster。Pull-based 把控制權留在 cluster 內部, CI 只需要寫 git 的權限。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;argocd-架構&#34;&gt;&lt;span&gt;ArgoCD 架構&lt;/span&gt;&#xA;  &lt;a href=&#34;#argocd-%e6%9e%b6%e6%a7%8b&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;flowchart LR&#xA;    Git[&amp;#34;Git Repo&amp;lt;br/&amp;gt;k8s/base/&amp;#34;] --&amp;gt;|clone &amp;#43; render| RS[argocd-repo-server]&#xA;    RS --&amp;gt;|desired state| AC[argocd-application-controller]&#xA;    AC &amp;lt;--&amp;gt;|compare| K8s[&amp;#34;k3s Cluster&amp;lt;br/&amp;gt;actual state&amp;#34;]&#xA;    AC --&amp;gt;|diff found: sync| K8s&#xA;    Redis[&amp;#34;argocd-redis&amp;lt;br/&amp;gt;cache&amp;#34;] --- AC&#xA;    Server[&amp;#34;argocd-server&amp;lt;br/&amp;gt;UI &amp;#43; API&amp;#34;] --- AC&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;各組件的職責：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;組件&lt;/th&gt;&#xA;          &lt;th&gt;職責&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;argocd-server&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;UI + API, 操作入口&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;argocd-repo-server&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;clone git repo, render YAML（Helm/Kustomize/plain）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;argocd-application-controller&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;核心, 每 3 分鐘比對 desired vs actual state&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;argocd-applicationset-controller&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;批量管理多個 Application&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;argocd-dex-server&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;SSO 認證（GitHub OAuth、LDAP）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;argocd-redis&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;cache application state, 加速比對&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;argocd-notifications-controller&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;發通知（Slack、email）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Render YAML 是指 ArgoCD 把 Helm template 或 Kustomize overlay 展開成合法的 K8s YAML。Plain YAML 不需要 render, 所見即所得。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;安裝-argocd&#34;&gt;&lt;span&gt;安裝 ArgoCD&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%ae%89%e8%a3%9d-argocd&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;kubectl create namespace argocd&#xA;kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml&#xA;kubectl get pods -n argocd -w&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;等所有 pod Running 後存取 UI：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;kubectl port-forward svc/argocd-server -n argocd 8080:443&#xA;&#xA;kubectl -n argocd get secret argocd-initial-admin-secret \&#xA;  -o jsonpath=&amp;#34;{.data.password}&amp;#34; | base64 -d &amp;amp;&amp;amp; echo&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;瀏覽器開 &lt;code&gt;https://localhost:8080&lt;/code&gt;, 帳號 &lt;code&gt;admin&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;k8s-目錄結構設計&#34;&gt;&lt;span&gt;k8s 目錄結構設計&lt;/span&gt;&#xA;  &lt;a href=&#34;#k8s-%e7%9b%ae%e9%8c%84%e7%b5%90%e6%a7%8b%e8%a8%ad%e8%a8%88&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;ArgoCD 監控的路徑只能包含要被管理的 YAML。練習用的 test 資源和 production 資源混在同一個目錄, ArgoCD 無法區分：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;k8s/&#xA;  base/            &amp;lt;- ArgoCD managed&#xA;    deployment.yaml&#xA;    service.yaml&#xA;    configmap.yaml&#xA;    hpa.yaml&#xA;    secret.yaml&#xA;  argocd/          &amp;lt;- Application manifest&#xA;    application.yaml&#xA;  test/            &amp;lt;- excluded from ArgoCD&#xA;    test-namespace.yaml&#xA;    test-liveness.yaml&#xA;    test-probe.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;base/&lt;/code&gt; 是 ArgoCD 的 source of truth。&lt;code&gt;test/&lt;/code&gt; 完全排除在 GitOps 流程之外。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;application-manifest-as-code&#34;&gt;&lt;span&gt;Application Manifest as Code&lt;/span&gt;&#xA;  &lt;a href=&#34;#application-manifest-as-code&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;ArgoCD Application 應該在 git 裡, 不是只在 UI 點出來。UI 建立的設定沒有版本控制, 違反 GitOps 原則：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# k8s/argocd/application.yaml&#xA;apiVersion: argoproj.io/v1alpha1&#xA;kind: Application&#xA;metadata:&#xA;  name: go-api&#xA;  namespace: argocd&#xA;spec:&#xA;  project: default&#xA;  source:&#xA;    repoURL: https://github.com/your-org/your-repo&#xA;    targetRevision: HEAD&#xA;    path: k8s/base&#xA;  destination:&#xA;    server: https://kubernetes.default.svc&#xA;    namespace: default&#xA;  syncPolicy:&#xA;    automated:&#xA;      prune: true&#xA;      selfHeal: true&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Field&lt;/th&gt;&#xA;          &lt;th&gt;意義&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;namespace: argocd&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Application 本身住在 argocd namespace, 固定&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;project: default&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;ArgoCD Project, 用來做 RBAC 和資源隔離&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;targetRevision: HEAD&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;永遠跟最新 commit&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;path: k8s/base&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;監控這個目錄下的所有 YAML&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;server: kubernetes.default.svc&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;ArgoCD 所在的 cluster 本身&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;automated.prune: true&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;git 裡刪掉的資源, cluster 上也刪&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;automated.selfHeal: true&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;有人手動改了 cluster, 自動改回 git 的狀態&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;pre&gt;&lt;code&gt;kubectl apply -f k8s/argocd/application.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果 Application 已經從 UI 建立, &lt;code&gt;kubectl apply&lt;/code&gt; 會更新現有的, 以 YAML 為準。之後 UI 只用來觀察。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;imagepullsecret讓-k3s-pull-artifact-registry&#34;&gt;&lt;span&gt;imagePullSecret：讓 k3s pull Artifact Registry&lt;/span&gt;&#xA;  &lt;a href=&#34;#imagepullsecret%e8%ae%93-k3s-pull-artifact-registry&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;k3s 是本機 cluster, 沒有 GCP Workload Identity, 需要明確的 credentials 才能 pull private registry 的 image。&lt;/p&gt;&#xA;&lt;p&gt;用 Service Account key 建立不會過期的 imagePullSecret：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;gcloud iam service-accounts keys create /tmp/ar-pull-key.json \&#xA;  --iam-account=YOUR_SA@YOUR_PROJECT.iam.gserviceaccount.com&#xA;&#xA;kubectl create secret docker-registry ar-secret \&#xA;  --docker-server=us-east1-docker.pkg.dev \&#xA;  --docker-username=_json_key \&#xA;  --docker-password=&amp;#34;$(cat /tmp/ar-pull-key.json)&amp;#34; \&#xA;  --namespace=default&#xA;&#xA;rm /tmp/ar-pull-key.json&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SA key 建完就刪——K8s Secret 裡已經有完整內容, 本機留著是不必要的風險。&lt;/p&gt;&#xA;&lt;p&gt;在 &lt;code&gt;deployment.yaml&lt;/code&gt; 加上 &lt;code&gt;imagePullSecrets&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;spec:&#xA;  imagePullSecrets:&#xA;    - name: ar-secret&#xA;  containers:&#xA;    - name: go-api&#xA;      image: us-east1-docker.pkg.dev/YOUR_PROJECT/go-api/go-api:prod-7639a24&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;GKE 上不需要這個, node 透過 Workload Identity 天生有 AR pull 權限。&lt;code&gt;ar-secret&lt;/code&gt; 是 k3s 本機環境的限制。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;完整-gitops-流程&#34;&gt;&lt;span&gt;完整 GitOps 流程&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%ae%8c%e6%95%b4-gitops-%e6%b5%81%e7%a8%8b&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;sequenceDiagram&#xA;    participant Dev as Developer&#xA;    participant GH as GitHub Actions&#xA;    participant AR as Artifact Registry&#xA;    participant Git as Git Repo&#xA;    participant Argo as ArgoCD&#xA;    participant K8s as k3s Cluster&#xA;&#xA;    Dev-&amp;gt;&amp;gt;GH: push to main&#xA;    GH-&amp;gt;&amp;gt;GH: go test &amp;#43; go build&#xA;    GH-&amp;gt;&amp;gt;AR: docker push prod-{sha}&#xA;    GH-&amp;gt;&amp;gt;Git: update deployment.yaml image tag&#xA;    GH-&amp;gt;&amp;gt;Git: push gitops/update-image-{sha} branch&#xA;    Dev-&amp;gt;&amp;gt;Git: merge PR to main&#xA;    loop every 3 minutes&#xA;        Argo-&amp;gt;&amp;gt;Git: poll for changes&#xA;        Git--&amp;gt;&amp;gt;Argo: deployment.yaml changed&#xA;    end&#xA;    Argo-&amp;gt;&amp;gt;K8s: kubectl apply k8s/base/&#xA;    K8s-&amp;gt;&amp;gt;K8s: rolling update&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;為什麼 CI 不能直接 push main：main 有 branch protection rule, 要求 PR。CI 用 gitops branch 繞過這個限制, 同時保留 code review 流程。&lt;/p&gt;&#xA;&lt;p&gt;Drift detection：有人手動 &lt;code&gt;kubectl edit deployment go-api&lt;/code&gt; 改了 replicas, ArgoCD 的 selfHeal 會在下次輪詢時自動改回 git 的值。cluster 的狀態永遠由 git 決定。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;gke-vs-k3s&#34;&gt;&lt;span&gt;GKE vs k3s&lt;/span&gt;&#xA;  &lt;a href=&#34;#gke-vs-k3s&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;k3s（本機）&lt;/th&gt;&#xA;          &lt;th&gt;GKE&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;imagePullSecret&lt;/td&gt;&#xA;          &lt;td&gt;需要手動建&lt;/td&gt;&#xA;          &lt;td&gt;不需要, node 有 Workload Identity&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Load Balancer&lt;/td&gt;&#xA;          &lt;td&gt;無, 需要 NodePort&lt;/td&gt;&#xA;          &lt;td&gt;原生支援 L4/L7 LB&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Workload Identity&lt;/td&gt;&#xA;          &lt;td&gt;不支援&lt;/td&gt;&#xA;          &lt;td&gt;原生支援&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;費用&lt;/td&gt;&#xA;          &lt;td&gt;免費&lt;/td&gt;&#xA;          &lt;td&gt;e2-small ~$38/80 天&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;k3s 適合本機學習環境, GKE 更貼近 production。下一個 Phase 會在 GKE 上做 Prometheus + Grafana, 順便移除 &lt;code&gt;ar-secret&lt;/code&gt; 的限制。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://argo-cd.readthedocs.io/en/stable/getting_started/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;ArgoCD Getting Started&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://argo-cd.readthedocs.io/en/stable/operator-manual/application.yaml/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;ArgoCD Application Specification&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://argo-cd.readthedocs.io/en/stable/user-guide/auto_sync/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;ArgoCD Auto Sync&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://opengitops.dev/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GitOps Principles&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GCP Workload Identity for GKE&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes imagePullSecrets&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/artifact-registry/docs/docker/authentication&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Artifact Registry Authentication&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item>
    <item>
      <title>GitHub Actions 進階：Path Filter、Concurrency、Cache 與 GitOps 自動更新</title>
      <link>https://loustack.dev/zh-tw/github-actions-advanced-cicd/</link>
      <pubDate>Mon, 20 Apr 2026 21:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/github-actions-advanced-cicd/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/devops/">DevOps</category>
      <category domain="https://loustack.dev/zh-tw/categories/ci/cd/">CI/CD</category>
      <description>&lt;p&gt;這篇記錄把 GitHub Actions pipeline 從基礎補到可用: trigger 設計、path filter、concurrency、cache, 到 CI 自動更新 &lt;code&gt;deployment.yaml&lt;/code&gt; 完成 GitOps 閉環。最後對比 Jenkins 和 CircleCI。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;trigger-設計&#34;&gt;&lt;span&gt;Trigger 設計&lt;/span&gt;&#xA;  &lt;a href=&#34;#trigger-%e8%a8%ad%e8%a8%88&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;三種 trigger 各自的用途：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;on:&#xA;  push:&#xA;    branches: [main]&#xA;  pull_request:&#xA;    branches: [main]&#xA;  workflow_dispatch:&#xA;    inputs:&#xA;      environment:&#xA;        description: &amp;#39;Target environment&amp;#39;&#xA;        required: true&#xA;        default: &amp;#39;dev&amp;#39;&#xA;        type: choice&#xA;        options: [dev, prod]&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Trigger&lt;/th&gt;&#xA;          &lt;th&gt;觸發時機&lt;/th&gt;&#xA;          &lt;th&gt;用途&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;push&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;merge 到 main&lt;/td&gt;&#xA;          &lt;td&gt;主線 CI, 每次合併後跑&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pull_request&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;PR 開啟或更新&lt;/td&gt;&#xA;          &lt;td&gt;review 前驗證, 保護 main&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;手動從 GitHub UI&lt;/td&gt;&#xA;          &lt;td&gt;hotfix 補跑、手動指定環境 deploy&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt; 加了 &lt;code&gt;inputs&lt;/code&gt; 之後, GitHub UI 會出現下拉選單, 讓你在觸發時選擇環境。&lt;code&gt;inputs&lt;/code&gt; 支援 &lt;code&gt;string&lt;/code&gt;、&lt;code&gt;boolean&lt;/code&gt;、&lt;code&gt;number&lt;/code&gt;、&lt;code&gt;choice&lt;/code&gt; 四種類型。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;path-filter只在相關檔案變動時觸發&#34;&gt;&lt;span&gt;Path Filter：只在相關檔案變動時觸發&lt;/span&gt;&#xA;  &lt;a href=&#34;#path-filter%e5%8f%aa%e5%9c%a8%e7%9b%b8%e9%97%9c%e6%aa%94%e6%a1%88%e8%ae%8a%e5%8b%95%e6%99%82%e8%a7%b8%e7%99%bc&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;push README 不應該觸發 CI。&lt;code&gt;paths&lt;/code&gt; filter 讓 workflow 只在有意義的檔案變動時才跑：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;on:&#xA;  push:&#xA;    branches: [main]&#xA;    paths:&#xA;      - &amp;#39;**.go&amp;#39;&#xA;      - &amp;#39;Dockerfile&amp;#39;&#xA;      - &amp;#39;go.mod&amp;#39;&#xA;      - &amp;#39;go.sum&amp;#39;&#xA;      - &amp;#39;.github/workflows/**&amp;#39;&#xA;  pull_request:&#xA;    branches: [main]&#xA;    paths:&#xA;      - &amp;#39;**.go&amp;#39;&#xA;      - &amp;#39;Dockerfile&amp;#39;&#xA;      - &amp;#39;go.mod&amp;#39;&#xA;      - &amp;#39;go.sum&amp;#39;&#xA;      - &amp;#39;.github/workflows/**&amp;#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;**&lt;/code&gt; 匹配任意深度的子目錄, &lt;code&gt;*&lt;/code&gt; 只匹配單層。&lt;code&gt;**.go&lt;/code&gt; 能匹配 &lt;code&gt;handlers/health.go&lt;/code&gt;、&lt;code&gt;middleware/auth.go&lt;/code&gt;, 而 &lt;code&gt;*.go&lt;/code&gt; 只匹配根目錄的 &lt;code&gt;.go&lt;/code&gt; 檔。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt; 不需要 &lt;code&gt;paths&lt;/code&gt;, 手動觸發永遠跑。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;if-條件控制-job-和-step-執行&#34;&gt;&lt;span&gt;&lt;code&gt;if&lt;/code&gt; 條件：控制 Job 和 Step 執行&lt;/span&gt;&#xA;  &lt;a href=&#34;#if-%e6%a2%9d%e4%bb%b6%e6%8e%a7%e5%88%b6-job-%e5%92%8c-step-%e5%9f%b7%e8%a1%8c&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;PR 的目的是 review, 不是上線。deploy job 不應該在 PR 觸發：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;deploy:&#xA;  needs: build&#xA;  if: github.event_name == &amp;#39;push&amp;#39; || github.event_name == &amp;#39;workflow_dispatch&amp;#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;github.event_name&lt;/code&gt; 是 GitHub Actions 內建的 context, 值對應觸發的 trigger 名稱。&lt;code&gt;pull_request&lt;/code&gt; 被排除在外, test 和 build 還是會跑, 確認 PR 可以合。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;if&lt;/code&gt; 可以放在兩個層級：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;層級&lt;/th&gt;&#xA;          &lt;th&gt;效果&lt;/th&gt;&#xA;          &lt;th&gt;範例&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;job 層級&lt;/td&gt;&#xA;          &lt;td&gt;整個 job 跳過或執行&lt;/td&gt;&#xA;          &lt;td&gt;deploy 只在 push 跑&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;step 層級&lt;/td&gt;&#xA;          &lt;td&gt;單一 step 跳過或執行&lt;/td&gt;&#xA;          &lt;td&gt;prod 環境才跑某個 step&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;step 層級常見的內建函式：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;if: success()    # all previous steps passed (default)&#xA;if: failure()    # any previous step failed&#xA;if: always()     # runs regardless — cleanup, notifications&#xA;if: cancelled()  # workflow was cancelled&lt;/code&gt;&lt;/pre&gt;&lt;h2 class=&#34;heading-element&#34; id=&#34;concurrency防止同時跑多個-deploy&#34;&gt;&lt;span&gt;Concurrency：防止同時跑多個 Deploy&lt;/span&gt;&#xA;  &lt;a href=&#34;#concurrency%e9%98%b2%e6%ad%a2%e5%90%8c%e6%99%82%e8%b7%91%e5%a4%9a%e5%80%8b-deploy&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;sequenceDiagram&#xA;    participant C1 as Commit A&#xA;    participant C2 as Commit B&#xA;    participant D as Deploy Job&#xA;&#xA;    C1-&amp;gt;&amp;gt;D: trigger deploy (3 min)&#xA;    C2-&amp;gt;&amp;gt;D: trigger deploy (30s later)&#xA;    Note over D: cancel-in-progress: true&amp;lt;br/&amp;gt;cancel A, run B&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;concurrency&lt;/code&gt; 讓同一組的 job 不能同時跑：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;deploy:&#xA;  concurrency:&#xA;    group: deploy-${{ github.ref }}&#xA;    cancel-in-progress: true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;group&lt;/code&gt; 是識別 key, &lt;code&gt;github.ref&lt;/code&gt; 是 branch 名稱, 讓 main branch 的 deploy 共用同一個 lock。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;&lt;code&gt;cancel-in-progress: true&lt;/code&gt;&lt;/th&gt;&#xA;          &lt;th&gt;&lt;code&gt;cancel-in-progress: false&lt;/code&gt;&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;行為&lt;/td&gt;&#xA;          &lt;td&gt;新的進來, 取消舊的&lt;/td&gt;&#xA;          &lt;td&gt;新的排隊等&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;適合&lt;/td&gt;&#xA;          &lt;td&gt;deploy（要最新版本）&lt;/td&gt;&#xA;          &lt;td&gt;database migration（不能中斷）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;concurrency 只放在 deploy job, 不放 test 和 build——每個 commit 都需要完整的測試記錄。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;go-module-cache加速-ci&#34;&gt;&lt;span&gt;Go Module Cache：加速 CI&lt;/span&gt;&#xA;  &lt;a href=&#34;#go-module-cache%e5%8a%a0%e9%80%9f-ci&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;每次 runner 是全新環境, 所有 dependencies 重新下載。&lt;code&gt;go.sum&lt;/code&gt; 沒變, 重新下載完全是浪費。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;actions/setup-go&lt;/code&gt; v4 之後內建 cache：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;- uses: actions/setup-go@v6&#xA;  with:&#xA;    go-version-file: go.mod&#xA;    cache: true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;cache: true&lt;/code&gt; 自動以 &lt;code&gt;runner.os + go.sum hash&lt;/code&gt; 為 key。&lt;code&gt;go.sum&lt;/code&gt; 沒變就命中 cache 跳過下載。&lt;/p&gt;&#xA;&lt;p&gt;key 設計原則：&lt;strong&gt;把決定 cache 內容的檔案 hash 進 key&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;情境&lt;/th&gt;&#xA;          &lt;th&gt;Key 包含什麼&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Go&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;runner.os&lt;/code&gt; + &lt;code&gt;hashFiles(&#39;**/go.sum&#39;)&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Node.js&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;runner.os&lt;/code&gt; + &lt;code&gt;hashFiles(&#39;**/package-lock.json&#39;)&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Python&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;runner.os&lt;/code&gt; + &lt;code&gt;hashFiles(&#39;**/requirements.txt&#39;)&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;env_tag環境感知的-image-tag&#34;&gt;&lt;span&gt;ENV_TAG：環境感知的 Image Tag&lt;/span&gt;&#xA;  &lt;a href=&#34;#env_tag%e7%92%b0%e5%a2%83%e6%84%9f%e7%9f%a5%e7%9a%84-image-tag&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;flowchart LR&#xA;    A{trigger?} --&amp;gt;|workflow_dispatch| B[inputs.environment]&#xA;    A --&amp;gt;|push to main| C[prod]&#xA;    A --&amp;gt;|other branch| D[dev]&#xA;    B --&amp;gt; E[ENV_TAG]&#xA;    C --&amp;gt; E&#xA;    D --&amp;gt; E&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;env:&#xA;  ENV_TAG: ${{ inputs.environment || (github.ref == &amp;#39;refs/heads/main&amp;#39; &amp;amp;&amp;amp; &amp;#39;prod&amp;#39;) || &amp;#39;dev&amp;#39; }}&lt;/code&gt;&lt;/pre&gt;&lt;h2 class=&#34;heading-element&#34; id=&#34;short_sha跨-step-共用變數&#34;&gt;&lt;span&gt;SHORT_SHA：跨 Step 共用變數&lt;/span&gt;&#xA;  &lt;a href=&#34;#short_sha%e8%b7%a8-step-%e5%85%b1%e7%94%a8%e8%ae%8a%e6%95%b8&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;每個 &lt;code&gt;run:&lt;/code&gt; block 是獨立的 shell, 變數不能直接跨 step 傳遞。&lt;code&gt;GITHUB_ENV&lt;/code&gt; 是 GitHub Actions 的特殊檔案, 寫進去的變數在後續所有 step 都能讀到：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;- name: Set SHORT_SHA&#xA;  run: echo &amp;#34;SHORT_SHA=${GITHUB_SHA::7}&amp;#34; &amp;gt;&amp;gt; $GITHUB_ENV&#xA;&#xA;- name: Docker push to AR&#xA;  run: |&#xA;    docker build -t ${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/go-api/go-api:${ENV_TAG}-${SHORT_SHA} .&#xA;    docker push ${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/go-api/go-api:${ENV_TAG}-${SHORT_SHA}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;${GITHUB_SHA::7}&lt;/code&gt; 是 bash 字串截斷語法, 取前 7 字元。Docker push 和 deployment.yaml 更新都用同一個 &lt;code&gt;SHORT_SHA&lt;/code&gt;, 保證 tag 一致。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;gitops-自動更新-deploymentyaml&#34;&gt;&lt;span&gt;GitOps 自動更新 deployment.yaml&lt;/span&gt;&#xA;  &lt;a href=&#34;#gitops-%e8%87%aa%e5%8b%95%e6%9b%b4%e6%96%b0-deploymentyaml&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;CI build 完之後, 自動更新 &lt;code&gt;k8s/base/deployment.yaml&lt;/code&gt; 的 image tag, push 到 gitops branch, ArgoCD 偵測到變更後 sync：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;- name: Update image tag&#xA;  run: |&#xA;    sed -i &amp;#34;s|image: .*-docker.pkg.dev/.*/go-api/go-api:.*|image: us-east1-docker.pkg.dev/${GCP_PROJECT_ID}/go-api/go-api:${ENV_TAG}-${SHORT_SHA}|&amp;#34; k8s/base/deployment.yaml&#xA;    git config user.email &amp;#34;41898282&amp;#43;github-actions[bot]@users.noreply.github.com&amp;#34;&#xA;    git config user.name &amp;#34;github-actions[bot]&amp;#34;&#xA;    git checkout -b gitops/update-image-${SHORT_SHA}&#xA;    git add k8s/base/deployment.yaml&#xA;    git commit -m &amp;#34;chore: update image tag to ${ENV_TAG}-${SHORT_SHA}&amp;#34;&#xA;    git push origin gitops/update-image-${SHORT_SHA}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;sed&lt;/code&gt; 用 &lt;code&gt;|&lt;/code&gt; 而不是 &lt;code&gt;/&lt;/code&gt; 作為分隔符, 是因為 image URL 裡有 &lt;code&gt;/&lt;/code&gt;, 避免語法混淆。push 到獨立的 gitops branch 而不是直接 push main, 是因為 main 有 branch protection rule。&lt;code&gt;contents: write&lt;/code&gt; permission 讓 runner 有寫入 repo 的權限。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;image-tagginggit-sha-vs-semver&#34;&gt;&lt;span&gt;Image Tagging：git SHA vs Semver&lt;/span&gt;&#xA;  &lt;a href=&#34;#image-tagginggit-sha-vs-semver&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;Git SHA&lt;/th&gt;&#xA;          &lt;th&gt;Semver (&lt;code&gt;v1.2.3&lt;/code&gt;)&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;唯一性&lt;/td&gt;&#xA;          &lt;td&gt;天生唯一&lt;/td&gt;&#xA;          &lt;td&gt;需要人工維護&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;可追蹤性&lt;/td&gt;&#xA;          &lt;td&gt;直接對應 commit&lt;/td&gt;&#xA;          &lt;td&gt;需要額外記錄 tag 對應的 commit&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;語意&lt;/td&gt;&#xA;          &lt;td&gt;無&lt;/td&gt;&#xA;          &lt;td&gt;有, &lt;code&gt;v1.3.0&lt;/code&gt; vs &lt;code&gt;v1.2.1&lt;/code&gt; 一眼看出&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;適合&lt;/td&gt;&#xA;          &lt;td&gt;內部服務、CI/CD 自動化&lt;/td&gt;&#xA;          &lt;td&gt;對外 API、library&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;pipeline 用 &lt;code&gt;{env}-{sha}&lt;/code&gt; 格式（例如 &lt;code&gt;prod-7639a24&lt;/code&gt;）, 一眼看出環境和 commit。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;jenkins-vs-github-actions&#34;&gt;&lt;span&gt;Jenkins vs GitHub Actions&lt;/span&gt;&#xA;  &lt;a href=&#34;#jenkins-vs-github-actions&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;pipeline {&#xA;    agent any&#xA;    environment {&#xA;        GCP_REGION = &amp;#39;us-east1&amp;#39;&#xA;    }&#xA;    stages {&#xA;        stage(&amp;#39;Test&amp;#39;) {&#xA;            steps { sh &amp;#39;go test ./...&amp;#39; }&#xA;        }&#xA;        stage(&amp;#39;Build&amp;#39;) {&#xA;            steps { sh &amp;#39;go build ./...&amp;#39; }&#xA;        }&#xA;        stage(&amp;#39;Deploy&amp;#39;) {&#xA;            when { branch &amp;#39;main&amp;#39; }&#xA;            steps { sh &amp;#39;docker build -t go-api .&amp;#39; }&#xA;        }&#xA;    }&#xA;    post {&#xA;        failure { echo &amp;#39;Pipeline failed&amp;#39; }&#xA;        always { cleanWs() }&#xA;    }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;GitHub Actions&lt;/th&gt;&#xA;          &lt;th&gt;Jenkins&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;設定檔&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;.github/workflows/*.yml&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Jenkinsfile&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;語法&lt;/td&gt;&#xA;          &lt;td&gt;YAML&lt;/td&gt;&#xA;          &lt;td&gt;Groovy DSL&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Job 依賴&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;needs: [test]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;stage 預設循序執行&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Branch 限制&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;if: github.event_name == &#39;push&#39;&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;when { branch &#39;main&#39; }&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Cleanup&lt;/td&gt;&#xA;          &lt;td&gt;step-level &lt;code&gt;if: always()&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;post { always {} }&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;架設&lt;/td&gt;&#xA;          &lt;td&gt;雲端, 不用管 infra&lt;/td&gt;&#xA;          &lt;td&gt;需要自己維護 server&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;整合&lt;/td&gt;&#xA;          &lt;td&gt;GitHub 深度整合&lt;/td&gt;&#xA;          &lt;td&gt;plugin 生態豐富&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;適合&lt;/td&gt;&#xA;          &lt;td&gt;雲端原生、開源專案&lt;/td&gt;&#xA;          &lt;td&gt;企業、on-premise、複雜 pipeline&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;circleci-對比&#34;&gt;&lt;span&gt;CircleCI 對比&lt;/span&gt;&#xA;  &lt;a href=&#34;#circleci-%e5%b0%8d%e6%af%94&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;# .circleci/config.yml&#xA;version: 2.1&#xA;&#xA;orbs:&#xA;  go: circleci/go@1.11&#xA;&#xA;workflows:&#xA;  ci:&#xA;    jobs:&#xA;      - test&#xA;      - build:&#xA;          requires: [test]&#xA;      - deploy:&#xA;          requires: [build]&#xA;          context: gcp-prod&#xA;          filters:&#xA;            branches:&#xA;              only: main&#xA;&#xA;jobs:&#xA;  test:&#xA;    docker:&#xA;      - image: cimg/go:1.23&#xA;    steps:&#xA;      - checkout&#xA;      - go/load-cache&#xA;      - run: go test ./...&#xA;      - go/save-cache&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;概念&lt;/th&gt;&#xA;          &lt;th&gt;GitHub Actions&lt;/th&gt;&#xA;          &lt;th&gt;CircleCI&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Job 依賴&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;needs: [test]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;requires: [test]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Branch 限制&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;if:&lt;/code&gt; condition&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;filters: branches: only:&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;可重用套件&lt;/td&gt;&#xA;          &lt;td&gt;Marketplace actions&lt;/td&gt;&#xA;          &lt;td&gt;Orbs&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;跨 repo 共用 credentials&lt;/td&gt;&#xA;          &lt;td&gt;每個 repo 各設 secrets&lt;/td&gt;&#xA;          &lt;td&gt;Contexts（org 層級共用）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;手動觸發 + 參數&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt; + UI&lt;/td&gt;&#xA;          &lt;td&gt;API call + &lt;code&gt;pipeline.parameters&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;CircleCI 的核心優勢是 Contexts 跨 repo 共用——多個 repo 指向同一個 Context, credentials 集中管理, 改一次全部生效。這個優勢在中大型企業多 repo 環境才有明顯差異。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GitHub Actions Workflow Syntax&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.github.com/en/actions/learn-github-actions/contexts&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GitHub Actions Contexts&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GitHub Actions GITHUB_ENV&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/actions/setup-go#caching-dependency-files-and-build-outputs&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;actions/setup-go cache&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.jenkins.io/doc/book/pipeline/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Jenkins Pipeline Documentation&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://circleci.com/docs/configuration-reference/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;CircleCI Configuration Reference&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://circleci.com/docs/contexts/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;CircleCI Contexts&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://semver.org/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Semantic Versioning&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item>
    <item>
      <title>Terraform Bootstrap 層設計: WIF、Artifact Registry 與 GitHub Actions CI/CD</title>
      <link>https://loustack.dev/zh-tw/opentofu-bootstrap-wif-artifact-registry-cicd/</link>
      <pubDate>Thu, 16 Apr 2026 21:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/opentofu-bootstrap-wif-artifact-registry-cicd/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/devops/">DevOps</category>
      <category domain="https://loustack.dev/zh-tw/categories/iac/">IaC</category>
      <description>&lt;p&gt;把 GitHub Actions 接到 GCP 推 Docker image, 最常見的做法是建一個 Service Account, 下載 key JSON, 存進 GitHub Secrets。這個方法能跑, 但 key 是長期憑證, 洩漏就完了。&lt;/p&gt;&#xA;&lt;p&gt;這篇整理我用 Terraform 建出 bootstrap 層的過程: WIF 設定、Artifact Registry、Service Account 最小權限, 以及 GitHub Actions workflow 怎麼把這些串起來, 完全不需要任何 SA key。我實際使用 OpenTofu (Terraform 的開源分支), 語法與 Terraform 完全相容。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;bootstrap-層-vs-environment-層&#34;&gt;&lt;span&gt;Bootstrap 層 vs Environment 層&lt;/span&gt;&#xA;  &lt;a href=&#34;#bootstrap-%e5%b1%a4-vs-environment-%e5%b1%a4&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Terraform 結構常見的分法是把資源按「建立頻率和共用程度」分層。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;terraform/&#xA;├── bootstrap/          # one-time setup, shared across environments&#xA;│   ├── main.tf&#xA;│   ├── variables.tf&#xA;│   ├── outputs.tf&#xA;│   └── backend.tf&#xA;├── environments/&#xA;│   └── dev/            # environment-specific resources&#xA;└── modules/&#xA;    └── vpc/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;bootstrap 放什麼&lt;/strong&gt;: 只建一次、很少改、整個 project 共用的資源。WIF pool 是 per-project 的, Artifact Registry 各環境共用同一個, Service Account 由 CI 統一使用。這些都適合放在 bootstrap。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;environments 放什麼&lt;/strong&gt;: VPC、Subnet、GKE cluster, 這些會隨 dev/prod 不同而有差異。&lt;/p&gt;&#xA;&lt;p&gt;bootstrap 的一個特殊性: 它自己的 state 存在哪裡? 如果 GCS bucket 已經存在, 直接用同一個 bucket 不同 prefix 即可。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# bootstrap/backend.tf&#xA;terraform {&#xA;  backend &amp;#34;gcs&amp;#34; {&#xA;    bucket = &amp;#34;&amp;lt;project-id&amp;gt;-tfstate&amp;#34;&#xA;    prefix = &amp;#34;bootstrap&amp;#34;&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 class=&#34;heading-element&#34; id=&#34;wif-的信任鏈&#34;&gt;&lt;span&gt;WIF 的信任鏈&lt;/span&gt;&#xA;  &lt;a href=&#34;#wif-%e7%9a%84%e4%bf%a1%e4%bb%bb%e9%8f%88&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;WIF 解決的問題是: GitHub Actions 怎麼向 GCP 證明「這個請求真的來自我的 repo」?&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;GitHub Actions executes&#xA;  -&amp;gt; GitHub issues a short-lived OIDC token (JWT)&#xA;     contains: iss, sub, repository, ref, actor, exp (minutes away)&#xA;          |&#xA;          v&#xA;GCP Workload Identity Pool&#xA;  -&amp;gt; validates: is iss from GitHub?&#xA;  -&amp;gt; checks attribute_condition: is repository == allowed repo?&#xA;          |&#xA;          v&#xA;impersonate Service Account&#xA;  -&amp;gt; SA has only artifactregistry.writer&#xA;  -&amp;gt; token expires in minutes, no key ever exists&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;沒有任何 key 存在任何地方。就算 token 被攔截, 幾分鐘後就過期。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;terraform-設定&#34;&gt;&lt;span&gt;Terraform 設定&lt;/span&gt;&#xA;  &lt;a href=&#34;#terraform-%e8%a8%ad%e5%ae%9a&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;需要啟用的-gcp-api&#34;&gt;&lt;span&gt;需要啟用的 GCP API&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e9%9c%80%e8%a6%81%e5%95%9f%e7%94%a8%e7%9a%84-gcp-api&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;這是最容易忘記的部分。GCP 每個服務預設是關閉的, 要明確啟用:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;resource &amp;#34;google_project_service&amp;#34; &amp;#34;artifact_registry&amp;#34; {&#xA;  service = &amp;#34;artifactregistry.googleapis.com&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;google_project_service&amp;#34; &amp;#34;iam_credentials&amp;#34; {&#xA;  service = &amp;#34;iamcredentials.googleapis.com&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;iamcredentials.googleapis.com&lt;/code&gt; 是 WIF impersonate SA 需要的, 不開就會在 CI 跑到一半時出現 403。&lt;code&gt;depends_on&lt;/code&gt; 確保資源建立順序正確:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;resource &amp;#34;google_artifact_registry_repository&amp;#34; &amp;#34;go_api&amp;#34; {&#xA;  depends_on    = [google_project_service.artifact_registry]&#xA;  repository_id = &amp;#34;go-api&amp;#34;&#xA;  format        = &amp;#34;DOCKER&amp;#34;&#xA;  location      = var.region&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;depends_on&lt;/code&gt; 是 Terraform 的顯式依賴宣告。通常 Terraform 能從 resource 之間的引用自動推斷順序, 但這裡 &lt;code&gt;google_artifact_registry_repository&lt;/code&gt; 並沒有直接引用 &lt;code&gt;google_project_service&lt;/code&gt;, 所以需要手動告訴它「先等 API 啟用再建 repository」。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;service-account-與最小權限&#34;&gt;&lt;span&gt;Service Account 與最小權限&lt;/span&gt;&#xA;  &lt;a href=&#34;#service-account-%e8%88%87%e6%9c%80%e5%b0%8f%e6%ac%8a%e9%99%90&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;resource &amp;#34;google_service_account&amp;#34; &amp;#34;github_actions&amp;#34; {&#xA;  account_id   = &amp;#34;github-actions-ci&amp;#34;&#xA;  display_name = &amp;#34;GitHub Actions CI&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;google_project_iam_member&amp;#34; &amp;#34;go_devops&amp;#34; {&#xA;  project = var.project_id&#xA;  role    = &amp;#34;roles/artifactregistry.writer&amp;#34;&#xA;  member  = &amp;#34;serviceAccount:${google_service_account.github_actions.email}&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SA 只給 &lt;code&gt;roles/artifactregistry.writer&lt;/code&gt;, 不給 &lt;code&gt;Editor&lt;/code&gt; 或 &lt;code&gt;Owner&lt;/code&gt;。就算 CI pipeline 被入侵, 最多只能推 image, 不能碰其他 GCP 資源。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;wif-pool-與-provider&#34;&gt;&lt;span&gt;WIF Pool 與 Provider&lt;/span&gt;&#xA;  &lt;a href=&#34;#wif-pool-%e8%88%87-provider&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;resource &amp;#34;google_iam_workload_identity_pool&amp;#34; &amp;#34;github&amp;#34; {&#xA;  workload_identity_pool_id = &amp;#34;github-actions-pool&amp;#34;&#xA;  display_name              = &amp;#34;GitHub Actions Pool&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;google_iam_workload_identity_pool_provider&amp;#34; &amp;#34;github&amp;#34; {&#xA;  workload_identity_pool_id          = google_iam_workload_identity_pool.github.workload_identity_pool_id&#xA;  workload_identity_pool_provider_id = &amp;#34;github-actions-oidc&amp;#34;&#xA;  display_name                       = &amp;#34;GitHub Actions OIDC&amp;#34;&#xA;&#xA;  attribute_mapping = {&#xA;    &amp;#34;google.subject&amp;#34;       = &amp;#34;assertion.sub&amp;#34;&#xA;    &amp;#34;attribute.repository&amp;#34; = &amp;#34;assertion.repository&amp;#34;&#xA;  }&#xA;&#xA;  attribute_condition = &amp;#34;attribute.repository == &amp;#39;${var.github_repository}&amp;#39;&amp;#34;&#xA;&#xA;  oidc {&#xA;    issuer_uri = &amp;#34;https://token.actions.githubusercontent.com&amp;#34;&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;attribute_mapping&lt;/code&gt; 的兩側意思不同&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;左邊是你在 GCP 這側定義的名字 (&lt;code&gt;attribute.repository&lt;/code&gt;)&lt;/li&gt;&#xA;&lt;li&gt;右邊是 GitHub JWT 裡的原始 claim (&lt;code&gt;assertion.repository&lt;/code&gt;)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;code&gt;assertion&lt;/code&gt; 就是 GitHub 發出的 JWT token 內容。&lt;code&gt;attribute_mapping&lt;/code&gt; 把 JWT claims 映射成 GCP 可以在 condition 裡使用的屬性。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;attribute_condition&lt;/code&gt;&lt;/strong&gt; 限制只有指定的 repo 才能通過驗證。沒有這個 condition, 任何 GitHub repo 都可以用這個 WIF pool。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;sa-impersonation-binding&#34;&gt;&lt;span&gt;SA Impersonation Binding&lt;/span&gt;&#xA;  &lt;a href=&#34;#sa-impersonation-binding&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;resource &amp;#34;google_service_account_iam_member&amp;#34; &amp;#34;wif_devops&amp;#34; {&#xA;  service_account_id = google_service_account.github_actions.id&#xA;  role               = &amp;#34;roles/iam.workloadIdentityUser&amp;#34;&#xA;  member             = &amp;#34;principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/${var.github_repository}&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這個 binding 允許「來自指定 repo 的 WIF principal」去 impersonate 這個 SA。&lt;code&gt;principalSet&lt;/code&gt; 代表一組符合條件的 principal, 不是單一個。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;outputs&#34;&gt;&lt;span&gt;Outputs&lt;/span&gt;&#xA;  &lt;a href=&#34;#outputs&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;CI workflow 需要這兩個值:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;output &amp;#34;workload_identity_provider&amp;#34; {&#xA;  value = google_iam_workload_identity_pool_provider.github.name&#xA;}&#xA;&#xA;output &amp;#34;service_account_email&amp;#34; {&#xA;  value = google_service_account.github_actions.email&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;name&lt;/code&gt; 和 &lt;code&gt;email&lt;/code&gt; 是 Attributes Reference 裡的欄位, 是資源建立後 GCP 回傳的值, 不是你傳入的 argument。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;github-actions-workflow&#34;&gt;&lt;span&gt;GitHub Actions Workflow&lt;/span&gt;&#xA;  &lt;a href=&#34;#github-actions-workflow&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;bootstrap apply 完, 把兩個 output 值存進 GitHub repo 的 Variables (不是 Secrets, 這兩個不是機密):&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;GCP_WORKLOAD_IDENTITY_PROVIDER&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;GCP_SERVICE_ACCOUNT&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;pre&gt;&lt;code&gt;deploy:&#xA;  needs: build&#xA;  runs-on: ubuntu-latest&#xA;  permissions:&#xA;    contents: read&#xA;    id-token: write&#xA;  env:&#xA;    GCP_REGION: us-east1&#xA;    GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}&#xA;  steps:&#xA;    - uses: actions/checkout@v4&#xA;    - uses: google-github-actions/auth@v2&#xA;      with:&#xA;        workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}&#xA;        service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}&#xA;    - uses: google-github-actions/setup-gcloud@v2&#xA;    - name: Build and push to Artifact Registry&#xA;      run: |&#xA;        gcloud auth configure-docker ${GCP_REGION}-docker.pkg.dev --quiet&#xA;        docker build -t ${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/go-api/go-api:${{ github.sha }} .&#xA;        docker push ${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/go-api/go-api:${{ github.sha }}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;id-token: write&lt;/code&gt; 只開在需要 GCP 認證的 job, 不要在 workflow 頂層給。test 和 build job 不需要 GCP 認證, 就不需要這個權限。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;env&lt;/code&gt; 區塊定義的變數在 &lt;code&gt;run:&lt;/code&gt; 步驟裡用 &lt;code&gt;$VAR_NAME&lt;/code&gt; 取用, 不是 &lt;code&gt;${{ }}&lt;/code&gt; syntax。&lt;code&gt;${{ }}&lt;/code&gt; 是 GitHub Actions 的 expression syntax, 用來讀 &lt;code&gt;vars.&lt;/code&gt;、&lt;code&gt;secrets.&lt;/code&gt;、&lt;code&gt;github.&lt;/code&gt; 這些 context。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;arguments-vs-attributes&#34;&gt;&lt;span&gt;Arguments vs Attributes&lt;/span&gt;&#xA;  &lt;a href=&#34;#arguments-vs-attributes&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;這個概念在寫 Terraform 時很重要:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Arguments&lt;/strong&gt; (Argument Reference): 你傳入 resource 的值, 例如 &lt;code&gt;account_id&lt;/code&gt;、&lt;code&gt;display_name&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Attributes&lt;/strong&gt; (Attributes Reference): 資源建立後可以引用的值, 包含你傳入的和 GCP 自動產生的&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;code&gt;email&lt;/code&gt; 不在 &lt;code&gt;google_service_account&lt;/code&gt; 的 arguments 裡, 你不需要寫它。但 apply 完之後它就存在, 可以用 &lt;code&gt;google_service_account.github_actions.email&lt;/code&gt; 引用。查法: Terraform Registry 每個 resource 頁面底部的 Attributes Reference 段落。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://search.opentofu.org/provider/hashicorp/google/latest&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;OpenTofu Registry - google provider&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://registry.terraform.io/providers/hashicorp/google/latest/docs&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Terraform Registry - google provider&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/iam/docs/workload-identity-federation&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Workload Identity Federation&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Workload Identity Federation with deployment pipelines&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;About security hardening with OpenID Connect&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/google-github-actions/auth&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;google-github-actions/auth&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/iam/docs/understanding-roles&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GCP IAM Roles Reference&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://opentofu.org/docs/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;OpenTofu Documentation&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item>
    <item>
      <title>System Design 核心：Redis、Rate Limiting、Circuit Breaker 與 Scaling 策略</title>
      <link>https://loustack.dev/zh-tw/system-design-redis-rate-limiting-circuit-breaker-scaling/</link>
      <pubDate>Wed, 15 Apr 2026 21:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/system-design-redis-rate-limiting-circuit-breaker-scaling/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/systemdesign/">SystemDesign</category>
      <category domain="https://loustack.dev/zh-tw/categories/sre/">SRE</category>
      <description>&lt;p&gt;這篇整理 SRE 面試高頻的 System Design 核心概念: Redis 的資料結構與使用場景、四種 Rate Limiting 演算法、Circuit Breaker 的三個狀態, 以及 Scaling Reads 和 Scaling Writes 的常見設計模式。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;redis&#34;&gt;&lt;span&gt;Redis&lt;/span&gt;&#xA;  &lt;a href=&#34;#redis&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;為什麼需要-redis&#34;&gt;&lt;span&gt;為什麼需要 Redis&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%82%ba%e4%bb%80%e9%ba%bc%e9%9c%80%e8%a6%81-redis&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;資料庫每次讀取都要走磁碟 I/O, 延遲約 1-10ms。Redis 把資料存在記憶體, 延遲約 0.1ms。對於熱資料 (頻繁讀取、不常變動), cache 可以讓資料庫壓力降低幾個數量級。&lt;/p&gt;&#xA;&lt;p&gt;Redis 是 key-value store, 但不只是 cache — 它的資料結構讓它能勝任多種不同的角色。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;五種資料結構&#34;&gt;&lt;span&gt;五種資料結構&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%ba%94%e7%a8%ae%e8%b3%87%e6%96%99%e7%b5%90%e6%a7%8b&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;結構&lt;/th&gt;&#xA;          &lt;th&gt;類比&lt;/th&gt;&#xA;          &lt;th&gt;適合場景&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;String&lt;/td&gt;&#xA;          &lt;td&gt;變數&lt;/td&gt;&#xA;          &lt;td&gt;cache、session、計數器&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Hash&lt;/td&gt;&#xA;          &lt;td&gt;物件/dict&lt;/td&gt;&#xA;          &lt;td&gt;使用者資料、設定檔&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;List&lt;/td&gt;&#xA;          &lt;td&gt;雙向佇列&lt;/td&gt;&#xA;          &lt;td&gt;訊息佇列、最近活動紀錄&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Set&lt;/td&gt;&#xA;          &lt;td&gt;無序集合&lt;/td&gt;&#xA;          &lt;td&gt;標籤、好友清單、去重&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Sorted Set&lt;/td&gt;&#xA;          &lt;td&gt;有分數的集合&lt;/td&gt;&#xA;          &lt;td&gt;排行榜、優先隊列、時間序列&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;pre&gt;&lt;code&gt;String:&#xA;  SET user:123:name &amp;#34;Alice&amp;#34;&#xA;  GET user:123:name  -&amp;gt; &amp;#34;Alice&amp;#34;&#xA;  INCR user:123:loginCount  -&amp;gt; atomic increment&#xA;&#xA;Hash:&#xA;  HSET user:123 name &amp;#34;Alice&amp;#34; age 30 city &amp;#34;Toronto&amp;#34;&#xA;  HGET user:123 name  -&amp;gt; &amp;#34;Alice&amp;#34;&#xA;  HGETALL user:123    -&amp;gt; all fields&#xA;&#xA;List:&#xA;  RPUSH queue:tasks &amp;#34;job1&amp;#34; &amp;#34;job2&amp;#34;&#xA;  LPOP queue:tasks  -&amp;gt; &amp;#34;job1&amp;#34;  (FIFO)&#xA;&#xA;Set:&#xA;  SADD user:123:tags &amp;#34;golang&amp;#34; &amp;#34;devops&amp;#34;&#xA;  SMEMBERS user:123:tags  -&amp;gt; {&amp;#34;golang&amp;#34;, &amp;#34;devops&amp;#34;}&#xA;&#xA;Sorted Set:&#xA;  ZADD leaderboard 9500 &amp;#34;user:123&amp;#34;&#xA;  ZADD leaderboard 8200 &amp;#34;user:456&amp;#34;&#xA;  ZREVRANGE leaderboard 0 9 WITHSCORES  -&amp;gt; top 10 with scores&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;七個使用場景&#34;&gt;&lt;span&gt;七個使用場景&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%b8%83%e5%80%8b%e4%bd%bf%e7%94%a8%e5%a0%b4%e6%99%af&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Cache&lt;/strong&gt;: 最常見。讀資料庫後存入 Redis, 下次直接從 Redis 拿。TTL 控制過期。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Distributed Lock&lt;/strong&gt;: 多個服務搶同一資源時 (例如搶票), 用 &lt;code&gt;SET key value NX EX 300&lt;/code&gt; 建 lock。&lt;code&gt;NX&lt;/code&gt; = 只有 key 不存在時才設定 (原子操作), 避免兩個 instance 同時拿到 lock。&lt;code&gt;EX 300&lt;/code&gt; = 300 秒後自動釋放, 防止 lock 永遠不釋放。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Leaderboard&lt;/strong&gt;: Sorted Set 天生支援排行榜。&lt;code&gt;ZADD&lt;/code&gt; 加分數, &lt;code&gt;ZREVRANGE&lt;/code&gt; 取前 N 名, 時間複雜度 O(log N)。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Rate Limiting&lt;/strong&gt;: 用 &lt;code&gt;INCR&lt;/code&gt; 計數, 每個 key 代表一個時間窗口。跨多個 API Gateway instance 共享同一個 Redis, 做到全域限流。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Proximity Search&lt;/strong&gt;: &lt;code&gt;GEOADD&lt;/code&gt; 加入座標, &lt;code&gt;GEODIST&lt;/code&gt; 計算距離, &lt;code&gt;GEOSEARCH&lt;/code&gt; 找範圍內的點。適合外送、叫車、地圖搜尋。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Streams&lt;/strong&gt;: 類似 Kafka 的訊息流, 支援 consumer group、訊息持久化、重播。比 Pub/Sub 更可靠。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Pub/Sub&lt;/strong&gt;: 發布/訂閱模式, 即時推送通知。特性是 fire-and-forget — 訊息送出後不保證對方收到, 斷線期間的訊息會遺失。適合即時通知, 不適合需要保證送達的場景。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;hot-key-問題&#34;&gt;&lt;span&gt;Hot Key 問題&lt;/span&gt;&#xA;  &lt;a href=&#34;#hot-key-%e5%95%8f%e9%a1%8c&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Hot Key 是指被大量請求集中打到同一個 Redis key 的情況。例如明星發文, 幾百萬人同時讀同一個貼文的 cache key, 造成那個 key 所在的 Redis node 過載。&lt;/p&gt;&#xA;&lt;p&gt;三種解法:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;本地快取 (Local Cache)&lt;/strong&gt;: 在每個應用服務 instance 上存一份記憶體內 cache, 減少打到 Redis 的請求數量。缺點是各 instance 的 cache 可能不一致, 且不同 instance 占用的記憶體加總起來會大很多。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;副本 (Replica)&lt;/strong&gt;: 把 Hot Key 複製到多個 Redis 節點, 例如 &lt;code&gt;post:viral:123:replica1&lt;/code&gt;, &lt;code&gt;post:viral:123:replica2&lt;/code&gt;。讀取時隨機選一個副本, 分散壓力。缺點是需要額外的管理邏輯來維護副本的一致性。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Key 分散 (Key Sharding)&lt;/strong&gt;: 在 key 後面加隨機後綴 &lt;code&gt;post:viral:123:{0~9}&lt;/code&gt;, 寫入時同時寫到所有後綴, 讀取時隨機選一個。本質上是把 Hot Key 打散到多個物理 key, 利用 Redis Cluster 的不同 slot 分流。這是最根本的解法, 因為它直接把流量分散到不同節點, 而不只是減少請求數量。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;三種部署模式&#34;&gt;&lt;span&gt;三種部署模式&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%b8%89%e7%a8%ae%e9%83%a8%e7%bd%b2%e6%a8%a1%e5%bc%8f&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;模式&lt;/th&gt;&#xA;          &lt;th&gt;特性&lt;/th&gt;&#xA;          &lt;th&gt;適合&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Standalone&lt;/td&gt;&#xA;          &lt;td&gt;單節點, 簡單&lt;/td&gt;&#xA;          &lt;td&gt;開發環境&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Sentinel&lt;/td&gt;&#xA;          &lt;td&gt;主從複製 + 自動 failover&lt;/td&gt;&#xA;          &lt;td&gt;中型系統, HA 需求&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Cluster&lt;/td&gt;&#xA;          &lt;td&gt;自動分片, 多主節點&lt;/td&gt;&#xA;          &lt;td&gt;大型系統, 水平擴展&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;rate-limiting&#34;&gt;&lt;span&gt;Rate Limiting&lt;/span&gt;&#xA;  &lt;a href=&#34;#rate-limiting&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;四種演算法&#34;&gt;&lt;span&gt;四種演算法&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%9b%9b%e7%a8%ae%e6%bc%94%e7%ae%97%e6%b3%95&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Fixed Window&lt;/strong&gt;: 把時間切成固定窗口 (例如每分鐘), 每個窗口有一個計數器。請求進來時計數器加一, 超過上限就拒絕。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Window: 00:00 - 01:00&#xA;Counter: 95 requests&#xA;Limit: 100&#xA;&#xA;request at 00:59 -&amp;gt; counter = 96 -&amp;gt; allowed&#xA;request at 01:00 -&amp;gt; new window, counter = 0 -&amp;gt; allowed&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;問題: 窗口交界處可以瞬間打兩倍流量 — 00:59 的 100 次 + 01:00 的 100 次 = 2 秒內 200 次。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Sliding Window&lt;/strong&gt;: 計算的是「過去 N 秒內」的請求數, 而不是「這個窗口內」。解決 Fixed Window 的邊界問題。代價是需要記錄每筆請求的時間戳, 儲存成本較高。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Token Bucket&lt;/strong&gt;: 令牌桶以固定速率產生令牌 (例如每秒 10 個), 桶有容量上限。每個請求消耗一個令牌, 桶空了就拒絕。允許短暫的流量爆發 (burst) 只要桶裡有令牌。適合允許一定彈性的 API。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Bucket capacity: 100 tokens&#xA;Refill rate: 10 tokens/second&#xA;&#xA;Burst scenario:&#xA;  - bucket is full (100 tokens)&#xA;  - 80 requests arrive at once -&amp;gt; allowed (80 tokens consumed)&#xA;  - next second: 10 tokens refilled&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Leaky Bucket&lt;/strong&gt;: 請求進入一個固定容量的佇列 (桶), 以固定速率從桶底流出處理。桶滿了就拒絕新請求。輸出速率絕對平滑, 沒有 burst。適合需要嚴格控制流量的場景 (例如付費 API)。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;演算法&lt;/th&gt;&#xA;          &lt;th&gt;Burst 支援&lt;/th&gt;&#xA;          &lt;th&gt;實作複雜度&lt;/th&gt;&#xA;          &lt;th&gt;適合場景&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Fixed Window&lt;/td&gt;&#xA;          &lt;td&gt;是 (邊界)&lt;/td&gt;&#xA;          &lt;td&gt;最低&lt;/td&gt;&#xA;          &lt;td&gt;簡單 API&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Sliding Window&lt;/td&gt;&#xA;          &lt;td&gt;否&lt;/td&gt;&#xA;          &lt;td&gt;中&lt;/td&gt;&#xA;          &lt;td&gt;精確限流&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Token Bucket&lt;/td&gt;&#xA;          &lt;td&gt;是 (可控)&lt;/td&gt;&#xA;          &lt;td&gt;中&lt;/td&gt;&#xA;          &lt;td&gt;一般 API&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Leaky Bucket&lt;/td&gt;&#xA;          &lt;td&gt;否&lt;/td&gt;&#xA;          &lt;td&gt;中&lt;/td&gt;&#xA;          &lt;td&gt;嚴格平滑輸出&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;分散式-rate-limiting&#34;&gt;&lt;span&gt;分散式 Rate Limiting&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%88%86%e6%95%a3%e5%bc%8f-rate-limiting&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;多個 API Gateway instance 各自計數, 用戶可以把限流繞過去 — 打三個 instance 各打 100 次, 實際是 300 次。&lt;/p&gt;&#xA;&lt;p&gt;解法: 所有 instance 共用 Redis 做計數器。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Gateway A --\&#xA;Gateway B ----&amp;gt; Redis (shared counter) -&amp;gt; enforce global limit&#xA;Gateway C --/&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;INCR rate:user:123:minute:1744840800   # key = user &amp;#43; current minute timestamp&#xA;EXPIRE rate:user:123:minute:1744840800 60&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;INCR&lt;/code&gt; 是原子操作 — Redis 保證同一時間只有一個操作能修改這個 key, 不會有兩個 instance 同時讀到相同的數字再各自加一的問題。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;circuit-breaker&#34;&gt;&lt;span&gt;Circuit Breaker&lt;/span&gt;&#xA;  &lt;a href=&#34;#circuit-breaker&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;為什麼需要-circuit-breaker&#34;&gt;&lt;span&gt;為什麼需要 Circuit Breaker&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%82%ba%e4%bb%80%e9%ba%bc%e9%9c%80%e8%a6%81-circuit-breaker&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Service A 呼叫 Service B, B 開始回應很慢 (例如資料庫過載)。A 的請求堆積等待 B 的回應, A 的線程全部被卡住, A 也開始無法處理新請求。這就是 Cascading Failure (級聯失敗) — 一個服務的問題蔓延到上游。&lt;/p&gt;&#xA;&lt;p&gt;Circuit Breaker 的設計: 偵測到下游失敗率超過閾值時, 直接短路 (不再嘗試呼叫 B), 立刻回傳錯誤或 fallback。讓 A 保持可用, 不被 B 拖垮。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;三個狀態&#34;&gt;&lt;span&gt;三個狀態&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%b8%89%e5%80%8b%e7%8b%80%e6%85%8b&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;Closed -&amp;gt; (error rate &amp;gt; threshold) -&amp;gt; Open&#xA;Open   -&amp;gt; (after timeout)          -&amp;gt; Half-Open&#xA;Half-Open -&amp;gt; (test request fails)  -&amp;gt; Open&#xA;Half-Open -&amp;gt; (test request ok)     -&amp;gt; Closed&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Closed (正常)&lt;/strong&gt;: 所有請求正常送出。統計失敗率, 超過閾值就切到 Open。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Open (斷開)&lt;/strong&gt;: 不再嘗試呼叫下游, 直接回傳錯誤或 fallback 結果。讓下游有時間恢復。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Half-Open (試探)&lt;/strong&gt;: 等待一段時間後, 放行少量測試請求。如果成功, 切回 Closed。如果還是失敗, 回到 Open 繼續等待。&lt;/p&gt;&#xA;&lt;p&gt;Fallback 可以是: 回傳快取的舊資料、回傳預設值、降級功能 (例如電商不顯示個人化推薦, 改顯示熱門商品)。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;scaling-reads&#34;&gt;&lt;span&gt;Scaling Reads&lt;/span&gt;&#xA;  &lt;a href=&#34;#scaling-reads&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;讀流量擴展的標準路徑: Index → Denormalization → Read Replica → Sharding → Caching Layer → CDN。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;cache-invalidation&#34;&gt;&lt;span&gt;Cache Invalidation&lt;/span&gt;&#xA;  &lt;a href=&#34;#cache-invalidation&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Cache 的核心問題: 資料更新後, cache 裡的舊資料什麼時候失效?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;TTL&lt;/strong&gt;: 最簡單。設定過期時間, 時間到自動失效。缺點是資料更新到 TTL 過期這段時間, 用戶看到的是舊資料。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Write-through&lt;/strong&gt;: 每次寫入資料庫時, 同步更新 cache。資料一致性最高, 但增加寫入延遲。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Write-behind (Write-back)&lt;/strong&gt;: 先寫 cache, 非同步批次寫入資料庫。寫入速度快, 但 cache 當機時可能遺失資料。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Tagged Invalidation&lt;/strong&gt;: 給 cache key 打標籤。更新資料時, 把該分類下的所有 cache key 一起失效。例如「所有 category:electronics 的商品 cache」。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Versioned Keys&lt;/strong&gt;: key 加版本號, 例如 &lt;code&gt;product:123:v5&lt;/code&gt;。更新資料時, 版本號加一, 舊 key 自然失效 (沒人讀就等 TTL 自動清除)。好處是不需要主動刪除 cache, 舊 key 繼續存在但不會被讀到。缺點是需要一個地方儲存「目前最新版本號是多少」。&lt;/p&gt;&#xA;&lt;p&gt;TTL 和 Write-through 通常一起用 — Write-through 確保資料更新立刻反映, TTL 作為保底機制。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;cdn-與-edge-caching&#34;&gt;&lt;span&gt;CDN 與 Edge Caching&lt;/span&gt;&#xA;  &lt;a href=&#34;#cdn-%e8%88%87-edge-caching&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;CDN (Content Delivery Network) 在全球多個節點 (edge) 儲存靜態資源的副本。用戶讀取時打到最近的 edge node, 不需要每次都打到 origin server。&lt;/p&gt;&#xA;&lt;p&gt;動態內容也可以 cache 在 CDN — 例如 API 回應在 CDN edge 快取 30 秒。要注意 cache invalidation: 資料更新後要主動 purge CDN 的快取。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;cache-stampede&#34;&gt;&lt;span&gt;Cache Stampede&lt;/span&gt;&#xA;  &lt;a href=&#34;#cache-stampede&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Cache 過期的瞬間, 大量請求同時穿透到資料庫, 造成資料庫瞬間過載, 這叫 Cache Stampede (快取雪崩)。&lt;/p&gt;&#xA;&lt;p&gt;三種解法:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Request Coalescing (請求合併)&lt;/strong&gt;: 第一個打到 cache miss 的請求去查資料庫, 其他請求等待。資料庫回傳後, 所有等待的請求一起拿到結果。問題是需要一個協調機制判斷「誰是第一個」— 通常用 Distributed Lock 實作, 拿到 lock 的去查 DB, 其他人等 lock 釋放後再讀 cache。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stale-While-Revalidate&lt;/strong&gt;: 先回傳過期的舊資料給用戶 (不讓用戶等待), 同時非同步去更新 cache。用戶感受不到延遲, 但有短暫的資料不一致窗口。適合對時效性要求不高的場景。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Probabilistic Early Refresh&lt;/strong&gt;: 在 cache 到期之前, 用機率決定是否提前刷新。越接近到期時間, 刷新的機率越高。這樣 cache 不會在同一時間大量到期, 流量分散到各個時間點。適合高流量系統, 不需要 Distributed Lock, 實作相對簡單。&lt;/p&gt;&#xA;&lt;p&gt;實際使用上, Request Coalescing 和 Stale-While-Revalidate 常一起用, Probabilistic Early Refresh 是更進階的解法。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;scaling-writes&#34;&gt;&lt;span&gt;Scaling Writes&lt;/span&gt;&#xA;  &lt;a href=&#34;#scaling-writes&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;寫流量擴展比讀流量複雜, 因為寫入需要保證一致性。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;資料庫選型&#34;&gt;&lt;span&gt;資料庫選型&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e8%b3%87%e6%96%99%e5%ba%ab%e9%81%b8%e5%9e%8b&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;選資料庫是寫 Scaling 的第一步, 不同 DB 對高寫入的支援能力差異很大。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;資料庫&lt;/th&gt;&#xA;          &lt;th&gt;適合場景&lt;/th&gt;&#xA;          &lt;th&gt;寫入特性&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;PostgreSQL&lt;/td&gt;&#xA;          &lt;td&gt;一般交易型系統&lt;/td&gt;&#xA;          &lt;td&gt;ACID, 強一致&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Cassandra&lt;/td&gt;&#xA;          &lt;td&gt;高寫入, 時間序列&lt;/td&gt;&#xA;          &lt;td&gt;Write-optimized, eventual consistency&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;InfluxDB&lt;/td&gt;&#xA;          &lt;td&gt;IoT, metrics&lt;/td&gt;&#xA;          &lt;td&gt;時序資料, 高壓縮率&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Column Store&lt;/td&gt;&#xA;          &lt;td&gt;分析查詢&lt;/td&gt;&#xA;          &lt;td&gt;批次寫入, 不適合即時&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;horizontal-sharding&#34;&gt;&lt;span&gt;Horizontal Sharding&lt;/span&gt;&#xA;  &lt;a href=&#34;#horizontal-sharding&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;資料量太大時, 把資料水平切分到多個資料庫 instance。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Partition Key 選擇&lt;/strong&gt;: 決定資料落在哪個 shard 的依據。選不好會造成 hot shard — 大量資料集中在同一個 shard。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;user_id % 4 -&amp;gt; shard 0, 1, 2, 3&#xA;&#xA;user:1 -&amp;gt; shard 1&#xA;user:2 -&amp;gt; shard 2&#xA;user:3 -&amp;gt; shard 3&#xA;user:4 -&amp;gt; shard 0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;問題: cross-shard query 複雜, JOIN 無法跨 shard。需要在應用層做聚合。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;vertical-partitioning&#34;&gt;&lt;span&gt;Vertical Partitioning&lt;/span&gt;&#xA;  &lt;a href=&#34;#vertical-partitioning&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;把不同的 table 或 column 拆到不同的資料庫。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Before: one DB with all tables&#xA;After:&#xA;  Users DB    -&amp;gt; user_id, name, email&#xA;  Orders DB   -&amp;gt; order_id, user_id, amount&#xA;  Products DB -&amp;gt; product_id, name, price&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;和 Foreign Key 的差別: Vertical Partitioning 是物理上把 table 放到不同的 DB instance, 不只是邏輯上的關聯。跨 DB 的 JOIN 變成需要在應用層處理。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;queue-解耦寫入&#34;&gt;&lt;span&gt;Queue 解耦寫入&lt;/span&gt;&#xA;  &lt;a href=&#34;#queue-%e8%a7%a3%e8%80%a6%e5%af%ab%e5%85%a5&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;高峰流量時, 把寫入請求先放進 Queue, 非同步處理。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;User request -&amp;gt; API -&amp;gt; Queue -&amp;gt; Consumer -&amp;gt; DB&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;好處: API 立刻回應 (不等 DB 寫入), 流量峰值被 Queue 吸收。壞處: 資料不是立刻寫入, 用戶可能短暫看不到自己剛才的操作結果。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;load-shedding&#34;&gt;&lt;span&gt;Load Shedding&lt;/span&gt;&#xA;  &lt;a href=&#34;#load-shedding&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;系統超載時, 主動拒絕低優先度的請求, 保護核心功能。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Traffic spike:&#xA;  - Priority 1 (payment): always process&#xA;  - Priority 2 (read profile): process if capacity &amp;gt; 60%&#xA;  - Priority 3 (recommendation): drop if capacity &amp;lt; 40%&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;和 Rate Limiting 的差別: Rate Limiting 是對單一用戶限流, Load Shedding 是系統層級的優先度決策。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;hierarchical-aggregation&#34;&gt;&lt;span&gt;Hierarchical Aggregation&lt;/span&gt;&#xA;  &lt;a href=&#34;#hierarchical-aggregation&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;高頻寫入的計數類資料 (例如按讚數、播放次數), 如果每個事件都直接寫入中央資料庫, root 會過載。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Without aggregation:&#xA;  Event 1: likes&amp;#43;&amp;#43; -&amp;gt; Central DB&#xA;  Event 2: likes&amp;#43;&amp;#43; -&amp;gt; Central DB&#xA;  ... (1M events/sec -&amp;gt; Central DB explodes)&#xA;&#xA;With Hierarchical Aggregation:&#xA;  Level 1 (edge nodes): buffer locally, aggregate every 1s&#xA;  Level 2 (regional nodes): aggregate from L1 every 10s&#xA;  Level 3 (central): receive pre-aggregated data every 60s&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;L1 node 每秒彙整一次, 送到 L2 的不是 1000 個事件而是一個「這 1 秒有 1000 個 likes」。L2 再彙整 10 秒的資料送到 central。Central 接收的是已經縮減幾個數量級的流量。&lt;/p&gt;&#xA;&lt;p&gt;注意: root 收到的是彙整後的數量, 不是每個原始事件。如果業務需求是「知道每個用戶各自按了幾次讚」, 就需要在 edge 層保留更細的資料。Hierarchical Aggregation 適合「總數統計」, 不適合「明細追蹤」。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://redis.io/docs/data-types/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Redis Data Types&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://redis.io/docs/latest/commands/geosearch/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Redis GEOSEARCH&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://redis.io/docs/data-types/streams/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Redis Streams Introduction&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://redis.io/docs/interact/pubsub/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Redis Pub/Sub&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://stripe.com/blog/rate-limiters&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Rate Limiting Patterns - Stripe Engineering&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://martinfowler.com/bliki/CircuitBreaker.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Circuit Breaker Pattern - Martin Fowler&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Cache_stampede&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Cache Stampede - Wikipedia&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Thundering_herd_problem&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Thundering Herd Problem&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cseweb.ucsd.edu/~avattani/papers/cache_stampede.pdf&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Probabilistic Early Expiration - Research Paper&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.datastax.com/blog/cassandra-vs-postgresql&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Cassandra vs PostgreSQL - DataStax&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://dataintensive.net/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Designing Data-Intensive Applications - Martin Kleppmann&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item>
    <item>
      <title>Terraform Module 化、Remote State 與 Ansible 基礎</title>
      <link>https://loustack.dev/zh-tw/terraform-modules-ansible-iac/</link>
      <pubDate>Tue, 14 Apr 2026 21:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/terraform-modules-ansible-iac/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/devops/">DevOps</category>
      <category domain="https://loustack.dev/zh-tw/categories/iac/">IaC</category>
      <description>&lt;p&gt;這篇涵蓋兩個主題: Terraform 的 module 化設計與 GCS remote state, 以及 Ansible 的核心結構與冪等性驗證。兩者在角色上互補 — Terraform 負責建立基礎設施, Ansible 負責設定伺服器內部。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;terraform-module-化&#34;&gt;&lt;span&gt;Terraform Module 化&lt;/span&gt;&#xA;  &lt;a href=&#34;#terraform-module-%e5%8c%96&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;為什麼要-module&#34;&gt;&lt;span&gt;為什麼要 Module&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%82%ba%e4%bb%80%e9%ba%bc%e8%a6%81-module&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Terraform 沒有 module 時, 同樣的 VPC 邏輯在 dev 和 prod 各寫一份。改一個地方, 另一個忘了改, 就出問題。Module 的作用和函式一樣: 定義一次, 傳入不同參數使用。&lt;/p&gt;&#xA;&lt;p&gt;目標結構:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;terraform/&#xA;├── modules/&#xA;│   └── vpc/&#xA;│       ├── variables.tf    # inputs&#xA;│       ├── main.tf         # resource definitions&#xA;│       └── outputs.tf      # outputs&#xA;└── environments/&#xA;    ├── dev/&#xA;    │   ├── backend.tf&#xA;    │   ├── variables.tf&#xA;    │   ├── terraform.tfvars&#xA;    │   └── main.tf&#xA;    └── prod/&#xA;        └── ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;modules/&lt;/code&gt; 定義可重用的 infra 單元, &lt;code&gt;environments/&lt;/code&gt; 負責真正的落地, 傳入不同的變數值。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;variablestf&#34;&gt;&lt;span&gt;variables.tf&lt;/span&gt;&#xA;  &lt;a href=&#34;#variablestf&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;variable &amp;#34;project_id&amp;#34; {&#xA;  type = string&#xA;}&#xA;&#xA;variable &amp;#34;region&amp;#34; {&#xA;  type    = string&#xA;  default = &amp;#34;us-east1&amp;#34;&#xA;}&#xA;&#xA;variable &amp;#34;vpc_name&amp;#34; {&#xA;  type = string&#xA;}&#xA;&#xA;variable &amp;#34;subnet_name&amp;#34; {&#xA;  type = string&#xA;}&#xA;&#xA;variable &amp;#34;subnet_cidr&amp;#34; {&#xA;  type = string&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;maintf&#34;&gt;&lt;span&gt;main.tf&lt;/span&gt;&#xA;  &lt;a href=&#34;#maintf&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;resource &amp;#34;google_compute_network&amp;#34; &amp;#34;vpc&amp;#34; {&#xA;  name                    = var.vpc_name&#xA;  auto_create_subnetworks = false&#xA;  project                 = var.project_id&#xA;}&#xA;&#xA;resource &amp;#34;google_compute_subnetwork&amp;#34; &amp;#34;subnet&amp;#34; {&#xA;  name          = var.subnet_name&#xA;  network       = google_compute_network.vpc.id&#xA;  region        = var.region&#xA;  ip_cidr_range = var.subnet_cidr&#xA;  project       = var.project_id&#xA;}&#xA;&#xA;resource &amp;#34;google_compute_firewall&amp;#34; &amp;#34;allow_internal&amp;#34; {&#xA;  name    = &amp;#34;${var.vpc_name}-allow-internal&amp;#34;&#xA;  network = google_compute_network.vpc.name&#xA;  project = var.project_id&#xA;&#xA;  allow {&#xA;    protocol = &amp;#34;tcp&amp;#34;&#xA;  }&#xA;  allow {&#xA;    protocol = &amp;#34;udp&amp;#34;&#xA;  }&#xA;  allow {&#xA;    protocol = &amp;#34;icmp&amp;#34;&#xA;  }&#xA;&#xA;  source_ranges = [var.subnet_cidr]&#xA;}&#xA;&#xA;resource &amp;#34;google_compute_firewall&amp;#34; &amp;#34;allow_ssh&amp;#34; {&#xA;  name    = &amp;#34;${var.vpc_name}-allow-ssh&amp;#34;&#xA;  network = google_compute_network.vpc.name&#xA;  project = var.project_id&#xA;&#xA;  allow {&#xA;    protocol = &amp;#34;tcp&amp;#34;&#xA;    ports    = [&amp;#34;22&amp;#34;]&#xA;  }&#xA;&#xA;  source_ranges = [&amp;#34;0.0.0.0/0&amp;#34;]&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;outputstf&#34;&gt;&lt;span&gt;outputs.tf&lt;/span&gt;&#xA;  &lt;a href=&#34;#outputstf&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;output &amp;#34;vpc_id&amp;#34; {&#xA;  value = google_compute_network.vpc.id&#xA;}&#xA;&#xA;output &amp;#34;vpc_name&amp;#34; {&#xA;  value = google_compute_network.vpc.name&#xA;}&#xA;&#xA;output &amp;#34;subnet_id&amp;#34; {&#xA;  value = google_compute_subnetwork.subnet.id&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;environmentsdevmaintf&#34;&gt;&lt;span&gt;environments/dev/main.tf&lt;/span&gt;&#xA;  &lt;a href=&#34;#environmentsdevmaintf&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;provider &amp;#34;google&amp;#34; {&#xA;  project = var.project_id&#xA;  region  = var.region&#xA;}&#xA;&#xA;module &amp;#34;vpc&amp;#34; {&#xA;  source      = &amp;#34;../../modules/vpc&amp;#34;&#xA;  project_id  = var.project_id&#xA;  region      = var.region&#xA;  vpc_name    = &amp;#34;devops-vpc&amp;#34;&#xA;  subnet_name = &amp;#34;devops-subnet&amp;#34;&#xA;  subnet_cidr = &amp;#34;10.0.1.0/24&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;terraformtfvars&#34;&gt;&lt;span&gt;terraform.tfvars&lt;/span&gt;&#xA;  &lt;a href=&#34;#terraformtfvars&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;project_id = &amp;#34;devops-lab-lou-2026&amp;#34;&#xA;region     = &amp;#34;us-east1&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;prod 環境呼叫同一個 module, 只是傳入不同的 vpc_name 和 subnet_cidr。module 本身不改動。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;remote-state-與-state-locking&#34;&gt;&lt;span&gt;Remote State 與 State Locking&lt;/span&gt;&#xA;  &lt;a href=&#34;#remote-state-%e8%88%87-state-locking&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;為什麼需要-remote-state&#34;&gt;&lt;span&gt;為什麼需要 Remote State&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%82%ba%e4%bb%80%e9%ba%bc%e9%9c%80%e8%a6%81-remote-state&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Terraform 的 state file 記錄「現在 GCP 上長什麼樣子」。沒有 remote state, state 只存在本機:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;本機不見 = infra 資訊全部丟失&lt;/li&gt;&#xA;&lt;li&gt;多人協作 = 每個人各自有一份 state, 互相衝突&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;GCS backend 把 state 集中存在 GCS bucket, 所有人共用同一份。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;為什麼需要-state-locking&#34;&gt;&lt;span&gt;為什麼需要 State Locking&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%82%ba%e4%bb%80%e9%ba%bc%e9%9c%80%e8%a6%81-state-locking&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;如果兩個人同時跑 &lt;code&gt;terraform apply&lt;/code&gt;, 兩個人都讀到相同的 state, 各自計算 diff, 各自套用變更 — 結果不可預期。&lt;/p&gt;&#xA;&lt;p&gt;State locking 讓第一個 &lt;code&gt;apply&lt;/code&gt; 開始後就鎖住, 第二個人必須等待鎖釋放才能繼續。這對應的是 CAP Theorem 的 Consistency: 分散式系統裡, 不能讓兩個操作同時修改同一個東西。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# environments/dev/backend.tf&#xA;terraform {&#xA;  backend &amp;#34;gcs&amp;#34; {&#xA;    bucket = &amp;#34;devops-lab-lou-2026-tfstate&amp;#34;&#xA;    prefix = &amp;#34;environments/dev&amp;#34;&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;GCS backend 原生支援 locking, 用 GCS object 的 &lt;code&gt;generation&lt;/code&gt; 機制實作。不需要額外設定。&lt;/p&gt;&#xA;&lt;p&gt;初始化:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;terraform init&#xA;terraform plan&#xA;terraform apply&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;plan&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 要分開的原因: &lt;code&gt;plan&lt;/code&gt; 讓你看到「即將發生什麼」, &lt;code&gt;apply&lt;/code&gt; 才真正執行。Production 環境裡, plan 的結果通常要先審核才能 apply。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;state-操作指令&#34;&gt;&lt;span&gt;State 操作指令&lt;/span&gt;&#xA;  &lt;a href=&#34;#state-%e6%93%8d%e4%bd%9c%e6%8c%87%e4%bb%a4&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;# list all resources tracked in state&#xA;terraform state list&#xA;&#xA;# show detail of a specific resource&#xA;terraform state show module.vpc.google_compute_network.vpc&#xA;&#xA;# move resource to a different address (e.g. refactor module path)&#xA;terraform state mv old_address new_address&#xA;&#xA;# remove resource from state without destroying it&#xA;terraform state rm module.vpc.google_compute_network.vpc&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;state rm&lt;/code&gt; 不會刪除 GCP 上的資源, 只是讓 Terraform 不再追蹤它。下次 apply 時, Terraform 以為那個資源不存在, 可能嘗試重建。所以 &lt;code&gt;state rm&lt;/code&gt; 通常搭配 &lt;code&gt;terraform import&lt;/code&gt; 使用, 先移除再重新 import 到新的 address。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;ansible&#34;&gt;&lt;span&gt;Ansible&lt;/span&gt;&#xA;  &lt;a href=&#34;#ansible&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;terraform-vs-ansible&#34;&gt;&lt;span&gt;Terraform vs Ansible&lt;/span&gt;&#xA;  &lt;a href=&#34;#terraform-vs-ansible&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;Terraform&lt;/th&gt;&#xA;          &lt;th&gt;Ansible&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;用途&lt;/td&gt;&#xA;          &lt;td&gt;建基礎設施&lt;/td&gt;&#xA;          &lt;td&gt;設定伺服器內部&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;語言&lt;/td&gt;&#xA;          &lt;td&gt;HCL (declarative)&lt;/td&gt;&#xA;          &lt;td&gt;YAML playbook&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;狀態&lt;/td&gt;&#xA;          &lt;td&gt;state file&lt;/td&gt;&#xA;          &lt;td&gt;無 state, 每次重新檢查&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;連線方式&lt;/td&gt;&#xA;          &lt;td&gt;API call&lt;/td&gt;&#xA;          &lt;td&gt;SSH (agentless)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;冪等性&lt;/td&gt;&#xA;          &lt;td&gt;天生冪等&lt;/td&gt;&#xA;          &lt;td&gt;需要正確寫 playbook&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Ansible agentless 的意思是: 目標機器不需要安裝任何 Ansible agent, 只需要有 SSH 和 Python。Ansible 在控制機 (你的 laptop 或 CI runner) 上執行, 透過 SSH 連到目標機器執行操作。&lt;/p&gt;&#xA;&lt;p&gt;現代 K8s 環境裡 Ansible 的角色變小了, 因為 K8s YAML 取代了很多設定工作。但 VM 環境和 on-premise 環境仍然很常見。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;核心結構&#34;&gt;&lt;span&gt;核心結構&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e6%a0%b8%e5%bf%83%e7%b5%90%e6%a7%8b&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Inventory&lt;/strong&gt;: 你要管理哪些機器, 分組管理。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[local]&#xA;localhost ansible_connection=local&#xA;&#xA;[webservers]&#xA;web1.example.com&#xA;web2.example.com&#xA;&#xA;[databases]&#xA;db1.example.com ansible_user=ubuntu&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Module&lt;/strong&gt;: Ansible 內建的操作單元, 例如 &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;copy&lt;/code&gt;, &lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;。Module 是冪等的基礎 — 每個 module 在執行前先檢查目標狀態, 狀態已符合就不做任何事。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Task&lt;/strong&gt;: 一個步驟, 呼叫一個 module。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Playbook&lt;/strong&gt;: 一個或多個 task 的組合, 定義在哪些 host 上執行什麼。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Role&lt;/strong&gt;: 多個 task 的集合, 可重用, 類似 Terraform 的 module。適合封裝 nginx 安裝、prometheus 設定這類可重複使用的邏輯。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;基本-playbook&#34;&gt;&lt;span&gt;基本 Playbook&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%9f%ba%e6%9c%ac-playbook&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;---&#xA;- name: localhost practice&#xA;  hosts: local&#xA;  tasks:&#xA;    - name: create practice directory&#xA;      file:&#xA;        path: /tmp/ansible-practice&#xA;        state: directory&#xA;        mode: &amp;#34;0755&amp;#34;&#xA;&#xA;    - name: write config file&#xA;      copy:&#xA;        dest: /tmp/ansible-practice/config.txt&#xA;        content: |&#xA;          env=dev&#xA;          version=1.0&#xA;&#xA;    - name: show file content&#xA;      command: cat /tmp/ansible-practice/config.txt&#xA;      register: file_content&#xA;&#xA;    - name: print result&#xA;      debug:&#xA;        msg: &amp;#34;{{ file_content.stdout }}&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;# check connectivity&#xA;ansible local -i inventory.ini -m ping&#xA;&#xA;# run playbook&#xA;ansible-playbook -i inventory.ini playbook.yml&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;冪等性驗證&#34;&gt;&lt;span&gt;冪等性驗證&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%86%aa%e7%ad%89%e6%80%a7%e9%a9%97%e8%ad%89&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;第一次執行:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;TASK [create practice directory] ... changed&#xA;TASK [write config file]         ... changed&#xA;TASK [show file content]         ... changed&#xA;TASK [print result]              ... ok&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;第二次執行 (不改任何東西):&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;TASK [create practice directory] ... ok      # already exists, no action&#xA;TASK [write config file]         ... ok      # content unchanged, no action&#xA;TASK [show file content]         ... changed # command module always runs&#xA;TASK [print result]              ... ok&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;file&lt;/code&gt; 和 &lt;code&gt;copy&lt;/code&gt; module 是冪等的, 第二次執行顯示 &lt;code&gt;ok&lt;/code&gt; 代表沒有做任何變更。&lt;code&gt;command&lt;/code&gt; module 不是冪等的, 每次都會執行 — 這是設計上需要注意的地方。需要冪等的指令操作要用 &lt;code&gt;creates&lt;/code&gt; 或 &lt;code&gt;when&lt;/code&gt; 條件控制。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/modules&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Terraform Modules documentation&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/backend/gcs&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Terraform GCS Backend&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/terraform/cli/commands/state&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Terraform State Commands&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/CAP_theorem&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;CAP Theorem - Wikipedia&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.ansible.com/ansible/latest/getting_started/index.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Ansible Getting Started&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Ansible Module Index&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.ansible.com/ansible/latest/reference_appendices/glossary.html#term-Idempotency&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Ansible Idempotency&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item>
    <item>
      <title>GCP Networking 基礎與 API Gateway 設計</title>
      <link>https://loustack.dev/zh-tw/gcp-networking-api-gateway-fundamentals/</link>
      <pubDate>Sat, 11 Apr 2026 21:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/gcp-networking-api-gateway-fundamentals/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/devops/">DevOps</category>
      <category domain="https://loustack.dev/zh-tw/categories/gcp/">GCP</category>
      <category domain="https://loustack.dev/zh-tw/categories/systemdesign/">SystemDesign</category>
      <description>&lt;p&gt;這篇涵蓋兩個主題: 在 GCP 上手動建立 VPC、Subnet、Firewall Rules、Service Account, 以及 API Gateway 的設計概念與面試常見問題。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;gcp-networking&#34;&gt;&lt;span&gt;GCP Networking&lt;/span&gt;&#xA;  &lt;a href=&#34;#gcp-networking&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;vpc-global-vs-regional&#34;&gt;&lt;span&gt;VPC: Global vs Regional&lt;/span&gt;&#xA;  &lt;a href=&#34;#vpc-global-vs-regional&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;GCP VPC 是 &lt;strong&gt;global&lt;/strong&gt; 的, Azure VNet 是 &lt;strong&gt;regional&lt;/strong&gt; 的。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;GCP:&#xA;  devops-vpc (global)&#xA;  ├── subnet: us-east1 (10.0.1.0/24)&#xA;  └── subnet: asia-east1 (10.0.2.0/24)   # same VPC, different region&#xA;  # VMs in different regions communicate over internal network directly&#xA;&#xA;Azure:&#xA;  vnet-eastus (East US)                   # region-scoped&#xA;  vnet-westus (West US)                   # separate VNet&#xA;  # cross-region requires VNet Peering&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;GCP 的優勢是同一個 VPC 內, 不同 region 的 VM 可以走內網直通, 不需要 Peering。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;subnet-與-cidr&#34;&gt;&lt;span&gt;Subnet 與 CIDR&lt;/span&gt;&#xA;  &lt;a href=&#34;#subnet-%e8%88%87-cidr&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Subnet 是 regional 的。建 Subnet 要指定 region 和 IP 範圍 (CIDR)。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;10.0.1.0/24&#xA;&#xA;10  .  0  .  1  .  0&#xA;00001010.00000000.00000001.00000000&#xA;|______fixed 24 bits_______| vary |&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;/24&lt;/code&gt; 後面 8 bits 可變 = 2^8 = 256 個 IP (可用 254 個)。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;CIDR&lt;/th&gt;&#xA;          &lt;th&gt;Usable IPs&lt;/th&gt;&#xA;          &lt;th&gt;Use case&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;/8&lt;/td&gt;&#xA;          &lt;td&gt;16,777,214&lt;/td&gt;&#xA;          &lt;td&gt;Large private network&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;/16&lt;/td&gt;&#xA;          &lt;td&gt;65,534&lt;/td&gt;&#xA;          &lt;td&gt;Medium VPC&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;/24&lt;/td&gt;&#xA;          &lt;td&gt;254&lt;/td&gt;&#xA;          &lt;td&gt;Typical subnet&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;/28&lt;/td&gt;&#xA;          &lt;td&gt;14&lt;/td&gt;&#xA;          &lt;td&gt;Small subnet (GCP minimum)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;/32&lt;/td&gt;&#xA;          &lt;td&gt;1&lt;/td&gt;&#xA;          &lt;td&gt;Single IP (firewall rule)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;code&gt;/0&lt;/code&gt; = 所有 IP。&lt;code&gt;0.0.0.0/0&lt;/code&gt; 就是「任意來源」。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# create VPC (custom mode: you control subnet creation)&#xA;gcloud compute networks create devops-vpc --subnet-mode=custom&#xA;&#xA;# create subnet in us-east1&#xA;gcloud compute networks subnets create devops-subnet \&#xA;  --network=devops-vpc \&#xA;  --region=us-east1 \&#xA;  --range=10.0.1.0/24&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;firewall-rules&#34;&gt;&lt;span&gt;Firewall Rules&lt;/span&gt;&#xA;  &lt;a href=&#34;#firewall-rules&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;GCP Firewall Rules 掛在 VPC 層, 不是 Subnet 層 (Azure NSG 可以掛在 Subnet 或 NIC)。&lt;/p&gt;&#xA;&lt;p&gt;Priority 數字越小, 優先度越高。預設 1000, 最高 0, 最低 65535。GCP 有一條隱藏的 implied deny-all (priority 65535), 所有沒有 match 到任何 rule 的流量都會被擋。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Priority 500: deny tcp:22 from 1.2.3.4   # matched first -&amp;gt; blocked&#xA;Priority 1000: allow tcp:22 from 0.0.0.0/0&#xA;# result: 1.2.3.4 is blocked, all other IPs allowed&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;# allow SSH from anywhere (dev only -- use your own IP in production)&#xA;gcloud compute firewall-rules create allow-ssh \&#xA;  --network=devops-vpc \&#xA;  --allow=tcp:22 \&#xA;  --source-ranges=0.0.0.0/0&#xA;&#xA;# allow all internal traffic within the subnet&#xA;gcloud compute firewall-rules create allow-internal \&#xA;  --network=devops-vpc \&#xA;  --allow=tcp,udp,icmp \&#xA;  --source-ranges=10.0.1.0/24&#xA;&#xA;# verify source ranges&#xA;gcloud compute firewall-rules describe allow-ssh --format=&amp;#34;get(sourceRanges)&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Production 做法是 &lt;code&gt;--source-ranges=YOUR_IP/32&lt;/code&gt;, 只允許自己的 IP。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;service-account-vs-managed-identity&#34;&gt;&lt;span&gt;Service Account vs Managed Identity&lt;/span&gt;&#xA;  &lt;a href=&#34;#service-account-vs-managed-identity&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Service Account 是給程式或服務用的身分, 不是給人用的。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;GCP Service Account&lt;/th&gt;&#xA;          &lt;th&gt;Azure Managed Identity&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;建立方式&lt;/td&gt;&#xA;          &lt;td&gt;手動建立, 手動指派&lt;/td&gt;&#xA;          &lt;td&gt;System-assigned 隨資源自動建立&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Credentials&lt;/td&gt;&#xA;          &lt;td&gt;可下載 JSON key (但不建議)&lt;/td&gt;&#xA;          &lt;td&gt;完全沒有 key, Azure 管 token lifecycle&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;生命週期&lt;/td&gt;&#xA;          &lt;td&gt;獨立存在, 手動管理&lt;/td&gt;&#xA;          &lt;td&gt;System-assigned 隨資源一起刪除&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Azure 設計上就把 key 的路堵死了, 是更安全的預設值。GCP 的 SA key 下載太方便, 是常見的安全漏洞來源。&lt;/p&gt;&#xA;&lt;p&gt;解法是 &lt;strong&gt;Workload Identity Federation (WIF)&lt;/strong&gt;: 讓 GitHub Actions 或 GKE Pod 用 OIDC token 換 GCP 權限, 整個流程不需要 SA key。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;GitHub Actions&#xA;  -&amp;gt; generate OIDC token&#xA;    -&amp;gt; WIF validates token&#xA;      -&amp;gt; exchange for SA permissions&#xA;        -&amp;gt; access GCP resources (no key involved)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SA + WIF = 跟 Managed Identity 同等安全, 但需要主動設定。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# create service account&#xA;gcloud iam service-accounts create devops-cicd-sa \&#xA;  --display-name=&amp;#34;DevOps CI/CD SA&amp;#34; \&#xA;  --project=devops-lab-lou-2026&#xA;&#xA;# verify&#xA;gcloud iam service-accounts list --project=devops-lab-lou-2026&lt;/code&gt;&lt;/pre&gt;&lt;h2 class=&#34;heading-element&#34; id=&#34;api-gateway&#34;&gt;&lt;span&gt;API Gateway&lt;/span&gt;&#xA;  &lt;a href=&#34;#api-gateway&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;定義&#34;&gt;&lt;span&gt;定義&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%ae%9a%e7%be%a9&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;API Gateway 是所有請求的統一入口, 負責處理橫切關注點 (cross-cutting concerns), 讓後端服務只需要專注在業務邏輯。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;在 Gateway 處理&lt;/th&gt;&#xA;          &lt;th&gt;後端服務就不用管&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;TLS termination&lt;/td&gt;&#xA;          &lt;td&gt;不用處理 HTTPS&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Auth (JWT / API key)&lt;/td&gt;&#xA;          &lt;td&gt;不用每個服務各自驗 token&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Rate limiting&lt;/td&gt;&#xA;          &lt;td&gt;不用自己擋暴力請求&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Caching&lt;/td&gt;&#xA;          &lt;td&gt;不用每個服務自己做 cache&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Routing&lt;/td&gt;&#xA;          &lt;td&gt;統一管理 path → service 對應&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;ssl-termination&#34;&gt;&lt;span&gt;SSL Termination&lt;/span&gt;&#xA;  &lt;a href=&#34;#ssl-termination&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;TLS handshake 有 CPU 開銷。讓 Gateway 處理 SSL, 後端和 Gateway 之間走 HTTP 內網通訊。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Client  --HTTPS--&amp;gt;  API Gateway  --HTTP--&amp;gt;  Backend Pod A&#xA;                    (decrypt here)          Backend Pod B&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;好處:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;憑證只在 Gateway 管一份, 到期只更新一個地方&lt;/li&gt;&#xA;&lt;li&gt;後端服務省去加解密的 CPU 消耗&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Gateway 和後端在同一個 VPC 內, 內部 HTTP 通常可以接受。需要更嚴格的場景 (金融、醫療) 可以加 mTLS, 後端之間也加密。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;caching&#34;&gt;&lt;span&gt;Caching&lt;/span&gt;&#xA;  &lt;a href=&#34;#caching&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;讀多寫少、回應不常變動的 API 適合在 Gateway 層 cache。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Without cache:&#xA;  User A -&amp;gt; GET /products -&amp;gt; Gateway -&amp;gt; Backend -&amp;gt; DB&#xA;  User B -&amp;gt; GET /products -&amp;gt; Gateway -&amp;gt; Backend -&amp;gt; DB&#xA;  # 100,000 requests = 100,000 DB queries&#xA;&#xA;With cache:&#xA;  User A -&amp;gt; GET /products -&amp;gt; Gateway -&amp;gt; Backend -&amp;gt; DB  (first request, store result)&#xA;  User B -&amp;gt; GET /products -&amp;gt; Gateway -&amp;gt; return cached  (no backend hit)&#xA;  # 100,000 requests = 1 DB query&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;個人化的回應 (&lt;code&gt;GET /cart&lt;/code&gt;, &lt;code&gt;GET /orders&lt;/code&gt;) 不適合 cache, 因為每個用戶的資料不同。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;bff-pattern&#34;&gt;&lt;span&gt;BFF Pattern&lt;/span&gt;&#xA;  &lt;a href=&#34;#bff-pattern&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;不同客戶端對 API 的需求差異越大, 用同一個 Gateway 服務所有客戶端就會出現各種妥協。BFF (Backend for Frontend) 的解法: 為不同類型的客戶端各自維護一個 Gateway。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Web BFF    ---&amp;gt; User Service&#xA;Mobile BFF ---&amp;gt; Order Service   (same backend, different gateway)&#xA;Public BFF ---&amp;gt; Product Service&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Web BFF 可以做大量資料聚合, Mobile BFF 只回傳精簡欄位, Public BFF 做嚴格版本控制。後端微服務保持不變。&lt;/p&gt;&#xA;&lt;p&gt;適合: 有多個差異明顯的客戶端, 且每個客戶端團隊有能力維護自己的 BFF。小型團隊或客戶端差異不大時, 一個統一 Gateway 就夠了。&lt;/p&gt;&#xA;&lt;p&gt;GraphQL 是另一個解法: 客戶端自己決定要哪些欄位, 同一個 endpoint 服務所有客戶端。代價是 schema 設計複雜度和 N+1 query 問題。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;架構與高可用&#34;&gt;&lt;span&gt;架構與高可用&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e6%9e%b6%e6%a7%8b%e8%88%87%e9%ab%98%e5%8f%af%e7%94%a8&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;Internet&#xA;  |&#xA;L7 LB          # distributes traffic across Gateway instances, health checks&#xA;  |&#xA;API Gateway x N instances (stateless)&#xA;  |- Auth (JWT / API key validation)&#xA;  |- Rate limiting (counters in Redis, shared across instances)&#xA;  |- Routing (/users -&amp;gt; User Service, /orders -&amp;gt; Order Service)&#xA;  |- SSL Termination&#xA;  |&#xA;L4 LB          # distributes traffic to backend pods&#xA;  |&#xA;Backend microservices&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Gateway 是&lt;strong&gt;無狀態&lt;/strong&gt;的, 路由規則存在設定檔或 DB, 任何 instance 都能處理任何請求, 水平擴展自然。&lt;/p&gt;&#xA;&lt;p&gt;Rate limiting 是有狀態的: 計數器存在 Redis, 所有 instance 共享同一份數據。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;API Gateway instance A --|&#xA;API Gateway instance B --|---&amp;gt; Redis (rate limit counters)&#xA;API Gateway instance C --|&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;沒有共享 Redis 的話, 用戶可以同時打三個 instance 各打 100 次, 繞過 100 次/分鐘的限制。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;常見面試問題&#34;&gt;&lt;span&gt;常見面試問題&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%b8%b8%e8%a6%8b%e9%9d%a2%e8%a9%a6%e5%95%8f%e9%a1%8c&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Q: Is the Gateway a SPOF?&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;No. Deploy multiple instances behind a Load Balancer. Gateway is stateless — any instance can handle any request — so horizontal scaling is straightforward.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Q: How do you share rate limit state across instances?&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Store rate limit counters in Redis. All Gateway instances read and write to the same Redis, so the limit is enforced globally regardless of which instance handles the request.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Q: What&amp;rsquo;s the difference between a Gateway and a Load Balancer?&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;LB distributes traffic at L4/L7. Gateway handles application-level concerns — auth, rate limiting, routing logic. LB sits in front of the Gateway for HA; Gateway sits in front of your services for business logic.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;consistent-hashing&#34;&gt;&lt;span&gt;Consistent Hashing&lt;/span&gt;&#xA;  &lt;a href=&#34;#consistent-hashing&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;多個 LB instance 如何在不共享狀態的情況下做路由決策?&lt;/p&gt;&#xA;&lt;p&gt;Round Robin 需要計數器 (「我上次給哪台」), 多個 instance 各自維護計數器, 分流會不均衡。&lt;/p&gt;&#xA;&lt;p&gt;Consistent Hashing 用確定性算法: 同一個輸入永遠得到同一個輸出, 不依賴任何外部狀態。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;hash(client IP) % number_of_backends = which backend to route to&#xA;&#xA;IP ending in 0-3 -&amp;gt; Backend A&#xA;IP ending in 4-6 -&amp;gt; Backend B&#xA;IP ending in 7-9 -&amp;gt; Backend C&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;任何 LB instance 收到同一個 IP, 算出來的結果都一樣。不需要溝通, 不需要共享狀態。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Algorithm&lt;/th&gt;&#xA;          &lt;th&gt;Needs shared state&lt;/th&gt;&#xA;          &lt;th&gt;Reason&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Round Robin&lt;/td&gt;&#xA;          &lt;td&gt;Yes&lt;/td&gt;&#xA;          &lt;td&gt;requires counter&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Least Connections&lt;/td&gt;&#xA;          &lt;td&gt;Yes&lt;/td&gt;&#xA;          &lt;td&gt;requires connection count per backend&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Random&lt;/td&gt;&#xA;          &lt;td&gt;No&lt;/td&gt;&#xA;          &lt;td&gt;stateless by nature&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Consistent Hashing&lt;/td&gt;&#xA;          &lt;td&gt;No&lt;/td&gt;&#xA;          &lt;td&gt;deterministic function, same input = same output&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/vpc/docs/overview&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GCP VPC overview&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/firewall/docs/firewalls&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GCP Firewall Rules&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/iam/docs/service-account-overview&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GCP Service Accounts&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/iam/docs/workload-identity-federation&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Workload Identity Federation&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://cloud.google.com/api-gateway/docs/about-api-gateway&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;API Gateway concepts - Google Cloud&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://tom-e-white.com/2007/11/consistent-hashing.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Consistent Hashing - Tom White&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://samnewman.io/patterns/architectural/bff/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;BFF Pattern - Sam Newman&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://redis.io/glossary/rate-limiting/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Rate Limiting with Redis&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item>
    <item>
      <title>Networking 基礎: DNS、HTTP/HTTPS、Load Balancer 與 Proxy</title>
      <link>https://loustack.dev/zh-tw/networking-fundamentals-dns-http-load-balancer-proxy/</link>
      <pubDate>Thu, 09 Apr 2026 22:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/networking-fundamentals-dns-http-load-balancer-proxy/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/devops/">DevOps</category>
      <category domain="https://loustack.dev/zh-tw/categories/networking/">Networking</category>
      <description>&lt;p&gt;網路是系統設計的地基。這篇從實際指令出發, 理解 DNS 解析、HTTP/HTTPS、TLS、Load Balancer、Proxy 的概念和 trade-off。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;dns&#34;&gt;&lt;span&gt;DNS&lt;/span&gt;&#xA;  &lt;a href=&#34;#dns&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;dig-指令&#34;&gt;&lt;span&gt;dig 指令&lt;/span&gt;&#xA;  &lt;a href=&#34;#dig-%e6%8c%87%e4%bb%a4&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;dig google.com&#xA;dig &amp;#43;short example.com&#xA;dig &amp;#43;short example.com CNAME&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;dig&lt;/code&gt; 是 DNS 查詢工具。&lt;code&gt;+short&lt;/code&gt; 只顯示結果, 省略雜訊。&lt;/p&gt;&#xA;&lt;p&gt;輸出解讀:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;;; QUESTION SECTION:&#xA;;google.com.    IN    A          # query A Record (IP address)&#xA;&#xA;;; ANSWER SECTION:&#xA;google.com.  300  IN  A  142.250.x.x   # TTL = 300 seconds&#xA;google.com.  300  IN  A  142.250.x.x&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Google 回傳多個 IP, 是 Client-side Load Balancing — DNS 回傳多個 IP 讓客戶端自己選一個連。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;a-record-vs-cname&#34;&gt;&lt;span&gt;A Record vs CNAME&lt;/span&gt;&#xA;  &lt;a href=&#34;#a-record-vs-cname&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;A Record&lt;/strong&gt;: domain → IP (直接給地址)&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;example.com  →  1.2.3.4&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;CNAME&lt;/strong&gt;: domain → 另一個 domain (別名, 再去查那個的 IP)&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;blog.example.com  →  user.github.io  # alias&#xA;user.github.io    →  140.82.x.x      # actual IP&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;ttl&#34;&gt;&lt;span&gt;TTL&lt;/span&gt;&#xA;  &lt;a href=&#34;#ttl&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;TTL (Time To Live) = DNS cache 的秒數。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;TTL 300 = 電腦 cache 這個結果 5 分鐘, 5 分鐘內不重新查詢&lt;/li&gt;&#xA;&lt;li&gt;改了 DNS record 後, 要等 TTL 才會全面生效&lt;/li&gt;&#xA;&lt;li&gt;TTL 越短, 改動越快生效, 但 DNS server 負擔越大&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;cname-flattening&#34;&gt;&lt;span&gt;CNAME Flattening&lt;/span&gt;&#xA;  &lt;a href=&#34;#cname-flattening&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;根域名 (apex domain, 如 &lt;code&gt;example.com&lt;/code&gt;) 技術上不能有 CNAME (DNS 標準限制)。Cloudflare 等 DNS 服務會自動把它壓平成 A Record 回傳, 稱為 CNAME Flattening。&lt;/p&gt;&#xA;&lt;p&gt;Proxied 模式下, Cloudflare 不回傳真實的 CNAME 或 origin IP, 只回傳自己的 proxy IP。所以 &lt;code&gt;dig example.com CNAME&lt;/code&gt; 可能回傳空的。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;devops-連結&#34;&gt;&lt;span&gt;DevOps 連結&lt;/span&gt;&#xA;  &lt;a href=&#34;#devops-%e9%80%a3%e7%b5%90&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;K8s 內部服務互相呼叫靠 &lt;strong&gt;CoreDNS&lt;/strong&gt; (&lt;code&gt;kube-dns&lt;/code&gt;)&lt;/li&gt;&#xA;&lt;li&gt;Pod 找其他 service: &lt;code&gt;service-name.namespace.svc.cluster.local&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;部署新服務需要設定 domain 指向 Load Balancer IP&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;http--https--tls&#34;&gt;&lt;span&gt;HTTP / HTTPS / TLS&lt;/span&gt;&#xA;  &lt;a href=&#34;#http--https--tls&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;curl--v-看連線過程&#34;&gt;&lt;span&gt;curl -v 看連線過程&lt;/span&gt;&#xA;  &lt;a href=&#34;#curl--v-%e7%9c%8b%e9%80%a3%e7%b7%9a%e9%81%8e%e7%a8%8b&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;curl -v https://example.com 2&amp;gt;&amp;amp;1 | head -40&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;-v&lt;/code&gt; 顯示詳細資訊。&lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; 把 stderr 合併到 stdout, 讓 &lt;code&gt;head&lt;/code&gt; 能同時截斷兩者。&lt;/p&gt;&#xA;&lt;p&gt;實際輸出片段:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;* Trying x.x.x.x:443...&#xA;* TLSv1.3 (OUT), TLS handshake, Client hello   # client proposes cipher suites&#xA;* TLSv1.3 (IN),  TLS handshake, Server hello   # server selects TLSv1.3&#xA;* TLSv1.3 (IN),  TLS handshake, Certificate    # server sends certificate&#xA;* TLSv1.3 (IN),  TLS handshake, Finished       # encrypted channel established&#xA;&amp;gt; GET / HTTP/2                                  # HTTP request (inside encrypted channel)&#xA;&amp;lt; HTTP/2 200                                    # response&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;連線順序&#34;&gt;&lt;span&gt;連線順序&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e9%80%a3%e7%b7%9a%e9%a0%86%e5%ba%8f&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;TCP 3-way handshake     (establish connection)&#xA;  SYN -&amp;gt; SYN-ACK -&amp;gt; ACK&#xA;&#xA;TLS handshake           (establish encryption, on top of TCP)&#xA;  Client Hello -&amp;gt; Server Hello -&amp;gt; Certificate -&amp;gt; Verify -&amp;gt; Finished&#xA;&#xA;HTTP request            (transmitted inside encrypted channel)&#xA;  GET / HTTP/2&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;TCP 握手&lt;/strong&gt; 和 &lt;strong&gt;TLS 握手&lt;/strong&gt; 是兩件不同的事, TCP 先完成才開始 TLS。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;憑證資訊&#34;&gt;&lt;span&gt;憑證資訊&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e6%86%91%e8%ad%89%e8%b3%87%e8%a8%8a&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;subject: CN=example.com        # certificate issued for this domain&#xA;issuer: Google Trust Services  # certificate authority&#xA;expire date: ...               # expiry date (auto-renewed by Cloudflare)&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;devops-連結-1&#34;&gt;&lt;span&gt;DevOps 連結&lt;/span&gt;&#xA;  &lt;a href=&#34;#devops-%e9%80%a3%e7%b5%90-1&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;憑證到期 → 服務掛掉, 是常見的 incident&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;curl -v&lt;/code&gt; 是排查 TLS 問題的基本工具&lt;/li&gt;&#xA;&lt;li&gt;Ingress Controller 做 TLS termination, 憑證設定在 Ingress 上, 後端 Pod 接收 plain HTTP&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;reverse-proxy-vs-forward-proxy&#34;&gt;&lt;span&gt;Reverse Proxy vs Forward Proxy&lt;/span&gt;&#xA;  &lt;a href=&#34;#reverse-proxy-vs-forward-proxy&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;forward-proxy&#34;&gt;&lt;span&gt;Forward Proxy&lt;/span&gt;&#xA;  &lt;a href=&#34;#forward-proxy&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;Client -&amp;gt; Proxy Server -&amp;gt; Target Website&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;代理&lt;strong&gt;客戶端&lt;/strong&gt;對外的請求。目標網站看到 Proxy 的 IP, 不是你的 IP。&lt;/p&gt;&#xA;&lt;p&gt;常見用途: 公司內網管控、VPN、隱藏真實 IP。Windows Network Manager 和瀏覽器設定的就是這個。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;reverse-proxy&#34;&gt;&lt;span&gt;Reverse Proxy&lt;/span&gt;&#xA;  &lt;a href=&#34;#reverse-proxy&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;Client -&amp;gt; Reverse Proxy -&amp;gt; Your Server&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;代理&lt;strong&gt;伺服器&lt;/strong&gt;接收請求。用戶看到 Proxy 的 IP, 不是伺服器的真實 IP。&lt;/p&gt;&#xA;&lt;p&gt;常見用途: Cloudflare、Nginx、K8s Ingress Controller。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;記法&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Forward Proxy = 幫&lt;strong&gt;客戶端&lt;/strong&gt;出去&lt;/li&gt;&#xA;&lt;li&gt;Reverse Proxy = 幫&lt;strong&gt;伺服器&lt;/strong&gt;接進來&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;nginx-的雙重角色&#34;&gt;&lt;span&gt;Nginx 的雙重角色&lt;/span&gt;&#xA;  &lt;a href=&#34;#nginx-%e7%9a%84%e9%9b%99%e9%87%8d%e8%a7%92%e8%89%b2&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Nginx 同時可以是:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Web Server&lt;/strong&gt;: 直接提供 HTML/CSS/圖片&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Reverse Proxy&lt;/strong&gt;: 把請求轉發給後端應用程式&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;K8s 的 Nginx Ingress Controller 是第二種用法。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;load-balancer&#34;&gt;&lt;span&gt;Load Balancer&lt;/span&gt;&#xA;  &lt;a href=&#34;#load-balancer&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;為什麼需要-load-balancer&#34;&gt;&lt;span&gt;為什麼需要 Load Balancer&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%82%ba%e4%bb%80%e9%ba%bc%e9%9c%80%e8%a6%81-load-balancer&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;單一伺服器處理不了大量流量時, 需要跑多個 Pod。Load Balancer 站在前面, 把請求分散到後面的 Pod。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;l4-vs-l7&#34;&gt;&lt;span&gt;L4 vs L7&lt;/span&gt;&#xA;  &lt;a href=&#34;#l4-vs-l7&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;L4 Load Balancer&lt;/th&gt;&#xA;          &lt;th&gt;L7 Load Balancer&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;運作層&lt;/td&gt;&#xA;          &lt;td&gt;Transport (TCP/UDP)&lt;/td&gt;&#xA;          &lt;td&gt;Application (HTTP)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;看得到&lt;/td&gt;&#xA;          &lt;td&gt;IP + Port&lt;/td&gt;&#xA;          &lt;td&gt;URL, Headers, Cookies&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;路由依據&lt;/td&gt;&#xA;          &lt;td&gt;IP/Port&lt;/td&gt;&#xA;          &lt;td&gt;URL path, hostname, cookie&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;速度&lt;/td&gt;&#xA;          &lt;td&gt;快 (封包檢查少)&lt;/td&gt;&#xA;          &lt;td&gt;較慢 (需解析 HTTP)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;適合&lt;/td&gt;&#xA;          &lt;td&gt;WebSocket, 持久連線&lt;/td&gt;&#xA;          &lt;td&gt;HTTP API, path-based routing&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;strong&gt;K8s 對應&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;ClusterIP Service ≈ L4 (TCP 轉發, 不看 HTTP 內容)&lt;/li&gt;&#xA;&lt;li&gt;Ingress ≈ L7 (根據 URL 路由, 由 Traefik 或 Nginx 實作)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;load-balancing-演算法&#34;&gt;&lt;span&gt;Load Balancing 演算法&lt;/span&gt;&#xA;  &lt;a href=&#34;#load-balancing-%e6%bc%94%e7%ae%97%e6%b3%95&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Round Robin&lt;/strong&gt;: 依序輪流&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;request 1 -&amp;gt; Pod 1&#xA;request 2 -&amp;gt; Pod 2&#xA;request 3 -&amp;gt; Pod 3&#xA;request 4 -&amp;gt; Pod 1  # back to the first&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;適合: 請求處理時間相近的 REST API。LB 幾乎不需要額外計算, 效能最高。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Least Connections&lt;/strong&gt;: 發給目前連線數最少的 Pod&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Pod 1: 100 connections&#xA;Pod 2:  20 connections  # next request goes here&#xA;Pod 3:  80 connections&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;適合: 請求處理時間差異大的服務 (如影片轉檔)。代價是 LB 需要持續追蹤每個 Pod 的連線數, 高流量下 LB 本身有額外負擔。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;IP Hash&lt;/strong&gt;: 同一個 IP 永遠打到同一台 Pod&lt;/p&gt;&#xA;&lt;p&gt;適合: 需要 sticky session 的舊系統 (session 存在特定 server 記憶體裡)。現代系統通常把 session 存 Redis, 不需要 IP Hash。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;k8s-ingress-vs-ingress-controller&#34;&gt;&lt;span&gt;K8s Ingress vs Ingress Controller&lt;/span&gt;&#xA;  &lt;a href=&#34;#k8s-ingress-vs-ingress-controller&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Ingress 分兩層:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Ingress Resource (YAML)&lt;/strong&gt; = 路由規格, 本身不執行任何事&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;rules:&#xA;  - path: /api&#xA;    backend: go-api-service&#xA;  - path: /web&#xA;    backend: frontend-service&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Ingress Controller&lt;/strong&gt; = 真正執行路由的程式, 需要另外安裝或由雲端提供&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Ingress Controller&lt;/th&gt;&#xA;          &lt;th&gt;說明&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Nginx Ingress Controller&lt;/td&gt;&#xA;          &lt;td&gt;最普遍, 自己安裝&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Traefik&lt;/td&gt;&#xA;          &lt;td&gt;k3s 預設內建&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;GKE Ingress&lt;/td&gt;&#xA;          &lt;td&gt;GKE 預設, 用 Google Cloud LB&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;AGIC&lt;/td&gt;&#xA;          &lt;td&gt;AKS 選項, 用 Azure App Gateway&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;pre&gt;&lt;code&gt;kubectl get svc -n kube-system  # check installed ingress controller&#xA;kubectl get ingress -A          # check routing rules&lt;/code&gt;&lt;/pre&gt;&lt;h2 class=&#34;heading-element&#34; id=&#34;websocket-與-l4-lb-的關係&#34;&gt;&lt;span&gt;WebSocket 與 L4 LB 的關係&lt;/span&gt;&#xA;  &lt;a href=&#34;#websocket-%e8%88%87-l4-lb-%e7%9a%84%e9%97%9c%e4%bf%82&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;http-vs-websocket&#34;&gt;&lt;span&gt;HTTP vs WebSocket&lt;/span&gt;&#xA;  &lt;a href=&#34;#http-vs-websocket&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;HTTP 是請求-回應模型, 每個請求獨立, 伺服器無法主動推資料。&lt;/p&gt;&#xA;&lt;p&gt;WebSocket 建立一次連線後持續保持, 伺服器可以隨時主動推資料給客戶端。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;HTTP (polling):&#xA;Client -&amp;gt; Server: &amp;#34;any new messages?&amp;#34;&#xA;Server -&amp;gt; Client: &amp;#34;no&amp;#34;&#xA;Client -&amp;gt; Server: &amp;#34;any new messages?&amp;#34;&#xA;Server -&amp;gt; Client: &amp;#34;yes, here it is&amp;#34;&#xA;&#xA;WebSocket:&#xA;Client &amp;lt;-&amp;gt; Server: (connection established, kept open)&#xA;Server -&amp;gt; Client: &amp;#34;new message!&amp;#34;&#xA;Server -&amp;gt; Client: &amp;#34;another message!&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;為什麼-websocket-需要-l4&#34;&gt;&lt;span&gt;為什麼 WebSocket 需要 L4&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%82%ba%e4%bb%80%e9%ba%bc-websocket-%e9%9c%80%e8%a6%81-l4&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;L7 LB 可能把同一個用戶的不同封包發到不同 Pod, 導致 WebSocket session 斷裂。&lt;/p&gt;&#xA;&lt;p&gt;L4 LB 在 TCP 層運作, 建立連線後整個 session 黏在同一台 Pod, WebSocket 正常運作。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;適合-websocket-的情境&#34;&gt;&lt;span&gt;適合 WebSocket 的情境&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e9%81%a9%e5%90%88-websocket-%e7%9a%84%e6%83%85%e5%a2%83&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;即時客服對話框&lt;/li&gt;&#xA;&lt;li&gt;線上遊戲&lt;/li&gt;&#xA;&lt;li&gt;股票即時報價&lt;/li&gt;&#xA;&lt;li&gt;IoT 設備控制 (如智慧插座, 燈泡)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;linux-cli-補充&#34;&gt;&lt;span&gt;Linux CLI 補充&lt;/span&gt;&#xA;  &lt;a href=&#34;#linux-cli-%e8%a3%9c%e5%85%85&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;stderr-stdout-重新導向&#34;&gt;&lt;span&gt;stderr, stdout, 重新導向&lt;/span&gt;&#xA;  &lt;a href=&#34;#stderr-stdout-%e9%87%8d%e6%96%b0%e5%b0%8e%e5%90%91&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;0 = stdin   (input)&#xA;1 = stdout  (normal output)&#xA;2 = stderr  (error output)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;command &amp;gt; file.log        # redirect stdout to file&#xA;command 2&amp;gt;/dev/null       # discard stderr&#xA;command 2&amp;gt;&amp;amp;1              # merge stderr into stdout&#xA;command &amp;gt; file.log 2&amp;gt;&amp;amp;1   # redirect both stdout and stderr to file&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;| head -40&lt;/code&gt; 只截斷進入 pipe 的 stdout。stderr 不進 pipe, 所以不受 &lt;code&gt;head&lt;/code&gt; 限制。加上 &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; 才能讓 &lt;code&gt;head&lt;/code&gt; 同時截斷兩者。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;常用網路診斷工具&#34;&gt;&lt;span&gt;常用網路診斷工具&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%b8%b8%e7%94%a8%e7%b6%b2%e8%b7%af%e8%a8%ba%e6%96%b7%e5%b7%a5%e5%85%b7&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;工具&lt;/th&gt;&#xA;          &lt;th&gt;用途&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;dig&lt;/code&gt; / &lt;code&gt;nslookup&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;DNS 查詢, 排查 domain 解析問題&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;curl -v&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;HTTP/HTTPS 測試, 看 TLS 憑證和 headers&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;ping&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;測試基本連通性&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;traceroute&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;看封包走哪條路徑&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;netstat&lt;/code&gt; / &lt;code&gt;ss&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;看目前的連線狀態&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Glossary/DNS&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;DNS - MDN Web Docs&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://curl.se/docs/sslcerts.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;How HTTPS works - curl documentation&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;TLS Handshake - Cloudflare Learning&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://developers.cloudflare.com/dns/cname-flattening/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;CNAME Flattening - Cloudflare Docs&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/WebSocket&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;WebSocket - MDN Web Docs&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/ingress/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Ingress - Official Docs&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.github.io/ingress-nginx/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Nginx Ingress Controller&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.k3s.io/networking/networking-services#traefik-ingress-controller&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Traefik on k3s&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item>
    <item>
      <title>Kubernetes 故障模擬: OOMKilled、Probe、ConfigMap 與 Namespace</title>
      <link>https://loustack.dev/zh-tw/kubernetes-failure-modeling-configmap-namespace/</link>
      <pubDate>Thu, 02 Apr 2026 20:00:00 -0400</pubDate><author>louChang.tw@gmail.com (Lou Chang)</author>
      <guid>https://loustack.dev/zh-tw/kubernetes-failure-modeling-configmap-namespace/</guid>
      <category domain="https://loustack.dev/zh-tw/categories/devops/">DevOps</category>
      <category domain="https://loustack.dev/zh-tw/categories/kubernetes/">Kubernetes</category>
      <description>&lt;p&gt;DevOps 學習筆記&lt;/p&gt;&#xA;&lt;p&gt;承接前兩篇 (K8s 核心架構、Service/HPA/Debug), 這篇深入 K8s 故障場景的實際行為, 加上 ConfigMap、Secret、Namespace 的用法 目標是能預測和診斷問題, 而不只是會 apply YAML&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;環境: k3s v1.34 single node cluster + go-api (Go HTTP server, scratch image)&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;oomkilled--container-被-kernel-殺掉&#34;&gt;&lt;span&gt;OOMKilled — Container 被 Kernel 殺掉&lt;/span&gt;&#xA;  &lt;a href=&#34;#oomkilled--container-%e8%a2%ab-kernel-%e6%ae%ba%e6%8e%89&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;什麼是-oomkilled&#34;&gt;&lt;span&gt;什麼是 OOMKilled&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%bb%80%e9%ba%bc%e6%98%af-oomkilled&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Container 的記憶體用量超過 &lt;code&gt;limits.memory&lt;/code&gt; 設定的上限, Linux kernel 的 OOM killer 直接把 process 殺掉&lt;/p&gt;&#xA;&lt;p&gt;不是 K8s 殺的, 是 &lt;strong&gt;kernel&lt;/strong&gt; 殺的 K8s 只是觀察到 container 死了, 紀錄原因&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;實驗&#34;&gt;&lt;span&gt;實驗&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%af%a6%e9%a9%97&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;在 go-api 加一個 &lt;code&gt;/oom&lt;/code&gt; endpoint, 瘋狂配置記憶體:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;func OOM(w http.ResponseWriter, r *http.Request) {&#xA;    log.Println(&amp;#34;oom endpoint hit, allocating memory until killed&amp;#34;)&#xA;    var data [][]byte&#xA;    for {&#xA;        block := make([]byte, 10*1024*1024) // 10MB per iteration&#xA;        data = append(data, block)&#xA;        log.Printf(&amp;#34;allocated %d MB&amp;#34;, len(data)*10)&#xA;    }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Deployment 設定 memory limit 為 64Mi:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;resources:&#xA;  requests:&#xA;    memory: 32Mi&#xA;  limits:&#xA;    memory: 64Mi&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;用 &lt;code&gt;curl&lt;/code&gt; 打 &lt;code&gt;/oom&lt;/code&gt;, 連線直接斷掉 — container 被殺了, 所以 connection 中斷&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;觀察結果&#34;&gt;&lt;span&gt;觀察結果&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e8%a7%80%e5%af%9f%e7%b5%90%e6%9e%9c&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;kubectl describe pod &amp;lt;pod-name&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;Last State:     Terminated&#xA;  Reason:       OOMKilled&#xA;  Exit Code:    137&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;欄位&lt;/th&gt;&#xA;          &lt;th&gt;值&lt;/th&gt;&#xA;          &lt;th&gt;意義&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Reason&lt;/td&gt;&#xA;          &lt;td&gt;OOMKilled&lt;/td&gt;&#xA;          &lt;td&gt;超過 memory limit, 被 OOM killer 終止&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Exit Code&lt;/td&gt;&#xA;          &lt;td&gt;137&lt;/td&gt;&#xA;          &lt;td&gt;128 + 9 = SIGKILL, 不是程式自己退出, 是被外力殺掉的&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;RESTARTS&lt;/td&gt;&#xA;          &lt;td&gt;增加&lt;/td&gt;&#xA;          &lt;td&gt;K8s 自動重啟了 container&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;重啟後因為 &lt;code&gt;/oom&lt;/code&gt; 沒有再被打, Pod 穩定下來 但如果 container &lt;strong&gt;啟動就 OOM&lt;/strong&gt; (比如 init 時就吃太多記憶體), 就會不斷重啟, 最終進入 CrashLoopBackOff&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;oomkilled-vs-crashloopbackoff&#34;&gt;&lt;span&gt;OOMKilled vs CrashLoopBackOff&lt;/span&gt;&#xA;  &lt;a href=&#34;#oomkilled-vs-crashloopbackoff&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;這兩個常常搞混, 但它們不是同一個層級:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;OOMKilled&lt;/strong&gt; 是&lt;strong&gt;原因&lt;/strong&gt; — 記憶體超標被殺&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;CrashLoopBackOff&lt;/strong&gt; 是&lt;strong&gt;狀態&lt;/strong&gt; — container 反覆 crash, K8s 拉長重啟間隔 (指數退避: 10s → 20s → 40s → &amp;hellip; 最多 5 分鐘)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;一個 container 可以因為 OOMKilled 而進入 CrashLoopBackOff, 也可以因為其他原因 (程式碼 bug、設定錯誤) 進入 CrashLoopBackOff&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;requests-vs-limits--兩個完全不同的東西&#34;&gt;&lt;span&gt;requests vs limits — 兩個完全不同的東西&lt;/span&gt;&#xA;  &lt;a href=&#34;#requests-vs-limits--%e5%85%a9%e5%80%8b%e5%ae%8c%e5%85%a8%e4%b8%8d%e5%90%8c%e7%9a%84%e6%9d%b1%e8%a5%bf&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;resources:&#xA;  requests:&#xA;    memory: 32Mi    # used by Scheduler&#xA;  limits:&#xA;    memory: 64Mi    # runtime hard cap&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;requests--我至少需要這麼多&#34;&gt;&lt;span&gt;requests — 「我至少需要這麼多」&lt;/span&gt;&#xA;  &lt;a href=&#34;#requests--%e6%88%91%e8%87%b3%e5%b0%91%e9%9c%80%e8%a6%81%e9%80%99%e9%ba%bc%e5%a4%9a&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Scheduler 用 requests 決定 Pod 放到哪個 Node 如果 Node 剩餘可分配資源 &amp;lt; Pod 的 requests, Scheduler 就不會把 Pod 排到那個 Node, Pod 狀態會是 &lt;strong&gt;Pending&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;limits--最多只能用這麼多&#34;&gt;&lt;span&gt;limits — 「最多只能用這麼多」&lt;/span&gt;&#xA;  &lt;a href=&#34;#limits--%e6%9c%80%e5%a4%9a%e5%8f%aa%e8%83%bd%e7%94%a8%e9%80%99%e9%ba%bc%e5%a4%9a&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Container 實際執行時的硬上限 超過就被 OOM kill&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;關鍵差異&#34;&gt;&lt;span&gt;關鍵差異&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e9%97%9c%e9%8d%b5%e5%b7%ae%e7%95%b0&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;requests&lt;/th&gt;&#xA;          &lt;th&gt;limits&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;誰看&lt;/td&gt;&#xA;          &lt;td&gt;Scheduler&lt;/td&gt;&#xA;          &lt;td&gt;Kernel (cgroup)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;什麼時候看&lt;/td&gt;&#xA;          &lt;td&gt;排程時&lt;/td&gt;&#xA;          &lt;td&gt;執行時&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;超過會怎樣&lt;/td&gt;&#xA;          &lt;td&gt;Pod Pending (排不上去)&lt;/td&gt;&#xA;          &lt;td&gt;OOMKilled (被殺掉)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;實驗-resourcequota-模擬資源不足&#34;&gt;&lt;span&gt;實驗: ResourceQuota 模擬資源不足&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%af%a6%e9%a9%97-resourcequota-%e6%a8%a1%e6%93%ac%e8%b3%87%e6%ba%90%e4%b8%8d%e8%b6%b3&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;因為本機 k3s node 有 39GB 記憶體, 用小小的 requests 根本排不滿 改用 ResourceQuota 在 namespace 層級限制:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;apiVersion: v1&#xA;kind: ResourceQuota&#xA;metadata:&#xA;  name: small-quota&#xA;  namespace: resource-test&#xA;spec:&#xA;  hard:&#xA;    requests.memory: 100Mi&#xA;    limits.memory: 200Mi&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Deploy 4 個 Pod, 各 requests 32Mi (4 × 32Mi = 128Mi &amp;gt; quota 100Mi):&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Warning  FailedCreate  replicaset/go-api-hungry-xxx&#xA;  Error creating: pods &amp;#34;...&amp;#34; is forbidden:&#xA;  exceeded quota: small-quota,&#xA;  requested: requests.memory=32Mi,&#xA;  used: requests.memory=96Mi,&#xA;  limited: requests.memory=100Mi&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;3 個 Pod 成功, 第 4 個被擋 注意這裡和真正的 Node 資源不足不同:&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;情境&lt;/th&gt;&#xA;          &lt;th&gt;錯誤來源&lt;/th&gt;&#xA;          &lt;th&gt;Pod 狀態&lt;/th&gt;&#xA;          &lt;th&gt;Event&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;ResourceQuota 超過&lt;/td&gt;&#xA;          &lt;td&gt;API server 直接拒絕&lt;/td&gt;&#xA;          &lt;td&gt;Pod 不存在&lt;/td&gt;&#xA;          &lt;td&gt;FailedCreate on ReplicaSet&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Node 資源不足&lt;/td&gt;&#xA;          &lt;td&gt;Scheduler 找不到合適 Node&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Pending&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;FailedScheduling on Pod&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;probe--k8s-怎麼知道你的-app-是不是正常的&#34;&gt;&lt;span&gt;Probe — K8s 怎麼知道你的 App 是不是正常的&lt;/span&gt;&#xA;  &lt;a href=&#34;#probe--k8s-%e6%80%8e%e9%ba%bc%e7%9f%a5%e9%81%93%e4%bd%a0%e7%9a%84-app-%e6%98%af%e4%b8%8d%e6%98%af%e6%ad%a3%e5%b8%b8%e7%9a%84&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Container 在跑不代表 app 正常 — 可能 deadlock 了, 可能 database 斷了 K8s 需要一個方法去確認&lt;/p&gt;&#xA;&lt;p&gt;Probe 就是 K8s &lt;strong&gt;定期打你指定的 endpoint&lt;/strong&gt;, 看 HTTP status code:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;200 → 正常&lt;/li&gt;&#xA;&lt;li&gt;非 200 → 有問題&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;兩種-probe-的差別&#34;&gt;&lt;span&gt;兩種 Probe 的差別&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%85%a9%e7%a8%ae-probe-%e7%9a%84%e5%b7%ae%e5%88%a5&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;&lt;/th&gt;&#xA;          &lt;th&gt;Readiness Probe&lt;/th&gt;&#xA;          &lt;th&gt;Liveness Probe&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;問的問題&lt;/td&gt;&#xA;          &lt;td&gt;「你準備好接流量了嗎?」&lt;/td&gt;&#xA;          &lt;td&gt;「你還活著嗎?」&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;失敗後果&lt;/td&gt;&#xA;          &lt;td&gt;從 Service 移除, &lt;strong&gt;不送流量&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;砍掉 container, 重啟&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Pod 還在嗎&lt;/td&gt;&#xA;          &lt;td&gt;在, 只是不接客&lt;/td&gt;&#xA;          &lt;td&gt;container 被殺, 重新啟動&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;用途&lt;/td&gt;&#xA;          &lt;td&gt;啟動中、暫時忙碌、依賴斷了&lt;/td&gt;&#xA;          &lt;td&gt;真的卡死了, 需要重啟才能救&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;用餐廳比喻:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Readiness&lt;/strong&gt; = 「廚房準備好了嗎?」→ 沒好就先不帶客人進來, 但廚房還在&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Liveness&lt;/strong&gt; = 「廚師還有呼吸嗎?」→ 沒有就換一個廚師 (重啟)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;設定方式&#34;&gt;&lt;span&gt;設定方式&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e8%a8%ad%e5%ae%9a%e6%96%b9%e5%bc%8f&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;在 container spec 裡用不同的 key 名稱:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;containers:&#xA;  - name: go-api&#xA;    image: go-api:0.0.5&#xA;    readinessProbe:         # ← this key makes it a readiness probe&#xA;      httpGet:&#xA;        path: /ready&#xA;        port: 8080&#xA;      initialDelaySeconds: 2&#xA;      periodSeconds: 5&#xA;    livenessProbe:          # ← this key makes it a liveness probe&#xA;      httpGet:&#xA;        path: /alive&#xA;        port: 8080&#xA;      initialDelaySeconds: 5&#xA;      periodSeconds: 3&#xA;      failureThreshold: 3   # 3 consecutive failures = dead&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;可以同時設兩個, 也可以指向同一個 endpoint Production 裡通常分開: readiness 檢查比較多 (database、cache), liveness 只檢查 app 本身有沒有卡死&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;readiness-probe-實驗&#34;&gt;&lt;span&gt;Readiness Probe 實驗&lt;/span&gt;&#xA;  &lt;a href=&#34;#readiness-probe-%e5%af%a6%e9%a9%97&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;設定 readiness probe 指向不存在的 &lt;code&gt;/ready&lt;/code&gt; endpoint, probe 每 5 秒打一次都拿到 404:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;kubectl get pods -l app=go-api-probe-test&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;NAME                                 READY   STATUS    RESTARTS   AGE&#xA;go-api-probe-test-86cd4c5787-7lmrj   0/1     Running   0          39s&#xA;go-api-probe-test-86cd4c5787-mtxl2   0/1     Running   0          39s&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;READY 是 &lt;code&gt;0/1&lt;/code&gt; — container 在跑, 但 K8s 認定沒準備好&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;kubectl describe pod &amp;lt;pod-name&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;Conditions:&#xA;  Ready       False&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;檢查 EndpointSlice:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;kubectl get endpointslice -l kubernetes.io/service-name=go-api-probe-test&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;EndpointSlice 會列出所有 address, 但每個 address 標記 &lt;code&gt;ready: false&lt;/code&gt; Service 不會送流量到 &lt;code&gt;ready: false&lt;/code&gt; 的 Pod&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;注意: K8s v1.33+ 開始 &lt;code&gt;kubectl get endpoints&lt;/code&gt; 會出 deprecation warning, 改用 &lt;code&gt;kubectl get endpointslice -l kubernetes.io/service-name=&amp;lt;svc&amp;gt;&lt;/code&gt; 查&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;liveness-probe-實驗&#34;&gt;&lt;span&gt;Liveness Probe 實驗&lt;/span&gt;&#xA;  &lt;a href=&#34;#liveness-probe-%e5%af%a6%e9%a9%97&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;設定 liveness probe 指向不存在的 &lt;code&gt;/alive&lt;/code&gt; endpoint, 每 3 秒失敗一次, 連續 3 次後 K8s 殺掉 container:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;kubectl get pods -l app=go-api-liveness-test -w&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;NAME                                   READY   STATUS             RESTARTS     AGE&#xA;go-api-liveness-test-95bb8f89c-7qn85   1/1     Running            3 (8s ago)   45s&#xA;go-api-liveness-test-95bb8f89c-7qn85   0/1     CrashLoopBackOff   3 (0s ago)   49s&#xA;go-api-liveness-test-95bb8f89c-7qn85   1/1     Running            4 (29s ago)  78s&#xA;go-api-liveness-test-95bb8f89c-7qn85   0/1     CrashLoopBackOff   4 (0s ago)   88s&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;和 readiness 完全不同 — RESTARTS 不斷增加, container 被殺了又重啟, 最終進入 CrashLoopBackOff&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;什麼時候用哪個--常見錯誤&#34;&gt;&lt;span&gt;什麼時候用哪個 — 常見錯誤&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%bb%80%e9%ba%bc%e6%99%82%e5%80%99%e7%94%a8%e5%93%aa%e5%80%8b--%e5%b8%b8%e8%a6%8b%e9%8c%af%e8%aa%a4&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;一個常見的錯誤: &lt;strong&gt;用 liveness probe 檢查 database 連線&lt;/strong&gt; 如果 database 掛了, liveness 失敗 → K8s 重啟你的 app → app 起來後 database 還是掛著 → 又 liveness 失敗 → 無限重啟&lt;/p&gt;&#xA;&lt;p&gt;正確做法: &lt;strong&gt;database 連線放 readiness probe&lt;/strong&gt; database 掛了, readiness 失敗, Pod 只是暫時不接流量, 等 database 恢復就自動回來, 不需要重啟&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;configmap-與-secret&#34;&gt;&lt;span&gt;ConfigMap 與 Secret&lt;/span&gt;&#xA;  &lt;a href=&#34;#configmap-%e8%88%87-secret&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;解決什麼問題&#34;&gt;&lt;span&gt;解決什麼問題&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e8%a7%a3%e6%b1%ba%e4%bb%80%e9%ba%bc%e5%95%8f%e9%a1%8c&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;App 需要設定值 (環境、log level、DB 密碼) 如果寫死在 code 裡, 改設定就要重新 build image, 不同環境 (dev/staging/prod) 要不同 image&lt;/p&gt;&#xA;&lt;p&gt;ConfigMap 和 Secret 把設定從 image 抽出來, 同一個 image 在不同環境只換設定&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;configmap--非敏感設定&#34;&gt;&lt;span&gt;ConfigMap — 非敏感設定&lt;/span&gt;&#xA;  &lt;a href=&#34;#configmap--%e9%9d%9e%e6%95%8f%e6%84%9f%e8%a8%ad%e5%ae%9a&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;一個 key-value 的設定檔, 存在 K8s 裡:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;apiVersion: v1&#xA;kind: ConfigMap&#xA;metadata:&#xA;  name: go-api-config&#xA;data:&#xA;  APP_ENV: &amp;#34;staging&amp;#34;&#xA;  LOG_LEVEL: &amp;#34;debug&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;secret--敏感資料&#34;&gt;&lt;span&gt;Secret — 敏感資料&lt;/span&gt;&#xA;  &lt;a href=&#34;#secret--%e6%95%8f%e6%84%9f%e8%b3%87%e6%96%99&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;和 ConfigMap 幾乎一樣, 但用來放密碼、API key:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;apiVersion: v1&#xA;kind: Secret&#xA;metadata:&#xA;  name: go-api-secret&#xA;type: Opaque&#xA;stringData:&#xA;  DB_PASSWORD: &amp;#34;my-secret-password-123&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;type: Opaque&lt;/code&gt; 是 Secret 的類型, 表示「一般用途的 secret」 90% 的情況用 Opaque&lt;/p&gt;&#xA;&lt;p&gt;其他內建類型:&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;type&lt;/th&gt;&#xA;          &lt;th&gt;用途&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Opaque&lt;/td&gt;&#xA;          &lt;td&gt;通用, 自己定義 key/value (最常用)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;kubernetes.io/tls&lt;/td&gt;&#xA;          &lt;td&gt;TLS 憑證 (必須有 tls.crt 和 tls.key)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;kubernetes.io/dockerconfigjson&lt;/td&gt;&#xA;          &lt;td&gt;Docker registry 登入帳密&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;注入--把值傳進-container&#34;&gt;&lt;span&gt;注入 — 把值傳進 Container&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e6%b3%a8%e5%85%a5--%e6%8a%8a%e5%80%bc%e5%82%b3%e9%80%b2-container&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Container 本身只認兩種東西: &lt;strong&gt;環境變數&lt;/strong&gt;和&lt;strong&gt;檔案&lt;/strong&gt; 需要把 ConfigMap / Secret 的值變成 container 讀得到的格式&lt;/p&gt;&#xA;&lt;p&gt;方式一 — 環境變數:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;containers:&#xA;  - name: go-api&#xA;    envFrom:&#xA;      - configMapRef:&#xA;          name: go-api-config    # all keys become env vars&#xA;    env:&#xA;      - name: DB_PASSWORD&#xA;        valueFrom:&#xA;          secretKeyRef:&#xA;            name: go-api-secret&#xA;            key: DB_PASSWORD     # pick a specific key from Secret&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;envFrom&lt;/code&gt; 把整個 ConfigMap 攤開, &lt;code&gt;valueFrom&lt;/code&gt; 從 Secret 挑單一個 key&lt;/p&gt;&#xA;&lt;p&gt;方式二 — 掛成檔案 (volume mount):&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;volumes:&#xA;  - name: config&#xA;    configMap:&#xA;      name: go-api-config&#xA;volumeMounts:&#xA;  - name: config&#xA;    mountPath: /etc/config&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Container 裡會有 &lt;code&gt;/etc/config/APP_ENV&lt;/code&gt; 這個檔案, 內容是 &lt;code&gt;staging&lt;/code&gt; 適合設定檔比較大的情況 (例如 nginx.conf)&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;實驗結果&#34;&gt;&lt;span&gt;實驗結果&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%af%a6%e9%a9%97%e7%b5%90%e6%9e%9c&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Go code 用 &lt;code&gt;os.Getenv&lt;/code&gt; 讀取:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;kubectl logs -l app=go-api-config-test&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;2026/04/02 23:17:11 env=staging log_level=debug db_configured=true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;三個值都成功注入&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;k8s-secret-安全嗎&#34;&gt;&lt;span&gt;K8s Secret 安全嗎&lt;/span&gt;&#xA;  &lt;a href=&#34;#k8s-secret-%e5%ae%89%e5%85%a8%e5%97%8e&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;K8s Secret 預設&lt;strong&gt;只是 base64 編碼, 不是加密&lt;/strong&gt; 任何有 kubectl 權限的人都能:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;kubectl get secret go-api-secret -o yaml&#xA;# base64 encoded, decode = plaintext&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Production 裡不會直接把密碼寫在 Secret YAML 然後 commit 到 git 實際做法:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;External Secret Manager (GCP Secret Manager / Vault / AWS Secrets Manager)&#xA;         ↓ (synced by tooling, not manual)&#xA;    K8s Secret object&#xA;         ↓ (envFrom / valueFrom)&#xA;    Container env vars&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;中間同步的「工具」通常是:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;External Secrets Operator&lt;/strong&gt; — 裝在 K8s 裡, 定期從外部 secret manager 同步到 K8s Secret&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;CI/CD pipeline&lt;/strong&gt; — deploy 時用 &lt;code&gt;kubectl create secret&lt;/code&gt; 建出來&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Terraform&lt;/strong&gt; — 用 &lt;code&gt;kubernetes_secret&lt;/code&gt; resource 建出來&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;K8s Secret 是外部 secret manager 和 container 之間的&lt;strong&gt;橋樑&lt;/strong&gt; — container 不會直接呼叫 GCP Secret Manager API, 它只從 K8s Secret 讀環境變數&lt;/p&gt;&#xA;&lt;p&gt;如果用 GCP Cloud Run (不是 K8s), 則不需要這層橋樑 — Cloud Run 可以直接從 GCP Secret Manager 注入環境變數&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;namespace--cluster-裡的隔離區域&#34;&gt;&lt;span&gt;Namespace — Cluster 裡的隔離區域&lt;/span&gt;&#xA;  &lt;a href=&#34;#namespace--cluster-%e8%a3%a1%e7%9a%84%e9%9a%94%e9%9b%a2%e5%8d%80%e5%9f%9f&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;什麼是-namespace&#34;&gt;&lt;span&gt;什麼是 Namespace&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%bb%80%e9%ba%bc%e6%98%af-namespace&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;想像一個 cluster 是一棟辦公大樓:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Namespace = 樓層&lt;/strong&gt; — 每個樓層有自己的房間 (Pod)、服務 (Service)、設定 (ConfigMap)&lt;/li&gt;&#xA;&lt;li&gt;不同樓層可以有一樣名字的房間, 互不衝突&lt;/li&gt;&#xA;&lt;li&gt;但大樓的電梯 (Node)、停車場 (Storage) 是共用的&lt;/li&gt;&#xA;&lt;li&gt;預設&lt;strong&gt;樓層之間的走廊是通的&lt;/strong&gt; (網路互通)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;不是 .NET 那種 code 組織用的 namespace, 也不是 Linux kernel 的 namespace (process 隔離) K8s Namespace 是&lt;strong&gt;資源的邏輯分群&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;用途&#34;&gt;&lt;span&gt;用途&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e7%94%a8%e9%80%94&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;cluster&#xA;├── namespace: dev        ← development&#xA;├── namespace: staging    ← testing&#xA;├── namespace: prod       ← production&#xA;└── namespace: default    ← default, used when no namespace specified&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;什麼隔離-什麼不隔離&#34;&gt;&lt;span&gt;什麼隔離, 什麼不隔離&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e4%bb%80%e9%ba%bc%e9%9a%94%e9%9b%a2-%e4%bb%80%e9%ba%bc%e4%b8%8d%e9%9a%94%e9%9b%a2&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Namespace 各自獨立的:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Pod、Deployment、Service、ConfigMap、Secret — 名字可以重複, 互不干擾&lt;/li&gt;&#xA;&lt;li&gt;ResourceQuota — 可以對不同 namespace 設不同資源配額&lt;/li&gt;&#xA;&lt;li&gt;RBAC 權限 — 可以限制某人只能操作某個 namespace (RBAC = Role-Based Access Control, 用角色控制誰能做什麼)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;整個 Cluster 共用的:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Node — 所有 namespace 的 Pod 都跑在同一批機器上&lt;/li&gt;&#xA;&lt;li&gt;Storage (PersistentVolume)&lt;/li&gt;&#xA;&lt;li&gt;網路 — &lt;strong&gt;預設跨 namespace 可以互通&lt;/strong&gt;, 要擋要另外設 NetworkPolicy&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;跨-namespace-網路互通實驗&#34;&gt;&lt;span&gt;跨 Namespace 網路互通實驗&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e8%b7%a8-namespace-%e7%b6%b2%e8%b7%af%e4%ba%92%e9%80%9a%e5%af%a6%e9%a9%97&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;建立 dev 和 staging namespace, 各自部署 go-api 和 Service (名字都叫 go-api, 不衝突)&lt;/p&gt;&#xA;&lt;p&gt;從 dev 的 Pod 打 staging 的 Service:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# run a Pod with curl in dev namespace&#xA;kubectl run test-curl -n dev --image=curlimages/curl -- sleep 3600&#xA;&#xA;# from dev, call staging service&#xA;kubectl exec -n dev test-curl -- curl -s http://go-api.staging.svc.cluster.local/healthz&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;{&amp;#34;status&amp;#34;:&amp;#34;ok&amp;#34;,&amp;#34;version&amp;#34;:&amp;#34;0.0.4&amp;#34;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;成功拿到回應, 證明&lt;strong&gt;跨 namespace 網路預設是通的&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;K8s 內部的 DNS 規則: &lt;code&gt;&amp;lt;service-name&amp;gt;.&amp;lt;namespace&amp;gt;.svc.cluster.local&lt;/code&gt; 同 namespace 內可以省略, 直接用 service name&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;注意: scratch image 裡面沒有任何工具 (連 wget、curl、sh 都沒有), 所以不能 &lt;code&gt;kubectl exec&lt;/code&gt; 進去 debug 需要另外跑一個有工具的 Pod 來測&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;必須記住的東西&#34;&gt;&lt;span&gt;必須記住的東西&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e5%bf%85%e9%a0%88%e8%a8%98%e4%bd%8f%e7%9a%84%e6%9d%b1%e8%a5%bf&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;故障診斷對照表&#34;&gt;&lt;span&gt;故障診斷對照表&lt;/span&gt;&#xA;  &lt;a href=&#34;#%e6%95%85%e9%9a%9c%e8%a8%ba%e6%96%b7%e5%b0%8d%e7%85%a7%e8%a1%a8&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;現象&lt;/th&gt;&#xA;          &lt;th&gt;原因&lt;/th&gt;&#xA;          &lt;th&gt;怎麼查&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;OOMKilled (exit code 137)&lt;/td&gt;&#xA;          &lt;td&gt;超過 memory limit&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;kubectl describe pod&lt;/code&gt; 看 Last State&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;CrashLoopBackOff&lt;/td&gt;&#xA;          &lt;td&gt;Container 反覆 crash&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;kubectl logs --previous&lt;/code&gt; 看上次的 log&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Pending&lt;/td&gt;&#xA;          &lt;td&gt;Node 資源不足或 Scheduler 排不上&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;kubectl describe pod&lt;/code&gt; 看 Events&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;FailedCreate&lt;/td&gt;&#xA;          &lt;td&gt;ResourceQuota 超標&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;kubectl get events&lt;/code&gt; 看 ReplicaSet event&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;READY 0/1 但 Running&lt;/td&gt;&#xA;          &lt;td&gt;Readiness probe 失敗&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;kubectl describe pod&lt;/code&gt; 看 Conditions&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;RESTARTS 不斷增加&lt;/td&gt;&#xA;          &lt;td&gt;Liveness probe 失敗或 app crash&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;kubectl describe pod&lt;/code&gt; 看 probe 設定&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;probe-選擇原則&#34;&gt;&lt;span&gt;Probe 選擇原則&lt;/span&gt;&#xA;  &lt;a href=&#34;#probe-%e9%81%b8%e6%93%87%e5%8e%9f%e5%89%87&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;檢查 app 本身卡死 → &lt;strong&gt;liveness&lt;/strong&gt; (重啟能救)&lt;/li&gt;&#xA;&lt;li&gt;檢查外部依賴 (DB、cache) → &lt;strong&gt;readiness&lt;/strong&gt; (重啟沒用, 等依賴恢復就好)&lt;/li&gt;&#xA;&lt;li&gt;不確定的時候 → 先用 &lt;strong&gt;readiness&lt;/strong&gt;, 比 liveness 安全&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;requests-vs-limits-一句話&#34;&gt;&lt;span&gt;requests vs limits 一句話&lt;/span&gt;&#xA;  &lt;a href=&#34;#requests-vs-limits-%e4%b8%80%e5%8f%a5%e8%a9%b1&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;requests&lt;/strong&gt; = Scheduler 排程用, 「我至少需要這麼多才能被排到 Node 上」&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;limits&lt;/strong&gt; = Runtime 上限, 「超過這麼多就被殺」&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Documentation — Container Resources&lt;/a&gt; — requests 與 limits 的完整說明&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Documentation — Pod Lifecycle&lt;/a&gt; — Pod 狀態與 restart policy&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Documentation — Liveness, Readiness and Startup Probes&lt;/a&gt; — 三種 probe 的設定與差異&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/configmap/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Documentation — ConfigMaps&lt;/a&gt; — ConfigMap 的建立與使用方式&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/secret/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Documentation — Secrets&lt;/a&gt; — Secret 類型、建立、注入方式&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Documentation — Namespaces&lt;/a&gt; — Namespace 的用途與隔離邊界&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/policy/resource-quotas/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Documentation — Resource Quotas&lt;/a&gt; — 用 ResourceQuota 限制 namespace 資源用量&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Documentation — DNS for Services and Pods&lt;/a&gt; — 跨 namespace 的 DNS 規則&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Kubernetes Documentation — EndpointSlice&lt;/a&gt; — EndpointSlice 取代舊版 Endpoints&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item>
  </channel>
</rss>
