<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Network on Tarragon</title><link>https://tarrragon.github.io/blog/tags/network/</link><description>Recent content in Network on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/network/index.xml" rel="self" type="application/rss+xml"/><item><title>網路地基 — VPC、subnet 分層與 security group 設計</title><link>https://tarrragon.github.io/blog/infra/03-network-foundation/vpc-subnet-security-group/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/03-network-foundation/vpc-subnet-security-group/</guid><description>&lt;p>網路地基要先於核心服務存在。VPC、subnet、route table 與 security group 構成一張「服務能落在哪、誰能跟誰講話」的地圖，資料庫、運算節點與對外入口都得落在這張地圖規劃好的格子裡。先把邊界畫清楚，後面每個核心服務上線時只需要選一塊已經定義好安全等級的位置，而不是邊開服務邊補洞。&lt;/p>
&lt;p>這篇文章建立四層邊界：最外層的 VPC 隔離、中層的 public / private subnet 切分、流量進出的 route table 與 NAT、以及最貼近服務的 security group。每一層解決的問題不同，疊起來才是一個可審計、可收斂的網路。&lt;/p>
&lt;h2 id="vpc網路隔離的最外層邊界">VPC：網路隔離的最外層邊界&lt;/h2>
&lt;p>VPC（Virtual Private Cloud）先圈定整個系統的網路地址空間 — 一塊邏輯隔離的私有網段，是其餘所有網路切分的起點。在 VPC 裡開出來的所有資源預設只看得到同一個 VPC 內的成員，與其他 VPC、與其他帳號的網路天然隔離。它是後面所有切分動作的容器 — 沒有 VPC，subnet 與 security group 無處依附。&lt;/p>
&lt;h3 id="cidr-規劃一次決定事後難改">CIDR 規劃：一次決定、事後難改&lt;/h3>
&lt;p>建立 VPC 時最關鍵的決策是 CIDR 區塊的大小。這個範圍要一次規劃足夠大，因為事後擴張地址空間在多數雲上是麻煩且容易出錯的操作。AWS 雖然允許在 VPC 上追加 secondary CIDR，但追加的網段不能與原有的重疊，也不是所有服務都能自然使用跨 CIDR 的 subnet，routing 的複雜度會因此上升。&lt;/p>
&lt;p>CIDR 規劃要同時考慮三件事。第一是容量：&lt;code>/16&lt;/code> 提供約六萬五千個位址，對多數單一環境的 VPC 足夠寬裕，切成 &lt;code>/24&lt;/code> 的 subnet 也有 256 個可用子網。第二是不重疊：未來若要透過 VPC peering、Transit Gateway 或 VPN 把這個 VPC 接回地端機房或其他環境，重疊的 CIDR 會讓路由無法解析。三個環境各自是 &lt;code>10.0.0.0/16&lt;/code>，在彼此不需要互連時不是問題，但一旦要開 peering 就會撞車 — 這時候改 CIDR 的代價是重建整個 VPC。第三是預留：如果公司同時有多個 VPC（不同環境或不同產品線），用連續但不重疊的大段分配（如 dev &lt;code>10.0.0.0/16&lt;/code>、staging &lt;code>10.1.0.0/16&lt;/code>、prod &lt;code>10.2.0.0/16&lt;/code>）讓路由表更乾淨。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_vpc&amp;#34; &amp;#34;main&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n"> cidr_block&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;10.0.0.0/16&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n"> enable_dns_support&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> enable_dns_hostnames&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> tags&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> Name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;platform-prod&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> Environment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;production&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>enable_dns_support&lt;/code> 和 &lt;code>enable_dns_hostnames&lt;/code> 在多數場景都該開啟。沒開 DNS hostname 時，EC2 instance 不會拿到可解析的 hostname，某些服務依賴 DNS 尋址而非 IP（如 VPC endpoint 的 private DNS），關著會讓它們靜靜失敗而不報錯。&lt;/p>
&lt;p>判讀訊號：規劃 CIDR 時先問「這個環境三年後會有幾個 subnet、跨幾個可用區、要不要跟其他 VPC 或地端互連」。風險集中在地址耗盡與網段衝突 — 兩者都得在開第一個 subnet 之前定案。VPC 只負責隔離與定址，它不決定哪個服務能對外，那是 subnet 與 security group 的工作。環境之間的 VPC 該怎麼分，是&lt;a href="https://tarrragon.github.io/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化&lt;/a>的主題。&lt;/p>
&lt;h2 id="public-與-private-subnet-的切分原則">public 與 private subnet 的切分原則&lt;/h2>
&lt;p>一塊資源對外暴露到什麼程度，取決於它被放進哪個 subnet。VPC 內部按可用區與暴露程度切出來的子網段，決定資源有沒有一條通往網際網路的路徑。判斷一個資源該放 public 還是 private，問題只有一個：它需不需要被網際網路直接定址。&lt;/p>
&lt;h3 id="兩類-subnet-的定位">兩類 subnet 的定位&lt;/h3>
&lt;p>public subnet 放的是必須接收外部入站流量的元件 — 對外的負載平衡器、NAT Gateway、堡壘主機（bastion）。這些資源透過 route table 連到 Internet Gateway，因此能被外部 IP 直接觸及。private subnet 放的是只該在內網被存取的元件 — 應用伺服器、資料庫、快取、內部佇列。它們沒有通往 Internet Gateway 的路由，外部無法主動連入，需要對外時才透過 NAT 出去。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Subnet 類型&lt;/th>
 &lt;th>典型住戶&lt;/th>
 &lt;th>對外路徑&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>public&lt;/td>
 &lt;td>對外 LB、NAT Gateway、bastion&lt;/td>
 &lt;td>經 Internet Gateway 雙向&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>private&lt;/td>
 &lt;td>應用節點、資料庫、快取、佇列&lt;/td>
 &lt;td>僅經 NAT 單向出站、不可入站&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>public subnet 的真實樣貌是「薄薄一層」：它通常只住負載平衡器與 NAT 這類入口設施，而不是業務邏輯。常見陷阱是為了 SSH 方便把應用伺服器直接開在 public subnet 並配公網 IP，等於把每一台業務主機的管理埠暴露在掃描流量下 — 全球的 bot 會在秒級頻率對公網 IP 的 22 埠嘗試登入。private subnet 的住戶反而是系統的主體 — 資料庫放這裡是因為它一旦能被外網定址，攻擊面就從「打穿入口層」變成「直接連資料庫埠試密碼」。&lt;/p></description><content:encoded><![CDATA[<p>網路地基要先於核心服務存在。VPC、subnet、route table 與 security group 構成一張「服務能落在哪、誰能跟誰講話」的地圖，資料庫、運算節點與對外入口都得落在這張地圖規劃好的格子裡。先把邊界畫清楚，後面每個核心服務上線時只需要選一塊已經定義好安全等級的位置，而不是邊開服務邊補洞。</p>
<p>這篇文章建立四層邊界：最外層的 VPC 隔離、中層的 public / private subnet 切分、流量進出的 route table 與 NAT、以及最貼近服務的 security group。每一層解決的問題不同，疊起來才是一個可審計、可收斂的網路。</p>
<h2 id="vpc網路隔離的最外層邊界">VPC：網路隔離的最外層邊界</h2>
<p>VPC（Virtual Private Cloud）先圈定整個系統的網路地址空間 — 一塊邏輯隔離的私有網段，是其餘所有網路切分的起點。在 VPC 裡開出來的所有資源預設只看得到同一個 VPC 內的成員，與其他 VPC、與其他帳號的網路天然隔離。它是後面所有切分動作的容器 — 沒有 VPC，subnet 與 security group 無處依附。</p>
<h3 id="cidr-規劃一次決定事後難改">CIDR 規劃：一次決定、事後難改</h3>
<p>建立 VPC 時最關鍵的決策是 CIDR 區塊的大小。這個範圍要一次規劃足夠大，因為事後擴張地址空間在多數雲上是麻煩且容易出錯的操作。AWS 雖然允許在 VPC 上追加 secondary CIDR，但追加的網段不能與原有的重疊，也不是所有服務都能自然使用跨 CIDR 的 subnet，routing 的複雜度會因此上升。</p>
<p>CIDR 規劃要同時考慮三件事。第一是容量：<code>/16</code> 提供約六萬五千個位址，對多數單一環境的 VPC 足夠寬裕，切成 <code>/24</code> 的 subnet 也有 256 個可用子網。第二是不重疊：未來若要透過 VPC peering、Transit Gateway 或 VPN 把這個 VPC 接回地端機房或其他環境，重疊的 CIDR 會讓路由無法解析。三個環境各自是 <code>10.0.0.0/16</code>，在彼此不需要互連時不是問題，但一旦要開 peering 就會撞車 — 這時候改 CIDR 的代價是重建整個 VPC。第三是預留：如果公司同時有多個 VPC（不同環境或不同產品線），用連續但不重疊的大段分配（如 dev <code>10.0.0.0/16</code>、staging <code>10.1.0.0/16</code>、prod <code>10.2.0.0/16</code>）讓路由表更乾淨。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_vpc&#34; &#34;main&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  cidr_block</span>           <span class="o">=</span> <span class="s2">&#34;10.0.0.0/16&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  enable_dns_support</span>   <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  enable_dns_hostnames</span> <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">    Name</span>        <span class="o">=</span> <span class="s2">&#34;platform-prod&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">    Environment</span> <span class="o">=</span> <span class="s2">&#34;production&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  }
</span></span><span class="line"><span class="ln">10</span><span class="cl">}</span></span></code></pre></div><p><code>enable_dns_support</code> 和 <code>enable_dns_hostnames</code> 在多數場景都該開啟。沒開 DNS hostname 時，EC2 instance 不會拿到可解析的 hostname，某些服務依賴 DNS 尋址而非 IP（如 VPC endpoint 的 private DNS），關著會讓它們靜靜失敗而不報錯。</p>
<p>判讀訊號：規劃 CIDR 時先問「這個環境三年後會有幾個 subnet、跨幾個可用區、要不要跟其他 VPC 或地端互連」。風險集中在地址耗盡與網段衝突 — 兩者都得在開第一個 subnet 之前定案。VPC 只負責隔離與定址，它不決定哪個服務能對外，那是 subnet 與 security group 的工作。環境之間的 VPC 該怎麼分，是<a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>的主題。</p>
<h2 id="public-與-private-subnet-的切分原則">public 與 private subnet 的切分原則</h2>
<p>一塊資源對外暴露到什麼程度，取決於它被放進哪個 subnet。VPC 內部按可用區與暴露程度切出來的子網段，決定資源有沒有一條通往網際網路的路徑。判斷一個資源該放 public 還是 private，問題只有一個：它需不需要被網際網路直接定址。</p>
<h3 id="兩類-subnet-的定位">兩類 subnet 的定位</h3>
<p>public subnet 放的是必須接收外部入站流量的元件 — 對外的負載平衡器、NAT Gateway、堡壘主機（bastion）。這些資源透過 route table 連到 Internet Gateway，因此能被外部 IP 直接觸及。private subnet 放的是只該在內網被存取的元件 — 應用伺服器、資料庫、快取、內部佇列。它們沒有通往 Internet Gateway 的路由，外部無法主動連入，需要對外時才透過 NAT 出去。</p>
<table>
  <thead>
      <tr>
          <th>Subnet 類型</th>
          <th>典型住戶</th>
          <th>對外路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>public</td>
          <td>對外 LB、NAT Gateway、bastion</td>
          <td>經 Internet Gateway 雙向</td>
      </tr>
      <tr>
          <td>private</td>
          <td>應用節點、資料庫、快取、佇列</td>
          <td>僅經 NAT 單向出站、不可入站</td>
      </tr>
  </tbody>
</table>
<p>public subnet 的真實樣貌是「薄薄一層」：它通常只住負載平衡器與 NAT 這類入口設施，而不是業務邏輯。常見陷阱是為了 SSH 方便把應用伺服器直接開在 public subnet 並配公網 IP，等於把每一台業務主機的管理埠暴露在掃描流量下 — 全球的 bot 會在秒級頻率對公網 IP 的 22 埠嘗試登入。private subnet 的住戶反而是系統的主體 — 資料庫放這裡是因為它一旦能被外網定址，攻擊面就從「打穿入口層」變成「直接連資料庫埠試密碼」。</p>
<h3 id="跨可用區冗餘">跨可用區冗餘</h3>
<p>每個 subnet 綁定單一可用區（Availability Zone）。高可用設計通常是每種角色跨至少兩個可用區各開一個 subnet：兩個 public、兩個 private，讓單一可用區故障時另一區的同類 subnet 還能承接。subnet 的 CIDR 切法要留足空間 — 如果 VPC 是 <code>/16</code>，每個 subnet 用 <code>/20</code>（約四千個位址）可以在三個可用區各開 public + private 共六個 subnet，還有大量空間留給未來擴展。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">locals</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  azs</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;ap-northeast-1a&#34;, &#34;ap-northeast-1c&#34;, &#34;ap-northeast-1d&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_subnet&#34; &#34;public&#34;</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  for_each</span>          <span class="o">=</span> <span class="k">toset</span><span class="p">(</span><span class="k">local</span><span class="p">.</span><span class="k">azs</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">  vpc_id</span>            <span class="o">=</span> <span class="k">aws_vpc</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  cidr_block</span>        <span class="o">=</span> <span class="k">cidrsubnet</span><span class="p">(</span><span class="k">aws_vpc</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">cidr_block</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="k">index</span><span class="p">(</span><span class="k">local</span><span class="p">.</span><span class="k">azs</span><span class="p">,</span> <span class="k">each</span><span class="p">.</span><span class="k">key</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  availability_zone</span> <span class="o">=</span> <span class="k">each</span><span class="p">.</span><span class="k">key</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;public-${each.key}&#34;</span> }
</span></span><span class="line"><span class="ln">12</span><span class="cl">}
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_subnet&#34; &#34;private&#34;</span> {
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">  for_each</span>          <span class="o">=</span> <span class="k">toset</span><span class="p">(</span><span class="k">local</span><span class="p">.</span><span class="k">azs</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">  vpc_id</span>            <span class="o">=</span> <span class="k">aws_vpc</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">  cidr_block</span>        <span class="o">=</span> <span class="k">cidrsubnet</span><span class="p">(</span><span class="k">aws_vpc</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">cidr_block</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="k">index</span><span class="p">(</span><span class="k">local</span><span class="p">.</span><span class="k">azs</span><span class="p">,</span> <span class="k">each</span><span class="p">.</span><span class="k">key</span><span class="p">)</span> <span class="err">+</span> <span class="k">length</span><span class="p">(</span><span class="k">local</span><span class="p">.</span><span class="k">azs</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">  availability_zone</span> <span class="o">=</span> <span class="k">each</span><span class="p">.</span><span class="k">key</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;private-${each.key}&#34;</span> }
</span></span><span class="line"><span class="ln">21</span><span class="cl">}</span></span></code></pre></div><p><code>cidrsubnet</code> 函式自動切分子網段，避免手動計算 CIDR。第二個參數 <code>4</code> 表示在 <code>/16</code> 基礎上加 4 bit 得到 <code>/20</code>，第三個參數是序號。public 與 private 各佔不同序號區間，保證不重疊。</p>
<p>對外入口怎麼把流量分到跨可用區的 private 後端，是 devops 層負載平衡的範圍。這裡只要確保 subnet 的地圖在多 AZ 下對稱。</p>
<h2 id="route-table-與-nat流量的進出路徑">route table 與 NAT：流量的進出路徑</h2>
<p>離開一個 subnet 的封包往哪走，逐條寫在 route table 這組轉送規則裡 — 它掛在 subnet 上，是封包出口方向的依據。一個 subnet 是 public 還是 private，技術上的差別就在它關聯的 route table 裡有沒有一條指向 Internet Gateway 的預設路由。subnet 的對外性質由它關聯的 route table 賦予，而非寫在 subnet 自身的屬性。</p>
<h3 id="public-與-private-的路由差異">public 與 private 的路由差異</h3>
<p>public subnet 的 route table 有一條 <code>0.0.0.0/0 → Internet Gateway</code>，讓未知目的地的流量直接出網、也讓外部可達。private subnet 的 route table 則把 <code>0.0.0.0/0</code> 指向 NAT Gateway。</p>
<p>NAT（Network Address Translation）解決的問題是：private subnet 的資源需要主動對外（拉套件、呼叫第三方 API、抓 OS 更新），但不能因此變得可被外部入站連入。NAT 讓出站流量借用一個公網位址出去、把回應導回原請求者，同時不開放任何外部主動發起的連線。</p>
<h3 id="每-az-一個-nat-vs-共享-nat-的取捨">每 AZ 一個 NAT vs 共享 NAT 的取捨</h3>
<p>NAT Gateway 是綁定單一可用區的資源 — 一個 NAT Gateway 活在某一個 public subnet，也就活在那個可用區裡。這帶來一個架構取捨：</p>
<p><strong>共享 NAT（成本優先）</strong>：全部 private subnet 的 route table 都指向同一個 NAT。用一份 NAT 成本服務整個 VPC，代價是把 NAT 所在的可用區變成出站方向的單點 — 該可用區故障時，所有 private subnet 的對外連線同時中斷，即使其他可用區的節點本身健康。</p>
<p><strong>每 AZ 一個 NAT（可用性優先）</strong>：每個可用區各放一個 NAT Gateway，並讓每一區的 private subnet route table 指向同區的 NAT。出站路徑與 subnet 的跨可用區冗餘對齊，單一 AZ 故障只影響該區。每個 NAT Gateway 的固定月費約 $32 加流量費 $0.045/GB 處理量。三個可用區各一個就是三倍固定費。這筆成本與業務對出站中斷的容忍度對齊——如果單一可用區故障導致全部出站中斷可接受（例如有重試機制），共享 NAT 的成本效益較高。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_eip&#34; &#34;nat&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  for_each</span> <span class="o">=</span> <span class="k">toset</span><span class="p">(</span><span class="k">local</span><span class="p">.</span><span class="k">azs</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  domain</span>   <span class="o">=</span> <span class="s2">&#34;vpc&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  tags</span>     <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;nat-${each.key}&#34;</span> }
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_nat_gateway&#34; &#34;per_az&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  for_each</span>      <span class="o">=</span> <span class="k">aws_subnet</span><span class="p">.</span><span class="k">public</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  allocation_id</span> <span class="o">=</span> <span class="k">aws_eip</span><span class="p">.</span><span class="k">nat</span><span class="p">[</span><span class="k">each</span><span class="p">.</span><span class="k">key</span><span class="p">].</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  subnet_id</span>     <span class="o">=</span> <span class="k">each</span><span class="p">.</span><span class="k">value</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  tags</span>          <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;nat-${each.key}&#34;</span> }
</span></span><span class="line"><span class="ln">12</span><span class="cl">}
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_route_table&#34; &#34;private&#34;</span> {
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">  for_each</span> <span class="o">=</span> <span class="k">aws_subnet</span><span class="p">.</span><span class="k">private</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">  vpc_id</span>   <span class="o">=</span> <span class="k">aws_vpc</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="k">route</span> {
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">    cidr_block</span>     <span class="o">=</span> <span class="s2">&#34;0.0.0.0/0&#34;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">    nat_gateway_id</span> <span class="o">=</span> <span class="k">aws_nat_gateway</span><span class="p">.</span><span class="k">per_az</span><span class="p">[</span><span class="k">each</span><span class="p">.</span><span class="k">key</span><span class="p">].</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  }
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;private-rt-${each.key}&#34;</span> }
</span></span><span class="line"><span class="ln">24</span><span class="cl">}
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_route_table_association&#34; &#34;private&#34;</span> {
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="n">  for_each</span>       <span class="o">=</span> <span class="k">aws_subnet</span><span class="p">.</span><span class="k">private</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="n">  subnet_id</span>      <span class="o">=</span> <span class="k">each</span><span class="p">.</span><span class="k">value</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="n">  route_table_id</span> <span class="o">=</span> <span class="k">aws_route_table</span><span class="p">.</span><span class="k">private</span><span class="p">[</span><span class="k">each</span><span class="p">.</span><span class="k">key</span><span class="p">].</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">}</span></span></code></pre></div><p>判讀訊號：private subnet 的服務拉不到外部套件、或第三方 API 全部逾時，先查它關聯的 route table 有沒有指向健康的 NAT；若只有某一個可用區的節點受影響，多半是那一區的 NAT 或其所在 subnet 出狀況。</p>
<h3 id="nat-的成本邊界">NAT 的成本邊界</h3>
<p>NAT Gateway 按處理流量計費（每 GB 一個費率），把大量出站流量長期走 NAT 會讓帳單可觀。常見的高流量場景包括：備份上傳到 S3、跨區資料同步、大量 API 呼叫。對於走向 AWS 自家服務的流量，成本效益較好的做法是用 VPC Endpoint（Gateway 型或 Interface 型）讓流量直連服務、繞過 NAT。S3 與 DynamoDB 的 Gateway Endpoint 是免費的，光是把 S3 備份流量從 NAT 改走 Gateway Endpoint 就能在流量大的環境省下可觀的費用。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_vpc_endpoint&#34; &#34;s3&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  vpc_id</span>       <span class="o">=</span> <span class="k">aws_vpc</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  service_name</span> <span class="o">=</span> <span class="s2">&#34;com.amazonaws.ap-northeast-1.s3&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">  route_table_ids</span> <span class="o">=</span> <span class="p">[</span><span class="k">for</span> <span class="k">rt</span> <span class="k">in</span> <span class="k">aws_route_table</span><span class="p">.</span><span class="k">private</span> <span class="err">:</span> <span class="k">rt</span><span class="p">.</span><span class="k">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;s3-gateway-endpoint&#34;</span> }
</span></span><span class="line"><span class="ln">8</span><span class="cl">}</span></span></code></pre></div><p>NAT 的數量取捨與出站成本的更完整討論在 <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a>。route table 與 NAT 只管「能不能出去、走哪條路」，至於某個埠允不允許連，是 security group 的職責。</p>
<h2 id="security-group-設計最小開放">security group 設計：最小開放</h2>
<p>一條連線究竟能不能打到某個埠，由 security group 逐埠拍板 — 它是掛在資源網卡（ENI）層級的有狀態防火牆，規則描述的是哪些來源連得進這個資源。它是貼著服務的最後一道網路邊界 — 即使封包順著 route table 抵達了 private subnet，security group 仍能逐埠決定放不放行。「有狀態」的意思是：放行一條入站連線後，對應的回應出站自動允許，規則只需描述入站方向想開放什麼。</p>
<h3 id="用-group-引用取代-ip-範圍">用 group 引用取代 IP 範圍</h3>
<p>設計原則是最小開放：每條規則只開「這個服務確實需要被誰連的那個埠」。資料庫的 security group 入站只允許來自應用層 security group 的資料庫埠，而不是某個 IP 範圍。用 security group 互相引用、而非寫死網段，是因為應用節點會隨擴縮而換 IP — 引用來源 group 讓規則跟著成員身分走、不跟著位址走。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_security_group&#34; &#34;app&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name_prefix</span> <span class="o">=</span> <span class="s2">&#34;app-&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  vpc_id</span>      <span class="o">=</span> <span class="k">aws_vpc</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  tags</span>        <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;app-sg&#34;</span> }
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_security_group&#34; &#34;database&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  name_prefix</span> <span class="o">=</span> <span class="s2">&#34;db-&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  vpc_id</span>      <span class="o">=</span> <span class="k">aws_vpc</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  tags</span>        <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;db-sg&#34;</span> }
</span></span><span class="line"><span class="ln">11</span><span class="cl">}
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_security_group_rule&#34; &#34;db_from_app&#34;</span> {
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">  type</span>                     <span class="o">=</span> <span class="s2">&#34;ingress&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">  from_port</span>                <span class="o">=</span> <span class="m">5432</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">  to_port</span>                  <span class="o">=</span> <span class="m">5432</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">  protocol</span>                 <span class="o">=</span> <span class="s2">&#34;tcp&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">  security_group_id</span>        <span class="o">=</span> <span class="k">aws_security_group</span><span class="p">.</span><span class="k">database</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">  source_security_group_id</span> <span class="o">=</span> <span class="k">aws_security_group</span><span class="p">.</span><span class="k">app</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">}</span></span></code></pre></div><p>這條規則表達的語意是「資料庫只接受來自 app group 成員的 5432 連線」。app 的 instance 數量從 2 台增長到 20 台時，規則本身不需要改 — 新 instance 只要也掛了 <code>app</code> 的 security group 就自動被允許。</p>
<h3 id="00000-的盤點紀律">0.0.0.0/0 的盤點紀律</h3>
<p>把入站來源設成 <code>0.0.0.0/0</code> 等於允許整個網際網路連這個埠。對資料庫埠（5432、3306、6379）或管理埠（22、3389）這麼做，會讓服務暴露在持續性的自動掃描與暴力嘗試下。</p>
<p>合理出現 <code>0.0.0.0/0</code> 的位置只有對外負載平衡器的 80 / 443 入站 — 因為它的工作本來就是接收公開流量。其餘所有 <code>0.0.0.0/0</code> 都該被質疑。</p>
<p>盤點的做法：列出所有 security group，過濾 source 是 <code>0.0.0.0/0</code> 的 ingress rule，逐條問「這個埠確實需要全世界都連得到嗎」。在 CLI 上可以用一條查詢掃：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws ec2 describe-security-groups <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;SecurityGroups[].{
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s1">    ID:GroupId,
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">    Name:GroupName,
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">    OpenPorts:IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]].[FromPort,ToPort]
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s1">  }&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --output table</span></span></code></pre></div><p>資料庫埠、SSH、內部 API 出現在這份清單上就是該收斂的目標。管理埠的存取更安全的替代方案是 SSM Session Manager — 它讓你透過 IAM 權限建立 shell session，完全不需要開 22 埠，連線經由 Systems Manager 的控制通道走、不走公網，同時自動留下 session log。誰能透過 IAM 改動這些規則，銜接<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>。</p>
<p>在 CI 層面，<a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>用 tfsec / checkov 做靜態掃描，自動攔截「敏感埠 + 全開 CIDR」的組合，把 security group 的盤點從人工定期做變成每次 PR 自動做。</p>
<p>邊界設備漏洞帶來的教訓同樣適用於 security group 設計。<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/check-point-cve-2024-24919-vpn-info-disclosure/" data-link-title="7.R7.3.12 Check Point 2024：VPN 資訊外洩與會話風險" data-link-desc="邊界設備資訊外洩漏洞可快速轉為憑證與會話濫用風險">Check Point CVE-2024-24919</a> 事件顯示 VPN 邊界設備的資訊外洩漏洞可以直接轉為憑證與會話濫用，攻擊路徑是「邊界入口 → 會話竊取 → 內部橫向擴散」。<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023</a> 則是邊界設備的會話資料外洩導致重放攻擊。這兩個案例的 infra 層教訓是：邊界設備（VPN concentrator、ADC、bastion）的 security group 只開必要的管理埠，且事件後需要全域 session/token 失效流程。</p>
<p>網路控制面的自動化也有風險。<a href="/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/" data-link-title="7.C1 Cloudflare：2026 Route Leak 事件" data-link-desc="BGP 路由政策自動化失誤如何回寫控制面治理。">Cloudflare 2026 Route Leak</a> 事件中，自動化路由政策配置的錯誤導致流量擁塞。infra 層的教訓是：路由與 security group 規則的自動化變更需要 pre-check 與影響範圍評估，且要有快速撤回機制——這正是 <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">infra 走 PR 流程</a>的 plan → review → apply 流程要擋的。</p>
<h2 id="nacl-與-security-group-的分工">NACL 與 security group 的分工</h2>
<p>subnet 這一層還有另一道防火牆 — network ACL（NACL），它與 security group 分工在兩個層級。</p>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>Security Group</th>
          <th>NACL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>掛在哪裡</td>
          <td>資源網卡（ENI）</td>
          <td>Subnet</td>
      </tr>
      <tr>
          <td>狀態</td>
          <td>有狀態（回程自動放行）</td>
          <td>無狀態（回程要另寫規則）</td>
      </tr>
      <tr>
          <td>規則方向</td>
          <td>只寫入站</td>
          <td>入站與出站各寫</td>
      </tr>
      <tr>
          <td>能否 deny</td>
          <td>只能列允許清單</td>
          <td>支援顯式 deny</td>
      </tr>
      <tr>
          <td>評估順序</td>
          <td>所有規則一起評估</td>
          <td>按規則編號順序，命中即停</td>
      </tr>
  </tbody>
</table>
<p>NACL 的特點是無狀態與支援顯式 deny。無狀態意味著放行了入站不代表回應的出站自動放行，回程封包得自己對得上另一條出站規則 — 這讓 NACL 的維護比 security group 複雜。支援顯式 deny 則是它獨有的能力：security group 只能說「誰可以進」，NACL 能說「誰一定不能進」，這在需要 subnet 邊界封鎖特定已知惡意網段時有用。</p>
<p>多數設計的主力是 security group：它貼著服務、用 group 互相引用就能表達「誰能連誰」，已經涵蓋大部分最小開放需求。NACL 留給少數情境 — 需要在 subnet 邊界擋掉一整段已知惡意網段、或要對某類流量做顯式 deny 時才展開。多數環境讓 NACL 維持預設全通、把存取控制集中在 security group，是可以接受的選擇。重點是知道這一層存在、在需要 subnet 層粗篩時記得它。</p>
<h2 id="為什麼網路要先於核心服務鋪好">為什麼網路要先於核心服務鋪好</h2>
<p>網路地基先行，是因為核心服務的安全位置由網路拓樸決定，而不是反過來。資料庫該落在哪個 private subnet、它的 security group 只接受哪個來源、它的出站走不走 NAT — 這些都是服務「出生時」就該確定的屬性。</p>
<p>先有規劃好的 subnet 與 security group，新服務上線只是挑一塊已定義安全等級的位置放進去。網路還沒鋪就先開服務，則往往落在預設 VPC 與寬鬆規則上。預設 VPC 是所有人共享的、CIDR 不可控的、security group 預設全通的 — 把正式服務放在這裡，等於跳過了所有隔離設計。事後再回頭收斂，要在服務已經有流量、有依賴的情況下改網段與防火牆，風險和協調成本都高得多。</p>
<p>這也呼應<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零：infra 是什麼</a>的 day-1 鐵律：邊界與隔離屬於一開始就該存在的地基，不是長出問題後才補的修補。網路規劃好之後，照「從零建置」路線下一步先進<a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>確定環境怎麼切，再讓核心服務落進這些 subnet（見<a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC</a>）。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：誰有權改動 security group 與路由表</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>：環境之間的 VPC 怎麼分</li>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC</a>：核心服務怎麼落進規劃好的 subnet</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：tfsec / checkov 自動攔截 security group 全開</li>
<li>→ <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a>：NAT 與出站流量的成本取捨</li>
<li>→ <a href="/blog/infra/03-network-foundation/security-group-audit-cleanup/" data-link-title="Security Group 稽核與清理" data-link-desc="盤點所有 security group 規則、找出 0.0.0.0/0 全開與未使用的 SG、依賴檢查後安全刪除、自動化治理">Security Group 稽核與清理</a>：SG 規則盤點、未使用 SG 識別、清理工作流</li>
</ul>
]]></content:encoded></item><item><title>Port 與 Localhost</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/port-and-localhost/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/port-and-localhost/</guid><description>&lt;p>Port 與 Localhost 的核心概念是「網路 server 暴露在哪個地址、聽哪個 port、讓誰能連進來」。本地 LLM 場景中、Ollama 預設聽 &lt;code>127.0.0.1:11434&lt;/code>、Continue.dev 等介面層透過這個地址呼叫 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/openai-compatible-api/" data-link-title="OpenAI 相容 API" data-link-desc="本地推論伺服器跟雲端 OpenAI 共用的 API 形狀標準">OpenAI 相容 API&lt;/a>；理解 listen address 跟 port 的角色、才能判讀「為什麼 port 撞 / 為什麼 LAN 上另一台連不到 / 暴露到 internet 安全嗎」。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>完整的 server 入口由兩個欄位定義：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>欄位&lt;/th>
 &lt;th>角色&lt;/th>
 &lt;th>範例值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Listen address&lt;/td>
 &lt;td>接受哪些網路介面送進來的封包&lt;/td>
 &lt;td>&lt;code>127.0.0.1&lt;/code> / &lt;code>0.0.0.0&lt;/code> / &lt;code>192.168.x&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Port&lt;/td>
 &lt;td>OS 用來區分「同一台機器上哪個 server」&lt;/td>
 &lt;td>&lt;code>11434&lt;/code> / &lt;code>1234&lt;/code> / &lt;code>8080&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Port 是 16 bit 數字（0 ~ 65535）、其中 0 ~ 1023 是 well-known port（HTTP 80、HTTPS 443 等、需 root 權限才能 bind）、1024 ~ 65535 是 user port、本地 LLM 工具都用這個區間（Ollama 11434、LM Studio 1234、llama.cpp 8080）。同一個 port 在同一個 listen address 上同時只能被一個 process 持有、要兩個 Ollama 並存就要其中一個換 port。&lt;/p>
&lt;p>三個常見 listen address 的語意：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>地址&lt;/th>
 &lt;th>等同名稱&lt;/th>
 &lt;th>接受誰的連線&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>127.0.0.1&lt;/code>&lt;/td>
 &lt;td>&lt;code>localhost&lt;/code>&lt;/td>
 &lt;td>只接受本機 process、外部裝置連不到&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>0.0.0.0&lt;/code>&lt;/td>
 &lt;td>所有介面&lt;/td>
 &lt;td>接受任何網路介面送進來的封包、包含 LAN / VPN / public&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>192.168.x&lt;/code>&lt;/td>
 &lt;td>特定 LAN 介面&lt;/td>
 &lt;td>只接受該 LAN 介面送進來的封包&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>驗證 server 真的在聽預期地址：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># macOS 下查誰佔了 11434&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">lsof -i :11434
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># ollama 1234 mac 6u IPv4 0xabcd 0t0 TCP localhost:11434 (LISTEN)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>TCP localhost:11434 (LISTEN)&lt;/code> 表示這個 process 只接 localhost 進來的封包。改 listen address 把 Ollama 暴露到 LAN：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nv">OLLAMA_HOST&lt;/span>&lt;span class="o">=&lt;/span>0.0.0.0:11434 ollama serve
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># lsof 之後會看到 TCP *:11434 (LISTEN)、星號表示所有介面&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>curl 用不同 host 名稱呼叫同一個 server：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">curl http://localhost:11434/api/version &lt;span class="c1"># 走 loopback、最快&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">curl http://127.0.0.1:11434/api/version &lt;span class="c1"># 跟上面等價&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">curl http://&amp;lt;本機 LAN IP&amp;gt;:11434/api/version &lt;span class="c1"># 若 listen 在 0.0.0.0、會通；只 listen localhost 會 connection refused&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「為什麼桌機跑 Ollama、筆電連不到」的最常見原因就是 Ollama 沒改 listen address、預設只接受 loopback。&lt;/p></description><content:encoded><![CDATA[<p>Port 與 Localhost 的核心概念是「網路 server 暴露在哪個地址、聽哪個 port、讓誰能連進來」。本地 LLM 場景中、Ollama 預設聽 <code>127.0.0.1:11434</code>、Continue.dev 等介面層透過這個地址呼叫 <a href="/blog/llm/knowledge-cards/openai-compatible-api/" data-link-title="OpenAI 相容 API" data-link-desc="本地推論伺服器跟雲端 OpenAI 共用的 API 形狀標準">OpenAI 相容 API</a>；理解 listen address 跟 port 的角色、才能判讀「為什麼 port 撞 / 為什麼 LAN 上另一台連不到 / 暴露到 internet 安全嗎」。</p>
<h2 id="概念位置">概念位置</h2>
<p>完整的 server 入口由兩個欄位定義：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>角色</th>
          <th>範例值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Listen address</td>
          <td>接受哪些網路介面送進來的封包</td>
          <td><code>127.0.0.1</code> / <code>0.0.0.0</code> / <code>192.168.x</code></td>
      </tr>
      <tr>
          <td>Port</td>
          <td>OS 用來區分「同一台機器上哪個 server」</td>
          <td><code>11434</code> / <code>1234</code> / <code>8080</code></td>
      </tr>
  </tbody>
</table>
<p>Port 是 16 bit 數字（0 ~ 65535）、其中 0 ~ 1023 是 well-known port（HTTP 80、HTTPS 443 等、需 root 權限才能 bind）、1024 ~ 65535 是 user port、本地 LLM 工具都用這個區間（Ollama 11434、LM Studio 1234、llama.cpp 8080）。同一個 port 在同一個 listen address 上同時只能被一個 process 持有、要兩個 Ollama 並存就要其中一個換 port。</p>
<p>三個常見 listen address 的語意：</p>
<table>
  <thead>
      <tr>
          <th>地址</th>
          <th>等同名稱</th>
          <th>接受誰的連線</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>127.0.0.1</code></td>
          <td><code>localhost</code></td>
          <td>只接受本機 process、外部裝置連不到</td>
      </tr>
      <tr>
          <td><code>0.0.0.0</code></td>
          <td>所有介面</td>
          <td>接受任何網路介面送進來的封包、包含 LAN / VPN / public</td>
      </tr>
      <tr>
          <td><code>192.168.x</code></td>
          <td>特定 LAN 介面</td>
          <td>只接受該 LAN 介面送進來的封包</td>
      </tr>
  </tbody>
</table>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>驗證 server 真的在聽預期地址：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># macOS 下查誰佔了 11434</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">lsof -i :11434
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># ollama  1234 mac  6u IPv4 0xabcd      0t0 TCP localhost:11434 (LISTEN)</span></span></span></code></pre></div><p><code>TCP localhost:11434 (LISTEN)</code> 表示這個 process 只接 localhost 進來的封包。改 listen address 把 Ollama 暴露到 LAN：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">OLLAMA_HOST</span><span class="o">=</span>0.0.0.0:11434 ollama serve
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># lsof 之後會看到 TCP *:11434 (LISTEN)、星號表示所有介面</span></span></span></code></pre></div><p>curl 用不同 host 名稱呼叫同一個 server：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl http://localhost:11434/api/version    <span class="c1"># 走 loopback、最快</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl http://127.0.0.1:11434/api/version    <span class="c1"># 跟上面等價</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">curl http://&lt;本機 LAN IP&gt;:11434/api/version <span class="c1"># 若 listen 在 0.0.0.0、會通；只 listen localhost 會 connection refused</span></span></span></code></pre></div><p>「為什麼桌機跑 Ollama、筆電連不到」的最常見原因就是 Ollama 沒改 listen address、預設只接受 loopback。</p>
<h2 id="設計責任">設計責任</h2>
<p>選 listen address 是信任邊界決定：</p>
<ul>
<li><strong><code>127.0.0.1</code>（預設）</strong>：機器本身就是信任邊界、外部進不來、最安全</li>
<li><strong><code>0.0.0.0</code> 在家用 / 信任 LAN</strong>：把 server 暴露給同網路裝置、便於多裝置共用、風險可接受</li>
<li><strong><code>0.0.0.0</code> 在公共 Wi-Fi / 對 internet</strong>：等於對所有路過裝置開放、Ollama 沒有內建 auth、需要 SSH tunnel 或 reverse proxy + auth 才安全</li>
</ul>
<p>Port 衝突的處理順序：用 <code>lsof</code> 確認佔用方身分 → 若是舊版自己 kill、若是別的服務改自己的 port → 同步更新 IDE plugin 的 <code>apiBase</code>。完整資料流判讀見 <a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流</a>。</p>
]]></content:encoded></item><item><title>Security Group 稽核與清理</title><link>https://tarrragon.github.io/blog/infra/03-network-foundation/security-group-audit-cleanup/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/03-network-foundation/security-group-audit-cleanup/</guid><description>&lt;p>Security group 的規則會隨時間累積：某次救火加了一條 0.0.0.0/0、某個已下線的服務留下沒人認領的 SG、某條規則的用途只存在建立者的記憶裡。稽核的目標是把這些累積的規則攤開來，逐條回答「這條規則還有在用嗎、來源該這麼寬嗎」，然後安全地清理不需要的部分。&lt;/p>
&lt;h2 id="匯出所有-security-group-與規則">匯出所有 security group 與規則&lt;/h2>
&lt;p>稽核的第一步是把當前所有 SG 和它們的規則拉出來存成可查詢的 JSON。這份 JSON 是後續所有分析的輸入，也是「稽核那天環境長什麼樣」的快照。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">aws ec2 describe-security-groups &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;SecurityGroups[].{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s1"> GroupId:GroupId,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s1"> GroupName:GroupName,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s1"> VpcId:VpcId,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s1"> Description:Description,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s1"> IngressRules:IpPermissions,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s1"> EgressRules:IpPermissionsEgress,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s1"> Tags:Tags
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --output json &amp;gt; sg-inventory-&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這份檔案通常幾百 KB 到幾 MB，存進 repo 的 &lt;code>inventory/&lt;/code> 目錄，方便日後比對變化。如果帳號有多個 region，每個 region 各跑一次並標明 region。&lt;/p>
&lt;p>用 jq 快速看有多少 SG 和總規則數：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">jq &lt;span class="s1">&amp;#39;length&amp;#39;&lt;/span> sg-inventory-*.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">jq &lt;span class="s1">&amp;#39;[.[].IngressRules | length] | add&amp;#39;&lt;/span> sg-inventory-*.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="找出-00000-全開的入站規則">找出 0.0.0.0/0 全開的入站規則&lt;/h2>
&lt;p>0.0.0.0/0 入站代表允許整個網際網路連到這個埠。對外 ALB 的 80/443 開 0.0.0.0/0 是設計意圖，但資料庫埠（5432、3306、6379）、SSH（22）或管理埠開 0.0.0.0/0 是需要收斂的目標。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">jq -r &lt;span class="s1">&amp;#39;.[] | select(.IngressRules[]?.IpRanges[]?.CidrIp == &amp;#34;0.0.0.0/0&amp;#34;) |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s1"> {GroupId, GroupName, OpenPorts: [.IngressRules[] |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s1"> select(.IpRanges[]?.CidrIp == &amp;#34;0.0.0.0/0&amp;#34;) |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s1"> &amp;#34;\(.FromPort // &amp;#34;all&amp;#34;)-\(.ToPort // &amp;#34;all&amp;#34;)/\(.IpProtocol)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s1"> ]}&amp;#39;&lt;/span> sg-inventory-*.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>輸出會列出每個有全開規則的 SG 和對應的 port 範圍。對每一條命中，判斷：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>全開是否合規&lt;/th>
 &lt;th>處理方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>ALB 的 80/443&lt;/td>
 &lt;td>合規 — 負載平衡器的職責就是接收公開流量&lt;/td>
 &lt;td>保留，標記為已審查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SSH (22) 或 RDP (3389)&lt;/td>
 &lt;td>需收斂 — 管理埠暴露在持續的暴力掃描下&lt;/td>
 &lt;td>改用 SSM Session Manager 或限縮到辦公室 IP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料庫埠 (5432/3306/6379)&lt;/td>
 &lt;td>需收斂 — 資料庫不應從公網可達&lt;/td>
 &lt;td>改為只允許應用層 SG 來源&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>全埠 (0-65535 / -1)&lt;/td>
 &lt;td>需收斂 — 等於沒有防火牆&lt;/td>
 &lt;td>拆成具體需要的埠和來源&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>IPv6 的 &lt;code>::/0&lt;/code> 也要一併查：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">jq -r &lt;span class="s1">&amp;#39;.[] | select(.IngressRules[]?.Ipv6Ranges[]?.CidrIpv6 == &amp;#34;::/0&amp;#34;) |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s1"> .GroupId&amp;#39;&lt;/span> sg-inventory-*.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="找出未使用的-security-group">找出未使用的 security group&lt;/h2>
&lt;p>未使用的 SG 是沒有任何網路介面（ENI）掛載的 SG。它不影響任何正在運行的資源，但佔用 SG 配額（每個 VPC 預設上限 2500 個），而且它的規則會讓稽核清單更長、更難判讀。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">aws ec2 describe-network-interfaces &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;NetworkInterfaces[].Groups[].GroupId&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --output text &lt;span class="p">|&lt;/span> tr &lt;span class="s1">&amp;#39;\t&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> sort -u &amp;gt; sg-in-use.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">jq -r &lt;span class="s1">&amp;#39;.[].GroupId&amp;#39;&lt;/span> sg-inventory-*.json &lt;span class="p">|&lt;/span> sort -u &amp;gt; sg-all.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">comm -23 sg-all.txt sg-in-use.txt &amp;gt; sg-unused.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">cat sg-unused.txt&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>sg-unused.txt&lt;/code> 裡列出的就是當前沒有任何 ENI 引用的 SG。注意幾個例外：&lt;/p></description><content:encoded><![CDATA[<p>Security group 的規則會隨時間累積：某次救火加了一條 0.0.0.0/0、某個已下線的服務留下沒人認領的 SG、某條規則的用途只存在建立者的記憶裡。稽核的目標是把這些累積的規則攤開來，逐條回答「這條規則還有在用嗎、來源該這麼寬嗎」，然後安全地清理不需要的部分。</p>
<h2 id="匯出所有-security-group-與規則">匯出所有 security group 與規則</h2>
<p>稽核的第一步是把當前所有 SG 和它們的規則拉出來存成可查詢的 JSON。這份 JSON 是後續所有分析的輸入，也是「稽核那天環境長什麼樣」的快照。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">aws ec2 describe-security-groups <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;SecurityGroups[].{
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s1">    GroupId:GroupId,
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s1">    GroupName:GroupName,
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s1">    VpcId:VpcId,
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s1">    Description:Description,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s1">    IngressRules:IpPermissions,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s1">    EgressRules:IpPermissionsEgress,
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s1">    Tags:Tags
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s1">  }&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --output json &gt; sg-inventory-<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.json</span></span></code></pre></div><p>這份檔案通常幾百 KB 到幾 MB，存進 repo 的 <code>inventory/</code> 目錄，方便日後比對變化。如果帳號有多個 region，每個 region 各跑一次並標明 region。</p>
<p>用 jq 快速看有多少 SG 和總規則數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">jq <span class="s1">&#39;length&#39;</span> sg-inventory-*.json
</span></span><span class="line"><span class="ln">2</span><span class="cl">jq <span class="s1">&#39;[.[].IngressRules | length] | add&#39;</span> sg-inventory-*.json</span></span></code></pre></div><h2 id="找出-00000-全開的入站規則">找出 0.0.0.0/0 全開的入站規則</h2>
<p>0.0.0.0/0 入站代表允許整個網際網路連到這個埠。對外 ALB 的 80/443 開 0.0.0.0/0 是設計意圖，但資料庫埠（5432、3306、6379）、SSH（22）或管理埠開 0.0.0.0/0 是需要收斂的目標。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">jq -r <span class="s1">&#39;.[] | select(.IngressRules[]?.IpRanges[]?.CidrIp == &#34;0.0.0.0/0&#34;) |
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s1">  {GroupId, GroupName, OpenPorts: [.IngressRules[] |
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s1">    select(.IpRanges[]?.CidrIp == &#34;0.0.0.0/0&#34;) |
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">    &#34;\(.FromPort // &#34;all&#34;)-\(.ToPort // &#34;all&#34;)/\(.IpProtocol)&#34;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">  ]}&#39;</span> sg-inventory-*.json</span></span></code></pre></div><p>輸出會列出每個有全開規則的 SG 和對應的 port 範圍。對每一條命中，判斷：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>全開是否合規</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ALB 的 80/443</td>
          <td>合規 — 負載平衡器的職責就是接收公開流量</td>
          <td>保留，標記為已審查</td>
      </tr>
      <tr>
          <td>SSH (22) 或 RDP (3389)</td>
          <td>需收斂 — 管理埠暴露在持續的暴力掃描下</td>
          <td>改用 SSM Session Manager 或限縮到辦公室 IP</td>
      </tr>
      <tr>
          <td>資料庫埠 (5432/3306/6379)</td>
          <td>需收斂 — 資料庫不應從公網可達</td>
          <td>改為只允許應用層 SG 來源</td>
      </tr>
      <tr>
          <td>全埠 (0-65535 / -1)</td>
          <td>需收斂 — 等於沒有防火牆</td>
          <td>拆成具體需要的埠和來源</td>
      </tr>
  </tbody>
</table>
<p>IPv6 的 <code>::/0</code> 也要一併查：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">jq -r <span class="s1">&#39;.[] | select(.IngressRules[]?.Ipv6Ranges[]?.CidrIpv6 == &#34;::/0&#34;) |
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s1">  .GroupId&#39;</span> sg-inventory-*.json</span></span></code></pre></div><h2 id="找出未使用的-security-group">找出未使用的 security group</h2>
<p>未使用的 SG 是沒有任何網路介面（ENI）掛載的 SG。它不影響任何正在運行的資源，但佔用 SG 配額（每個 VPC 預設上限 2500 個），而且它的規則會讓稽核清單更長、更難判讀。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws ec2 describe-network-interfaces <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;NetworkInterfaces[].Groups[].GroupId&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --output text <span class="p">|</span> tr <span class="s1">&#39;\t&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> sort -u &gt; sg-in-use.txt
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">jq -r <span class="s1">&#39;.[].GroupId&#39;</span> sg-inventory-*.json <span class="p">|</span> sort -u &gt; sg-all.txt
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl">comm -23 sg-all.txt sg-in-use.txt &gt; sg-unused.txt
</span></span><span class="line"><span class="ln">8</span><span class="cl">cat sg-unused.txt</span></span></code></pre></div><p><code>sg-unused.txt</code> 裡列出的就是當前沒有任何 ENI 引用的 SG。注意幾個例外：</p>
<ul>
<li><strong>default SG</strong>：每個 VPC 都有一個 default SG，即使未使用也無法刪除，可以跳過</li>
<li><strong>被其他 SG 引用</strong>：某個 SG 雖然沒有掛在任何 ENI 上，但被另一個 SG 的入站規則引用為 source — 刪除它會讓引用方的規則失效</li>
<li><strong>被 launch template 或 auto-scaling group 引用</strong>：新啟動的實例會套用這個 SG，刪了之後新實例啟動會失敗</li>
</ul>
<h2 id="依賴檢查刪除前確認沒有間接引用">依賴檢查：刪除前確認沒有間接引用</h2>
<p>直接刪一個 SG 之前，確認沒有其他資源引用它。AWS 在 SG 被引用時會擋住刪除（報 DependencyViolation），但提前知道引用方可以避免白跑一趟。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nv">SG_ID</span><span class="o">=</span><span class="s2">&#34;sg-0abc123&#34;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 哪些 SG 的入站規則引用了這個 SG 作為來源</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">jq -r --arg sg <span class="s2">&#34;</span><span class="nv">$SG_ID</span><span class="s2">&#34;</span> <span class="s1">&#39;.[] |
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s1">  select(.IngressRules[]?.UserIdGroupPairs[]?.GroupId == $sg) |
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s1">  &#34;\(.GroupId) (\(.GroupName)) 的入站規則引用了 \($sg)&#34;&#39;</span> sg-inventory-*.json
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 哪些 ENI 掛了這個 SG</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">aws ec2 describe-network-interfaces <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --filters <span class="nv">Name</span><span class="o">=</span>group-id,Values<span class="o">=</span><span class="nv">$SG_ID</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;NetworkInterfaces[].{Id:NetworkInterfaceId,Desc:Description,Status:Status}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --output table
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 哪些 RDS instance 使用這個 SG</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">aws rds describe-db-instances <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;DBInstances[?VpcSecurityGroups[?VpcSecurityGroupId==&#39;</span><span class="nv">$SG_ID</span><span class="s2">&#39;]].[DBInstanceIdentifier]&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  --output text
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 哪些 ELB 使用這個 SG</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">aws elbv2 describe-load-balancers <span class="se">\
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;LoadBalancers[?SecurityGroups[?contains(@,&#39;</span><span class="nv">$SG_ID</span><span class="s2">&#39;)]].[LoadBalancerName]&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="se"></span>  --output text</span></span></code></pre></div><p>如果所有查詢都回傳空，這個 SG 可以安全刪除。</p>
<h2 id="清理流程標記--通知--等待--刪除">清理流程：標記 → 通知 → 等待 → 刪除</h2>
<p>批量清理不是一次 <code>delete-security-group</code> 的事。安全的流程有四步：</p>
<h3 id="標記候選">標記候選</h3>
<p>對每個要清理的 SG 加一個 tag 標明狀態和預定刪除日期：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws ec2 create-tags <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --resources sg-0abc123 sg-0def456 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --tags <span class="nv">Key</span><span class="o">=</span>cleanup-status,Value<span class="o">=</span>pending-deletion <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>         <span class="nv">Key</span><span class="o">=</span>cleanup-date,Value<span class="o">=</span>2026-07-10 <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>         <span class="nv">Key</span><span class="o">=</span>cleanup-reason,Value<span class="o">=</span><span class="s2">&#34;unused-no-eni-no-reference&#34;</span></span></span></code></pre></div><h3 id="通知">通知</h3>
<p>如果 SG 有 <code>owner</code> tag，通知該 owner：「這個 SG 預計在 cleanup-date 刪除，如果仍在使用請回報」。如果沒有 owner tag（多數需要清理的 SG 都沒有），在團隊頻道公告清理清單。</p>
<h3 id="等待">等待</h3>
<p>給 7-14 天的寬限期。期間如果有人回報某個 SG 仍在使用，把 cleanup-status 改成 <code>retained</code> 並補上正確的 owner tag。</p>
<h3 id="刪除">刪除</h3>
<p>寬限期過後，對仍是 <code>pending-deletion</code> 的 SG 執行刪除：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> sg in <span class="k">$(</span>aws ec2 describe-security-groups <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --filters <span class="nv">Name</span><span class="o">=</span>tag:cleanup-status,Values<span class="o">=</span>pending-deletion <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;SecurityGroups[].GroupId&#39;</span> --output text<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Deleting </span><span class="nv">$sg</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  aws ec2 delete-security-group --group-id <span class="nv">$sg</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>DependencyViolation 代表有遺漏的引用，跳過該 SG 並重新調查。</p>
<h2 id="自動化持續治理">自動化持續治理</h2>
<p>手動稽核適合第一次清理，持續治理靠自動化：</p>
<h3 id="aws-config-規則">AWS Config 規則</h3>
<p><code>restricted-ssh</code> 和 <code>restricted-common-ports</code> 是 AWS Config 的 managed rule，啟用後會持續監控 SG 規則，新增的 0.0.0.0/0 規則會在幾分鐘內被標記為 non-compliant。</p>
<h3 id="prowler-定期掃描">Prowler 定期掃描</h3>
<p>在 CI 排程中定期跑 Prowler，掃描結果存進 repo 作為趨勢追蹤：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">prowler aws --services ec2 --checks ec2_securitygroup_allow_ingress_from_internet_to_any_port <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -M json-ocsf -o inventory/prowler/</span></span></code></pre></div><h3 id="pr-流程攔截">PR 流程攔截</h3>
<p><a href="/blog/infra/07-infra-as-pr/plan-review-apply-guardrails/" data-link-title="infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七的 checkov/tfsec 護欄</a>在 PR 階段攔截新增的 0.0.0.0/0 規則。這是把治理從「事後稽核」推到「事前攔截」的關鍵一步：稽核能發現已存在的問題，PR 護欄能阻止新問題被引入。</p>
<p>AWS Security Hub 啟用 Foundational Security Best Practices 標準後，會自動聚合 SG 相關的合規 finding 並提供統一 dashboard，適合作為管理層報告的來源。Security Hub 整合了 Config rules 和 Prowler 各自能發現的問題，提供單一窗口追蹤合規趨勢。</p>
<h2 id="稽核節奏">稽核節奏</h2>
<p>第一次稽核最花時間（半天到一天，取決於 SG 數量）。之後的節奏取決於環境變動速度：</p>
<table>
  <thead>
      <tr>
          <th>環境類型</th>
          <th>建議節奏</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>有 PR 流程 + checkov 的環境</td>
          <td>每季</td>
          <td>新規則已被 PR 攔截，稽核主要看 drift</td>
      </tr>
      <tr>
          <td>有 IaC 但沒有 PR 護欄</td>
          <td>每月</td>
          <td>手動 apply 可能繞過審查</td>
      </tr>
      <tr>
          <td>全手動環境</td>
          <td>每月或每次事故後</td>
          <td>沒有任何自動攔截機制</td>
      </tr>
  </tbody>
</table>
<p>稽核產出一份報告：SG 總數、0.0.0.0/0 規則數、未使用 SG 數、上次稽核以來的變化。這份報告可以作為治理進度的量化指標，納入月報。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">網路地基 — security group 設計</a>：SG 的設計原則（最小開放、group 互相引用）</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/plan-review-apply-guardrails/" data-link-title="infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">infra 走 PR 流程</a>：checkov/tfsec 在 PR 階段攔截 0.0.0.0/0</li>
<li>→ <a href="/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">治理好習慣 — tagging</a>：tag 是識別 SG owner 和清理候選的依據</li>
</ul>
]]></content:encoded></item><item><title>模組三：網路地基 — VPC 與分層</title><link>https://tarrragon.github.io/blog/infra/03-network-foundation/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/03-network-foundation/</guid><description>&lt;p>網路地基要先於核心服務存在。VPC、subnet、route table 與 security group 構成一張「服務能落在哪、誰能跟誰講話」的地圖，資料庫、運算節點與對外入口都得落在這張地圖規劃好的格子裡。先把邊界畫清楚，後面每個核心服務上線時只需要選一塊已經定義好安全等級的位置，而不是邊開服務邊補洞。&lt;/p>
&lt;p>這一章建立四層邊界：最外層的 VPC 隔離、中層的 public / private subnet 切分、流量進出的 route table 與 NAT、以及最貼近服務的 security group。每一層解決的問題不同，疊起來才是一個可審計、可收斂的網路。&lt;/p>
&lt;h2 id="vpc網路隔離的最外層邊界">VPC：網路隔離的最外層邊界&lt;/h2>
&lt;p>VPC（Virtual Private Cloud）先圈定整個系統的網路地址空間 — 一塊邏輯隔離的私有網段，是其餘所有網路切分的起點。在 VPC 裡開出來的所有資源預設只看得到同一個 VPC 內的成員，與其他 VPC、與其他帳號的網路天然隔離。它是後面所有切分動作的容器 — 沒有 VPC，subnet 與 security group 無處依附。&lt;/p>
&lt;p>建立 VPC 時最關鍵的決策是 CIDR 區塊的大小，例如 &lt;code>10.0.0.0/16&lt;/code> 提供約六萬五千個位址。這個範圍要一次規劃足夠大，因為事後擴張地址空間在多數雲上是麻煩且容易出錯的操作。同時要避免與公司其他網段重疊：未來若要透過 VPC peering、Transit Gateway 或 VPN 把這個 VPC 接回地端機房或其他環境，重疊的 CIDR 會讓路由無法解析。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_vpc&amp;#34; &amp;#34;main&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n"> cidr_block&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;10.0.0.0/16&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n"> enable_dns_support&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> enable_dns_hostnames&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> tags&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> Name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;platform-main&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> Environment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;production&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀訊號：規劃 CIDR 時先問「這個環境三年後會有幾個 subnet、跨幾個可用區、要不要接地端」。風險集中在地址耗盡與網段衝突 — 兩者都得在開第一個 subnet 之前定案。邊界是：VPC 只負責隔離與定址，它不決定哪個服務能對外，那是 subnet 與 security group 的工作。環境之間的 VPC 該怎麼分，是「模組四：環境分離與模組化」的主題，這裡只先確保單一 VPC 的地址規劃站得住。&lt;/p>
&lt;h2 id="public-與-private-subnet-的切分原則">public 與 private subnet 的切分原則&lt;/h2>
&lt;p>一塊資源對外暴露到什麼程度，取決於它被放進哪個 subnet — VPC 內部按可用區與暴露程度切出來的子網段，決定資源有沒有一條通往網際網路的路徑。判斷一個資源該放 public 還是 private，問題只有一個：它需不需要被網際網路直接定址。&lt;/p>
&lt;p>public subnet 放的是必須接收外部入站流量的元件 — 對外的負載平衡器、需要公開的 NAT Gateway、堡壘主機（bastion）。這些資源透過 route table 連到 Internet Gateway，因此能被外部 IP 直接觸及。private subnet 放的是只該在內網被存取的元件 — 應用伺服器、資料庫、快取、內部佇列。它們沒有通往 Internet Gateway 的路由，外部無法主動連入，需要對外時才透過 NAT 出去。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Subnet 類型&lt;/th>
 &lt;th>典型住戶&lt;/th>
 &lt;th>對外路徑&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>public&lt;/td>
 &lt;td>對外 LB、NAT Gateway、bastion&lt;/td>
 &lt;td>經 Internet Gateway 雙向&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>private&lt;/td>
 &lt;td>應用節點、資料庫、快取、佇列&lt;/td>
 &lt;td>僅經 NAT 單向出站、不可入站&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>public subnet 的真實樣貌是「薄薄一層」：它通常只住負載平衡器與 NAT 這類入口設施，而不是業務邏輯。常見陷阱是為了 SSH 方便把應用伺服器直接開在 public subnet 並配公網 IP，等於把每一台業務主機的管理埠暴露在掃描流量下。private subnet 的住戶反而是系統的主體 — 資料庫放這裡是因為它一旦能被外網定址，攻擊面就從「打穿入口層」變成「直接連資料庫埠試密碼」。&lt;/p>
&lt;p>每個 subnet 綁定單一可用區，所以高可用設計通常是每種角色跨至少兩個可用區各開一個 subnet：兩個 public、兩個 private，讓單一可用區故障時另一區的同類 subnet 還能承接。對外入口怎麼把流量分到跨可用區的 private 後端，是「devops 模組一：負載平衡」的範圍。&lt;/p>
&lt;h2 id="route-table-與-nat流量的進出路徑">route table 與 NAT：流量的進出路徑&lt;/h2>
&lt;p>離開一個 subnet 的封包往哪走，逐條寫在 route table 這組轉送規則裡 — 它掛在 subnet 上，是封包出口方向的依據。一個 subnet 是 public 還是 private，技術上的差別就在它關聯的 route table 裡有沒有一條指向 Internet Gateway 的預設路由。換句話說，subnet 的對外性質由它關聯的 route table 賦予，而非寫在 subnet 自身。&lt;/p></description><content:encoded><![CDATA[<p>網路地基要先於核心服務存在。VPC、subnet、route table 與 security group 構成一張「服務能落在哪、誰能跟誰講話」的地圖，資料庫、運算節點與對外入口都得落在這張地圖規劃好的格子裡。先把邊界畫清楚，後面每個核心服務上線時只需要選一塊已經定義好安全等級的位置，而不是邊開服務邊補洞。</p>
<p>這一章建立四層邊界：最外層的 VPC 隔離、中層的 public / private subnet 切分、流量進出的 route table 與 NAT、以及最貼近服務的 security group。每一層解決的問題不同，疊起來才是一個可審計、可收斂的網路。</p>
<h2 id="vpc網路隔離的最外層邊界">VPC：網路隔離的最外層邊界</h2>
<p>VPC（Virtual Private Cloud）先圈定整個系統的網路地址空間 — 一塊邏輯隔離的私有網段，是其餘所有網路切分的起點。在 VPC 裡開出來的所有資源預設只看得到同一個 VPC 內的成員，與其他 VPC、與其他帳號的網路天然隔離。它是後面所有切分動作的容器 — 沒有 VPC，subnet 與 security group 無處依附。</p>
<p>建立 VPC 時最關鍵的決策是 CIDR 區塊的大小，例如 <code>10.0.0.0/16</code> 提供約六萬五千個位址。這個範圍要一次規劃足夠大，因為事後擴張地址空間在多數雲上是麻煩且容易出錯的操作。同時要避免與公司其他網段重疊：未來若要透過 VPC peering、Transit Gateway 或 VPN 把這個 VPC 接回地端機房或其他環境，重疊的 CIDR 會讓路由無法解析。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_vpc&#34; &#34;main&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  cidr_block</span>           <span class="o">=</span> <span class="s2">&#34;10.0.0.0/16&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  enable_dns_support</span>   <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  enable_dns_hostnames</span> <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">    Name</span>        <span class="o">=</span> <span class="s2">&#34;platform-main&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">    Environment</span> <span class="o">=</span> <span class="s2">&#34;production&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  }
</span></span><span class="line"><span class="ln">10</span><span class="cl">}</span></span></code></pre></div><p>判讀訊號：規劃 CIDR 時先問「這個環境三年後會有幾個 subnet、跨幾個可用區、要不要接地端」。風險集中在地址耗盡與網段衝突 — 兩者都得在開第一個 subnet 之前定案。邊界是：VPC 只負責隔離與定址，它不決定哪個服務能對外，那是 subnet 與 security group 的工作。環境之間的 VPC 該怎麼分，是「模組四：環境分離與模組化」的主題，這裡只先確保單一 VPC 的地址規劃站得住。</p>
<h2 id="public-與-private-subnet-的切分原則">public 與 private subnet 的切分原則</h2>
<p>一塊資源對外暴露到什麼程度，取決於它被放進哪個 subnet — VPC 內部按可用區與暴露程度切出來的子網段，決定資源有沒有一條通往網際網路的路徑。判斷一個資源該放 public 還是 private，問題只有一個：它需不需要被網際網路直接定址。</p>
<p>public subnet 放的是必須接收外部入站流量的元件 — 對外的負載平衡器、需要公開的 NAT Gateway、堡壘主機（bastion）。這些資源透過 route table 連到 Internet Gateway，因此能被外部 IP 直接觸及。private subnet 放的是只該在內網被存取的元件 — 應用伺服器、資料庫、快取、內部佇列。它們沒有通往 Internet Gateway 的路由，外部無法主動連入，需要對外時才透過 NAT 出去。</p>
<table>
  <thead>
      <tr>
          <th>Subnet 類型</th>
          <th>典型住戶</th>
          <th>對外路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>public</td>
          <td>對外 LB、NAT Gateway、bastion</td>
          <td>經 Internet Gateway 雙向</td>
      </tr>
      <tr>
          <td>private</td>
          <td>應用節點、資料庫、快取、佇列</td>
          <td>僅經 NAT 單向出站、不可入站</td>
      </tr>
  </tbody>
</table>
<p>public subnet 的真實樣貌是「薄薄一層」：它通常只住負載平衡器與 NAT 這類入口設施，而不是業務邏輯。常見陷阱是為了 SSH 方便把應用伺服器直接開在 public subnet 並配公網 IP，等於把每一台業務主機的管理埠暴露在掃描流量下。private subnet 的住戶反而是系統的主體 — 資料庫放這裡是因為它一旦能被外網定址，攻擊面就從「打穿入口層」變成「直接連資料庫埠試密碼」。</p>
<p>每個 subnet 綁定單一可用區，所以高可用設計通常是每種角色跨至少兩個可用區各開一個 subnet：兩個 public、兩個 private，讓單一可用區故障時另一區的同類 subnet 還能承接。對外入口怎麼把流量分到跨可用區的 private 後端，是「devops 模組一：負載平衡」的範圍。</p>
<h2 id="route-table-與-nat流量的進出路徑">route table 與 NAT：流量的進出路徑</h2>
<p>離開一個 subnet 的封包往哪走，逐條寫在 route table 這組轉送規則裡 — 它掛在 subnet 上，是封包出口方向的依據。一個 subnet 是 public 還是 private，技術上的差別就在它關聯的 route table 裡有沒有一條指向 Internet Gateway 的預設路由。換句話說，subnet 的對外性質由它關聯的 route table 賦予，而非寫在 subnet 自身。</p>
<p>public subnet 的 route table 有一條 <code>0.0.0.0/0 → Internet Gateway</code>，讓未知目的地的流量直接出網、也讓外部可達。private subnet 的 route table 則把 <code>0.0.0.0/0</code> 指向 NAT Gateway。NAT（Network Address Translation）解決的問題是：private subnet 的資源需要主動對外（拉套件、呼叫第三方 API、抓 OS 更新），但不能因此變得可被外部入站連入。NAT 讓出站流量借用一個公網位址出去、把回應導回原請求者，同時不開放任何外部主動發起的連線。</p>
<p>NAT Gateway 的核心取捨是成本與可用性。它是綁定單一可用區的資源 — 一個 NAT Gateway 活在某一個 public subnet、也就活在那個可用區裡。若全部 private subnet 的 route table 都指向同一個 NAT，這個設計用一份 NAT 成本服務整個 VPC，代價是把 NAT 所在的可用區變成出站方向的單點：該可用區故障時，所有 private subnet 的對外連線同時中斷，即使其他可用區的節點本身健康。要讓出站路徑與 subnet 的跨可用區冗餘對齊，做法是每個可用區各放一個 NAT Gateway，並讓每一區的 private subnet route table 指向同區的 NAT。下面用 <code>for_each</code> 在每個可用區建立一個 NAT，再讓每個 private subnet 的 route table 走本區出口。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_nat_gateway&#34; &#34;per_az&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  for_each</span>      <span class="o">=</span> <span class="k">aws_subnet</span><span class="p">.</span><span class="k">public</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  allocation_id</span> <span class="o">=</span> <span class="k">aws_eip</span><span class="p">.</span><span class="k">nat</span><span class="p">[</span><span class="k">each</span><span class="p">.</span><span class="k">key</span><span class="p">].</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  subnet_id</span>     <span class="o">=</span> <span class="k">each</span><span class="p">.</span><span class="k">value</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_route_table&#34; &#34;private&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  for_each</span> <span class="o">=</span> <span class="k">aws_subnet</span><span class="p">.</span><span class="k">private</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  vpc_id</span>   <span class="o">=</span> <span class="k">aws_vpc</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">route</span> {
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">    cidr_block</span>     <span class="o">=</span> <span class="s2">&#34;0.0.0.0/0&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">    nat_gateway_id</span> <span class="o">=</span> <span class="k">aws_nat_gateway</span><span class="p">.</span><span class="k">per_az</span><span class="p">[</span><span class="k">each</span><span class="p">.</span><span class="k">key</span><span class="p">].</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  }
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;private-rt-${each.key}&#34;</span> }
</span></span><span class="line"><span class="ln">17</span><span class="cl">}</span></span></code></pre></div><p>每個可用區一個 NAT 是可用性優先的版本；若環境對成本敏感、且能接受出站在單一可用區故障時短暫中斷，也可以退回單一 NAT，但要把它當成明示的取捨、而非預設。判讀訊號：private subnet 的服務拉不到外部套件、或第三方 API 全部逾時，先查它關聯的 route table 有沒有指向健康的 NAT；若只有某一個可用區的節點受影響，多半是那一區的 NAT 或其所在 subnet 出狀況。風險與成本在這裡交會 — NAT Gateway 按處理流量計費，把大量出站流量（例如備份上傳、跨區同步）長期走 NAT 會讓帳單可觀，這類流量較划算的做法是改走 VPC Endpoint 直連雲服務、繞過 NAT。NAT 的數量取捨與出站成本在「devops 模組八：成本管理」有更完整的討論。邊界是：route table 與 NAT 只管「能不能出去、走哪條路」，至於某個埠允不允許連，是 security group 的職責。</p>
<h2 id="security-group-設計最小開放">security group 設計：最小開放</h2>
<p>一條連線究竟能不能打到某個埠，由 security group 逐埠拍板 — 它是掛在資源網卡層級的有狀態防火牆，規則描述的是哪些來源連得進這個資源。它是貼著服務的最後一道網路邊界 — 即使封包順著 route table 抵達了 private subnet，security group 仍能逐埠決定放不放行。它有狀態的意思是：放行一條入站連線後，對應的回應出站自動允許，規則只需描述入站方向想開放什麼。</p>
<p>設計原則是最小開放：每條規則只開「這個服務確實需要被誰連的那個埠」。資料庫的 security group 入站只允許來自應用層 security group 的資料庫埠，而不是某個 IP 範圍。用 security group 互相引用、而非寫死網段，是因為應用節點會隨擴縮而換 IP，引用來源 group 讓規則跟著成員身分走、不跟著位址走。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_security_group_rule&#34; &#34;db_from_app&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  type</span>                     <span class="o">=</span> <span class="s2">&#34;ingress&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  from_port</span>                <span class="o">=</span> <span class="m">5432</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  to_port</span>                  <span class="o">=</span> <span class="m">5432</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">  protocol</span>                 <span class="o">=</span> <span class="s2">&#34;tcp&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">  security_group_id</span>        <span class="o">=</span> <span class="k">aws_security_group</span><span class="p">.</span><span class="k">database</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">  source_security_group_id</span> <span class="o">=</span> <span class="k">aws_security_group</span><span class="p">.</span><span class="k">app</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">}</span></span></code></pre></div><p>要特別防的是 <code>0.0.0.0/0</code> 全開。把入站來源設成 <code>0.0.0.0/0</code> 等於允許整個網際網路連這個埠，對資料庫埠（5432、3306、6379）或管理埠（22、3389）這麼做，會讓服務暴露在持續性的自動掃描與暴力嘗試下。合理出現 <code>0.0.0.0/0</code> 的位置只有對外負載平衡器的 80 / 443 入站 — 因為它的工作本來就是接收公開流量。判讀訊號：盤點所有 security group，列出 source 是 <code>0.0.0.0/0</code> 的規則，逐條問「這個埠真的需要全世界都連得到嗎」；資料庫埠、SSH、內部 API 出現在這份清單上就是該收斂的目標。管理埠的存取較划算的替代方案是 SSM Session Manager 或堡壘主機，把 22 埠從公網清單上拿掉。誰能透過 IAM 改動這些規則，銜接「模組二：身分與憑證地基」。</p>
<p>subnet 這一層還有另一道防火牆 — network ACL（NACL），它與 security group 分工在兩個層級。NACL 掛在 subnet 上、作用於進出整個 subnet 的流量，而且是無狀態的：入站與出站要各寫一條規則，放行了入站不代表回應的出站自動放行，回程封包得自己對得上另一條規則。security group 則掛在資源網卡（ENI）層、有狀態，放行入站後對應回應自動允許。兩者的另一個差別是 NACL 支援顯式 deny、security group 只能列允許清單，所以 NACL 適合做 subnet 層的粗篩或針對特定來源的明確封鎖。實務上多數設計的主力是 security group：它貼著服務、用 group 互相引用就能表達「誰能連誰」，已經涵蓋大部分最小開放需求。NACL 留給少數情境 — 需要在 subnet 邊界擋掉一整段已知惡意網段、或要對某類流量做顯式 deny 時才展開；多數環境讓 NACL 維持預設全通、把存取控制集中在 security group，是可以接受的選擇，重點是知道這一層存在、在需要 subnet 層粗篩時記得它。</p>
<h2 id="為什麼網路要先於核心服務鋪好">為什麼網路要先於核心服務鋪好</h2>
<p>網路地基先行，是因為核心服務的安全位置由網路拓樸決定，而不是反過來。資料庫該落在哪個 private subnet、它的 security group 只接受哪個來源、它的出站走不走 NAT — 這些都是服務「出生時」就該確定的屬性。先有規劃好的 subnet 與 security group，新服務上線只是挑一塊已定義安全等級的位置放進去；網路還沒鋪就先開服務，則往往落在預設 VPC 與寬鬆規則上，事後再回頭收斂，要在服務已經有流量、有依賴的情況下改網段與防火牆，風險和協調成本都高得多。</p>
<p>這也呼應「模組零：infra 是什麼」的 day-1 鐵律：邊界與隔離屬於一開始就該存在的地基，不是長出問題後才補的修補。網路規劃好之後，照「從零建置」路線下一步先進「模組四：環境分離與模組化」確定環境怎麼切，再讓核心服務落進這些 subnet。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">網路地基 — VPC、subnet 分層與 security group 設計</a></td>
          <td>VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計與 NACL 定位</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/03-network-foundation/security-group-audit-cleanup/" data-link-title="Security Group 稽核與清理" data-link-desc="盤點所有 security group 規則、找出 0.0.0.0/0 全開與未使用的 SG、依賴檢查後安全刪除、自動化治理">Security Group 稽核與清理</a></td>
          <td>0.0.0.0/0 偵測、未使用 SG 識別、依賴檢查、清理工作流、自動化治理</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：誰有權改動 security group 與路由表</li>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC</a>：核心服務怎麼落進規劃好的 subnet</li>
<li>→ <a href="/blog/devops/01-load-balancing/" data-link-title="模組一：負載平衡與反向代理" data-link-desc="流量進來怎麼分給多個服務實例 — nginx / HAProxy / DNS round-robin 的選型和健康檢查路由設計">devops 模組一：負載平衡</a>：入口流量怎麼分到 private subnet 的後端</li>
<li>→ <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a>：NAT 與出站流量的成本取捨</li>
</ul>
]]></content:encoded></item><item><title>網路斷線 UX 模式</title><link>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/network-offline-ux/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/network-offline-ux/</guid><description>&lt;p>網路 &lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/gate/" data-link-title="Gate（UX）" data-link-desc="說明使用者操作流程中「必須通過才能繼續」的關卡，以及成功/失敗/不確定三條路徑的設計責任">gate&lt;/a> 和其他 gate 的差異在於狀態的連續性。生物辨識是二元結果（通過或不通過），網路狀態是連續的 — 連線中、已連線、斷線、重新連線、連線但延遲高、連線但頻繁斷開。處理策略取決於功能對即時連線的依賴程度。&lt;/p>
&lt;h2 id="三種處理策略">三種處理策略&lt;/h2>
&lt;h3 id="offline-first">Offline-first&lt;/h3>
&lt;p>功能的核心操作在本地完成，網路用於同步。斷線時使用者仍可操作，重新連線後自動同步差異。&lt;/p>
&lt;p>Offline-first 適合的前提是資料可以本地存儲且衝突可以解決。筆記 app、待辦事項、表單填寫 — 使用者的操作產生本地資料，網路只負責把資料同步到 server。&lt;/p>
&lt;p>Offline-first 的 UX 設計重點是讓使用者知道同步狀態：已同步、待同步、同步失敗。不需要 gate — 網路狀態不阻擋使用者操作。&lt;/p>
&lt;h3 id="retry-with-feedback">Retry with feedback&lt;/h3>
&lt;p>功能需要網路但可以等待。斷線時顯示狀態和重試選項，使用者決定要等還是離開。&lt;/p>
&lt;p>app_tunnel 的 terminal 連線屬於這個模式。WebSocket 連線需要網路，斷線時使用者無法操作終端機。error 和 disconnected 狀態提供重連按鈕讓使用者手動重試。&lt;/p>
&lt;p>Retry 策略的 UX 設計重點：&lt;/p>
&lt;ul>
&lt;li>告知使用者發生什麼（「連線中斷」而非空白畫面）&lt;/li>
&lt;li>提供手動重試（重連按鈕）&lt;/li>
&lt;li>提供退出路徑（返回首頁 — app_tunnel 原本缺少這個）&lt;/li>
&lt;li>自動重試要有上限和間隔遞增（避免無限重試消耗電量）&lt;/li>
&lt;/ul>
&lt;h3 id="degraded-mode">Degraded mode&lt;/h3>
&lt;p>功能部分依賴網路。核心功能離線可用，進階功能需要網路。斷線時自動切換到降級模式，不阻擋使用者操作但功能受限。&lt;/p>
&lt;p>降級模式的 UX 設計重點是清楚標示哪些功能可用、哪些不可用。「離線模式 — 搜尋功能暫時不可用」比靜默隱藏搜尋按鈕更透明。&lt;/p>
&lt;h2 id="網路狀態的-ui-呈現">網路狀態的 UI 呈現&lt;/h2>
&lt;h3 id="全域指示器">全域指示器&lt;/h3>
&lt;p>在 app 頂部或狀態列顯示「離線」標示。適合網路狀態影響全域功能的 app。&lt;/p>
&lt;h3 id="功能級指示器">功能級指示器&lt;/h3>
&lt;p>在需要網路的功能旁邊顯示不可用狀態。適合只有部分功能依賴網路的 app。&lt;/p>
&lt;h3 id="非侵入式通知">非侵入式通知&lt;/h3>
&lt;p>用 Snackbar 或 Toast 短暫顯示「已恢復連線」或「網路中斷」。適合網路狀態偶爾變化的場景。不適合頻繁斷開重連的場景（通知太多會干擾使用者）。&lt;/p>
&lt;h2 id="連線但品質差的場景">連線但品質差的場景&lt;/h2>
&lt;p>網路存在但延遲高或頻繁斷開，比完全離線更難處理。完全離線時 app 可以立即切換到離線模式；連線不穩定時，每次請求可能成功也可能逾時，使用者體驗是「有時候行有時候不行」。&lt;/p>
&lt;p>處理策略：&lt;/p>
&lt;ul>
&lt;li>設定合理的逾時時間（太短會把慢回應判定為失敗，太長讓使用者等太久）&lt;/li>
&lt;li>逾時後顯示狀態和重試選項，不自動重試（避免在不穩定網路上累積重試）&lt;/li>
&lt;li>在 loading 狀態提供取消選項，讓使用者可以中斷等待&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Gate 設計的通用方法論 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法&lt;/a>&lt;/li>
&lt;li>權限請求的 UX 設計 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/permission-request-timing/" data-link-title="Permission 請求時機與措辭" data-link-desc="系統權限請求的時機選擇（首次開啟 vs 功能使用時）和說明文字的設計 — 使用者只有一次機會理解為什麼需要這個權限">Permission 請求時機與措辭&lt;/a>&lt;/li>
&lt;li>畫面狀態矩陣中的網路狀態 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一 畫面狀態機&lt;/a>&lt;/li>
&lt;li>Server 端背壓如何影響 client UX → &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &amp;#43; 回壓訊號的設計、和 rate limit 的區別">DevOps 背壓機制&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>網路 <a href="/blog/ux-design/knowledge-cards/gate/" data-link-title="Gate（UX）" data-link-desc="說明使用者操作流程中「必須通過才能繼續」的關卡，以及成功/失敗/不確定三條路徑的設計責任">gate</a> 和其他 gate 的差異在於狀態的連續性。生物辨識是二元結果（通過或不通過），網路狀態是連續的 — 連線中、已連線、斷線、重新連線、連線但延遲高、連線但頻繁斷開。處理策略取決於功能對即時連線的依賴程度。</p>
<h2 id="三種處理策略">三種處理策略</h2>
<h3 id="offline-first">Offline-first</h3>
<p>功能的核心操作在本地完成，網路用於同步。斷線時使用者仍可操作，重新連線後自動同步差異。</p>
<p>Offline-first 適合的前提是資料可以本地存儲且衝突可以解決。筆記 app、待辦事項、表單填寫 — 使用者的操作產生本地資料，網路只負責把資料同步到 server。</p>
<p>Offline-first 的 UX 設計重點是讓使用者知道同步狀態：已同步、待同步、同步失敗。不需要 gate — 網路狀態不阻擋使用者操作。</p>
<h3 id="retry-with-feedback">Retry with feedback</h3>
<p>功能需要網路但可以等待。斷線時顯示狀態和重試選項，使用者決定要等還是離開。</p>
<p>app_tunnel 的 terminal 連線屬於這個模式。WebSocket 連線需要網路，斷線時使用者無法操作終端機。error 和 disconnected 狀態提供重連按鈕讓使用者手動重試。</p>
<p>Retry 策略的 UX 設計重點：</p>
<ul>
<li>告知使用者發生什麼（「連線中斷」而非空白畫面）</li>
<li>提供手動重試（重連按鈕）</li>
<li>提供退出路徑（返回首頁 — app_tunnel 原本缺少這個）</li>
<li>自動重試要有上限和間隔遞增（避免無限重試消耗電量）</li>
</ul>
<h3 id="degraded-mode">Degraded mode</h3>
<p>功能部分依賴網路。核心功能離線可用，進階功能需要網路。斷線時自動切換到降級模式，不阻擋使用者操作但功能受限。</p>
<p>降級模式的 UX 設計重點是清楚標示哪些功能可用、哪些不可用。「離線模式 — 搜尋功能暫時不可用」比靜默隱藏搜尋按鈕更透明。</p>
<h2 id="網路狀態的-ui-呈現">網路狀態的 UI 呈現</h2>
<h3 id="全域指示器">全域指示器</h3>
<p>在 app 頂部或狀態列顯示「離線」標示。適合網路狀態影響全域功能的 app。</p>
<h3 id="功能級指示器">功能級指示器</h3>
<p>在需要網路的功能旁邊顯示不可用狀態。適合只有部分功能依賴網路的 app。</p>
<h3 id="非侵入式通知">非侵入式通知</h3>
<p>用 Snackbar 或 Toast 短暫顯示「已恢復連線」或「網路中斷」。適合網路狀態偶爾變化的場景。不適合頻繁斷開重連的場景（通知太多會干擾使用者）。</p>
<h2 id="連線但品質差的場景">連線但品質差的場景</h2>
<p>網路存在但延遲高或頻繁斷開，比完全離線更難處理。完全離線時 app 可以立即切換到離線模式；連線不穩定時，每次請求可能成功也可能逾時，使用者體驗是「有時候行有時候不行」。</p>
<p>處理策略：</p>
<ul>
<li>設定合理的逾時時間（太短會把慢回應判定為失敗，太長讓使用者等太久）</li>
<li>逾時後顯示狀態和重試選項，不自動重試（避免在不穩定網路上累積重試）</li>
<li>在 loading 狀態提供取消選項，讓使用者可以中斷等待</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Gate 設計的通用方法論 → <a href="/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法</a></li>
<li>權限請求的 UX 設計 → <a href="/blog/ux-design/02-gate-fallback/permission-request-timing/" data-link-title="Permission 請求時機與措辭" data-link-desc="系統權限請求的時機選擇（首次開啟 vs 功能使用時）和說明文字的設計 — 使用者只有一次機會理解為什麼需要這個權限">Permission 請求時機與措辭</a></li>
<li>畫面狀態矩陣中的網路狀態 → <a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一 畫面狀態機</a></li>
<li>Server 端背壓如何影響 client UX → <a href="/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &#43; 回壓訊號的設計、和 rate limit 的區別">DevOps 背壓機制</a></li>
</ul>
]]></content:encoded></item><item><title>VPC（Virtual Private Cloud）</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/vpc/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/vpc/</guid><description>&lt;p>VPC（Virtual Private Cloud）是雲端帳號內的一塊邏輯隔離私有網段，是其餘所有網路切分的起點。在 VPC 裡開出來的所有資源預設只看得到同一個 VPC 內的成員，與其他 VPC、與其他帳號的網路天然隔離。沒有 VPC，&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">subnet&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">security group&lt;/a> 無處依附。&lt;/p>
&lt;p>VPC 用 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/cidr/" data-link-title="CIDR（Classless Inter-Domain Routing）" data-link-desc="用前綴長度表示 IP 地址範圍的表示法，決定 VPC 與 subnet 的地址空間大小">CIDR&lt;/a> 區塊定義地址空間。建立時的 CIDR 大小是一次性決策——事後擴張地址空間在多數雲端平台上是麻煩且容易出錯的操作（AWS 允許追加 secondary CIDR，但追加的網段在 routing 與服務相容性上有限制）。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>VPC 是&lt;a href="https://tarrragon.github.io/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">模組三：網路地基&lt;/a>的最外層邊界。Infra 系列的網路設計從 VPC 開始：先圈定地址空間，再往內切 subnet、掛 route table、設 security group。環境之間的 VPC 怎麼分（每個環境一個 VPC），屬於&lt;a href="https://tarrragon.github.io/blog/infra/04-environment-separation/directory-module-parameterization/" data-link-title="環境分離與模組化 — 目錄結構、module 參數化與 retrofit 路徑" data-link-desc="用目錄結構在第一天就隔開 dev 與 prod 的 state，用 module 讓環境共用同一套邏輯只差參數，以及已經單環境跑起來後怎麼安全拆分">模組四：環境分離&lt;/a>的設計決策。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>VPC 設計需要關注的訊號：CIDR 空間快用完（subnet 切不出新的子網段）、需要跟其他 VPC 或地端互連時發現 CIDR 重疊（peering 無法建立）、服務被放在預設 VPC 裡（預設 VPC 是所有人共享的、CIDR 不可控的、security group 預設全通的）。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>規劃 VPC 時要決定：&lt;/p>
&lt;ul>
&lt;li>CIDR 大小：&lt;code>/16&lt;/code> 提供約六萬五千個位址，對多數單一環境足夠&lt;/li>
&lt;li>不重疊：多個 VPC（不同環境或產品線）用連續但不重疊的大段分配&lt;/li>
&lt;li>DNS 設定：&lt;code>enable_dns_support&lt;/code> 和 &lt;code>enable_dns_hostnames&lt;/code> 在多數場景都該開啟&lt;/li>
&lt;li>預設 VPC 的處理：正式服務不該放在預設 VPC，新帳號的預設 VPC 可以刪除或保留唯讀&lt;/li>
&lt;/ul>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">Subnet&lt;/a> — VPC 內按可用區與暴露程度切出的子網段&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">Security Group&lt;/a> — 掛在資源上的有狀態防火牆&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/cidr/" data-link-title="CIDR（Classless Inter-Domain Routing）" data-link-desc="用前綴長度表示 IP 地址範圍的表示法，決定 VPC 與 subnet 的地址空間大小">CIDR&lt;/a> — VPC 的地址空間定義方式&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/nat/" data-link-title="NAT Gateway" data-link-desc="讓 private subnet 的資源主動對外連線、同時不被外部入站觸及的網路地址轉換服務">NAT&lt;/a> — 讓 private subnet 出站的地址轉換機制&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>VPC（Virtual Private Cloud）是雲端帳號內的一塊邏輯隔離私有網段，是其餘所有網路切分的起點。在 VPC 裡開出來的所有資源預設只看得到同一個 VPC 內的成員，與其他 VPC、與其他帳號的網路天然隔離。沒有 VPC，<a href="/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">subnet</a> 與 <a href="/blog/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">security group</a> 無處依附。</p>
<p>VPC 用 <a href="/blog/infra/knowledge-cards/cidr/" data-link-title="CIDR（Classless Inter-Domain Routing）" data-link-desc="用前綴長度表示 IP 地址範圍的表示法，決定 VPC 與 subnet 的地址空間大小">CIDR</a> 區塊定義地址空間。建立時的 CIDR 大小是一次性決策——事後擴張地址空間在多數雲端平台上是麻煩且容易出錯的操作（AWS 允許追加 secondary CIDR，但追加的網段在 routing 與服務相容性上有限制）。</p>
<h2 id="概念位置">概念位置</h2>
<p>VPC 是<a href="/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">模組三：網路地基</a>的最外層邊界。Infra 系列的網路設計從 VPC 開始：先圈定地址空間，再往內切 subnet、掛 route table、設 security group。環境之間的 VPC 怎麼分（每個環境一個 VPC），屬於<a href="/blog/infra/04-environment-separation/directory-module-parameterization/" data-link-title="環境分離與模組化 — 目錄結構、module 參數化與 retrofit 路徑" data-link-desc="用目錄結構在第一天就隔開 dev 與 prod 的 state，用 module 讓環境共用同一套邏輯只差參數，以及已經單環境跑起來後怎麼安全拆分">模組四：環境分離</a>的設計決策。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>VPC 設計需要關注的訊號：CIDR 空間快用完（subnet 切不出新的子網段）、需要跟其他 VPC 或地端互連時發現 CIDR 重疊（peering 無法建立）、服務被放在預設 VPC 裡（預設 VPC 是所有人共享的、CIDR 不可控的、security group 預設全通的）。</p>
<h2 id="設計責任">設計責任</h2>
<p>規劃 VPC 時要決定：</p>
<ul>
<li>CIDR 大小：<code>/16</code> 提供約六萬五千個位址，對多數單一環境足夠</li>
<li>不重疊：多個 VPC（不同環境或產品線）用連續但不重疊的大段分配</li>
<li>DNS 設定：<code>enable_dns_support</code> 和 <code>enable_dns_hostnames</code> 在多數場景都該開啟</li>
<li>預設 VPC 的處理：正式服務不該放在預設 VPC，新帳號的預設 VPC 可以刪除或保留唯讀</li>
</ul>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">Subnet</a> — VPC 內按可用區與暴露程度切出的子網段</li>
<li><a href="/blog/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">Security Group</a> — 掛在資源上的有狀態防火牆</li>
<li><a href="/blog/infra/knowledge-cards/cidr/" data-link-title="CIDR（Classless Inter-Domain Routing）" data-link-desc="用前綴長度表示 IP 地址範圍的表示法，決定 VPC 與 subnet 的地址空間大小">CIDR</a> — VPC 的地址空間定義方式</li>
<li><a href="/blog/infra/knowledge-cards/nat/" data-link-title="NAT Gateway" data-link-desc="讓 private subnet 的資源主動對外連線、同時不被外部入站觸及的網路地址轉換服務">NAT</a> — 讓 private subnet 出站的地址轉換機制</li>
</ul>
]]></content:encoded></item><item><title>Subnet（子網路）</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/subnet/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/subnet/</guid><description>&lt;p>Subnet 是 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC&lt;/a> 內部按可用區（Availability Zone）與暴露程度切出來的子網段。一塊資源對外暴露到什麼程度，取決於它被放進哪個 subnet——技術上的差別在於該 subnet 關聯的 route table 裡有沒有一條指向 Internet Gateway 的預設路由。&lt;/p>
&lt;p>Subnet 分兩類：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Public subnet&lt;/strong>：route table 有 &lt;code>0.0.0.0/0 → Internet Gateway&lt;/code>，讓資源能被外部 IP 直接觸及。典型住戶是對外負載平衡器、&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/nat/" data-link-title="NAT Gateway" data-link-desc="讓 private subnet 的資源主動對外連線、同時不被外部入站觸及的網路地址轉換服務">NAT&lt;/a> Gateway。&lt;/li>
&lt;li>&lt;strong>Private subnet&lt;/strong>：route table 把 &lt;code>0.0.0.0/0&lt;/code> 指向 NAT Gateway，外部無法主動連入。典型住戶是應用伺服器、資料庫、快取。&lt;/li>
&lt;/ul>
&lt;p>Public subnet 的真實樣貌是「薄薄一層」——它通常只住入口設施，業務邏輯跟資料儲存都在 private subnet。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Subnet 是&lt;a href="https://tarrragon.github.io/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">模組三：網路地基&lt;/a>的中層邊界。VPC 定好地址空間後，subnet 決定「哪些資源能被外網碰到、哪些只能在內網存取」。每個 subnet 綁定單一可用區，高可用設計通常是每種角色跨至少兩個可用區各開一個 subnet。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>Subnet 配置有問題的訊號：應用伺服器被放在 public subnet 並配了公網 IP（管理埠暴露在掃描流量下）、private subnet 的服務拉不到外部套件（route table 沒指向健康的 NAT）、新服務上線時找不到適合的 subnet（CIDR 切得太小、空間不夠）。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>規劃 subnet 時要決定：&lt;/p>
&lt;ul>
&lt;li>CIDR 切法：VPC 是 &lt;code>/16&lt;/code> 時，每個 subnet 用 &lt;code>/20&lt;/code>（約四千位址）可以在三個可用區各開 public + private 共六個 subnet&lt;/li>
&lt;li>跨可用區對稱：每種角色至少跨兩個 AZ，讓單一 AZ 故障時另一區能承接&lt;/li>
&lt;li>public 的住戶限制：只放入口設施，業務邏輯一律放 private&lt;/li>
&lt;/ul>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC&lt;/a> — subnet 的容器&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/nat/" data-link-title="NAT Gateway" data-link-desc="讓 private subnet 的資源主動對外連線、同時不被外部入站觸及的網路地址轉換服務">NAT&lt;/a> — 讓 private subnet 出站的機制&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">Security Group&lt;/a> — 掛在資源上的埠級存取控制&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Subnet 是 <a href="/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC</a> 內部按可用區（Availability Zone）與暴露程度切出來的子網段。一塊資源對外暴露到什麼程度，取決於它被放進哪個 subnet——技術上的差別在於該 subnet 關聯的 route table 裡有沒有一條指向 Internet Gateway 的預設路由。</p>
<p>Subnet 分兩類：</p>
<ul>
<li><strong>Public subnet</strong>：route table 有 <code>0.0.0.0/0 → Internet Gateway</code>，讓資源能被外部 IP 直接觸及。典型住戶是對外負載平衡器、<a href="/blog/infra/knowledge-cards/nat/" data-link-title="NAT Gateway" data-link-desc="讓 private subnet 的資源主動對外連線、同時不被外部入站觸及的網路地址轉換服務">NAT</a> Gateway。</li>
<li><strong>Private subnet</strong>：route table 把 <code>0.0.0.0/0</code> 指向 NAT Gateway，外部無法主動連入。典型住戶是應用伺服器、資料庫、快取。</li>
</ul>
<p>Public subnet 的真實樣貌是「薄薄一層」——它通常只住入口設施，業務邏輯跟資料儲存都在 private subnet。</p>
<h2 id="概念位置">概念位置</h2>
<p>Subnet 是<a href="/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">模組三：網路地基</a>的中層邊界。VPC 定好地址空間後，subnet 決定「哪些資源能被外網碰到、哪些只能在內網存取」。每個 subnet 綁定單一可用區，高可用設計通常是每種角色跨至少兩個可用區各開一個 subnet。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>Subnet 配置有問題的訊號：應用伺服器被放在 public subnet 並配了公網 IP（管理埠暴露在掃描流量下）、private subnet 的服務拉不到外部套件（route table 沒指向健康的 NAT）、新服務上線時找不到適合的 subnet（CIDR 切得太小、空間不夠）。</p>
<h2 id="設計責任">設計責任</h2>
<p>規劃 subnet 時要決定：</p>
<ul>
<li>CIDR 切法：VPC 是 <code>/16</code> 時，每個 subnet 用 <code>/20</code>（約四千位址）可以在三個可用區各開 public + private 共六個 subnet</li>
<li>跨可用區對稱：每種角色至少跨兩個 AZ，讓單一 AZ 故障時另一區能承接</li>
<li>public 的住戶限制：只放入口設施，業務邏輯一律放 private</li>
</ul>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC</a> — subnet 的容器</li>
<li><a href="/blog/infra/knowledge-cards/nat/" data-link-title="NAT Gateway" data-link-desc="讓 private subnet 的資源主動對外連線、同時不被外部入站觸及的網路地址轉換服務">NAT</a> — 讓 private subnet 出站的機制</li>
<li><a href="/blog/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">Security Group</a> — 掛在資源上的埠級存取控制</li>
</ul>
]]></content:encoded></item><item><title>Security Group</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/security-group/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/security-group/</guid><description>&lt;p>Security group 是掛在資源網卡（ENI）層級的有狀態防火牆，規則描述的是「哪些來源能連到這個資源的哪個埠」。「有狀態」的意思是放行一條入站連線後，對應的回應出站自動允許——規則只需描述入站方向想開放什麼。&lt;/p>
&lt;p>設計原則是最小開放：每條規則只開「這個服務確實需要被誰連的那個埠」。資料庫的 security group 入站只允許來自應用層 security group 的資料庫埠（如 5432），而不是某個 IP 範圍。用 security group 互相引用（source 指向另一個 group 而非 CIDR）讓規則跟著成員身分走、不跟著位址走——應用節點會隨擴縮而換 IP，引用 group 不會因此失效。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Security group 是&lt;a href="https://tarrragon.github.io/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">模組三：網路地基&lt;/a>的最內層邊界——貼著服務的最後一道網路防線。即使封包順著 route table 抵達了 private &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">subnet&lt;/a>，security group 仍能逐埠決定放不放行。&lt;a href="https://tarrragon.github.io/blog/infra/07-infra-as-pr/plan-review-apply-guardrails/" data-link-title="infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程&lt;/a>用 tfsec / checkov 在 CI 攔截 &lt;code>0.0.0.0/0&lt;/code> 全開的規則。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>Security group 需要收斂的訊號：入站來源是 &lt;code>0.0.0.0/0&lt;/code>（允許全網連入），且目標埠是資料庫（5432、3306、6379）或管理埠（22、3389）——合理出現 &lt;code>0.0.0.0/0&lt;/code> 的位置只有對外負載平衡器的 80 / 443。盤點方式是列出所有 source 為 &lt;code>0.0.0.0/0&lt;/code> 的規則，逐條問「這個埠需要全世界都連得到嗎」。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 security group 時要決定：&lt;/p>
&lt;ul>
&lt;li>引用方式：用 group 互相引用（推薦）vs 用 CIDR 限定範圍&lt;/li>
&lt;li>開放範圍：只開需要的埠與來源，&lt;code>0.0.0.0/0&lt;/code> 只給對外 LB&lt;/li>
&lt;li>管理埠存取：SSH（22）改用 SSM Session Manager 取代，從公網清單上拿掉&lt;/li>
&lt;li>與 NACL 的分工：security group 是主力（有狀態、group 引用），NACL 留給少數需要 subnet 層顯式 deny 的情境&lt;/li>
&lt;/ul>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC&lt;/a> — security group 依附的網路容器&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">Subnet&lt;/a> — security group 與 subnet 各守不同層級的邊界&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Security group 是掛在資源網卡（ENI）層級的有狀態防火牆，規則描述的是「哪些來源能連到這個資源的哪個埠」。「有狀態」的意思是放行一條入站連線後，對應的回應出站自動允許——規則只需描述入站方向想開放什麼。</p>
<p>設計原則是最小開放：每條規則只開「這個服務確實需要被誰連的那個埠」。資料庫的 security group 入站只允許來自應用層 security group 的資料庫埠（如 5432），而不是某個 IP 範圍。用 security group 互相引用（source 指向另一個 group 而非 CIDR）讓規則跟著成員身分走、不跟著位址走——應用節點會隨擴縮而換 IP，引用 group 不會因此失效。</p>
<h2 id="概念位置">概念位置</h2>
<p>Security group 是<a href="/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">模組三：網路地基</a>的最內層邊界——貼著服務的最後一道網路防線。即使封包順著 route table 抵達了 private <a href="/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">subnet</a>，security group 仍能逐埠決定放不放行。<a href="/blog/infra/07-infra-as-pr/plan-review-apply-guardrails/" data-link-title="infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>用 tfsec / checkov 在 CI 攔截 <code>0.0.0.0/0</code> 全開的規則。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>Security group 需要收斂的訊號：入站來源是 <code>0.0.0.0/0</code>（允許全網連入），且目標埠是資料庫（5432、3306、6379）或管理埠（22、3389）——合理出現 <code>0.0.0.0/0</code> 的位置只有對外負載平衡器的 80 / 443。盤點方式是列出所有 source 為 <code>0.0.0.0/0</code> 的規則，逐條問「這個埠需要全世界都連得到嗎」。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 security group 時要決定：</p>
<ul>
<li>引用方式：用 group 互相引用（推薦）vs 用 CIDR 限定範圍</li>
<li>開放範圍：只開需要的埠與來源，<code>0.0.0.0/0</code> 只給對外 LB</li>
<li>管理埠存取：SSH（22）改用 SSM Session Manager 取代，從公網清單上拿掉</li>
<li>與 NACL 的分工：security group 是主力（有狀態、group 引用），NACL 留給少數需要 subnet 層顯式 deny 的情境</li>
</ul>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC</a> — security group 依附的網路容器</li>
<li><a href="/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">Subnet</a> — security group 與 subnet 各守不同層級的邊界</li>
</ul>
]]></content:encoded></item><item><title>NAT Gateway</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/nat/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/nat/</guid><description>&lt;p>NAT Gateway（Network Address Translation Gateway）的核心職責是讓 private subnet 的資源能主動發起對外連線（拉套件、呼叫第三方 API、下載 OS 更新），同時不開放任何外部主動發起的入站連線。它借用一個公網 IP 把出站封包送出去，再把回應導回原請求者。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>NAT Gateway 在網路地基裡的角色是 private subnet 的出站閘道。它解決的問題是：private subnet 的設計意圖是「外部連不進來」，但服務仍需要主動對外。沒有 NAT，private subnet 的資源完全無法對外通訊 — 連 &lt;code>apt update&lt;/code> 或 &lt;code>pip install&lt;/code> 都做不到。&lt;/p>
&lt;p>NAT Gateway 是綁定單一可用區的資源，活在某個 public subnet 裡。這帶來一個架構取捨：共享一個 NAT（成本低、出站方向有單點）還是每個可用區各放一個（成本高、出站與 subnet 冗餘對齊）。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>以下狀況指向 NAT 相關問題：&lt;/p>
&lt;ul>
&lt;li>Private subnet 的服務拉不到外部套件或第三方 API 全部逾時 — 先查 route table 有沒有指向健康的 NAT&lt;/li>
&lt;li>只有某一個可用區的節點受影響 — 該區的 NAT 或其所在 subnet 可能故障&lt;/li>
&lt;li>雲帳單裡 NAT Gateway 的流量費用異常高 — 大量走 NAT 的流量（S3 備份、跨區同步）可用 VPC Endpoint 繞過&lt;/li>
&lt;/ul>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>使用 NAT Gateway 時要決定：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>數量&lt;/strong>：每個可用區一個（可用性優先）還是全 VPC 共享一個（成本優先）。每個 NAT 固定月費約 $32 加流量費 $0.045/GB&lt;/li>
&lt;li>&lt;strong>高流量路徑&lt;/strong>：對 AWS 自家服務的流量（S3、DynamoDB）改用 Gateway Endpoint 直連，繞過 NAT 省流量費&lt;/li>
&lt;li>&lt;strong>route table 關聯&lt;/strong>：每個 private subnet 的 route table 要明確指向哪個 NAT&lt;/li>
&lt;/ul>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">Subnet&lt;/a> — NAT 放在 public subnet、服務放在 private subnet&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC&lt;/a> — NAT 屬於 VPC 內部的出站路徑設施&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>NAT Gateway（Network Address Translation Gateway）的核心職責是讓 private subnet 的資源能主動發起對外連線（拉套件、呼叫第三方 API、下載 OS 更新），同時不開放任何外部主動發起的入站連線。它借用一個公網 IP 把出站封包送出去，再把回應導回原請求者。</p>
<h2 id="概念位置">概念位置</h2>
<p>NAT Gateway 在網路地基裡的角色是 private subnet 的出站閘道。它解決的問題是：private subnet 的設計意圖是「外部連不進來」，但服務仍需要主動對外。沒有 NAT，private subnet 的資源完全無法對外通訊 — 連 <code>apt update</code> 或 <code>pip install</code> 都做不到。</p>
<p>NAT Gateway 是綁定單一可用區的資源，活在某個 public subnet 裡。這帶來一個架構取捨：共享一個 NAT（成本低、出站方向有單點）還是每個可用區各放一個（成本高、出站與 subnet 冗餘對齊）。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>以下狀況指向 NAT 相關問題：</p>
<ul>
<li>Private subnet 的服務拉不到外部套件或第三方 API 全部逾時 — 先查 route table 有沒有指向健康的 NAT</li>
<li>只有某一個可用區的節點受影響 — 該區的 NAT 或其所在 subnet 可能故障</li>
<li>雲帳單裡 NAT Gateway 的流量費用異常高 — 大量走 NAT 的流量（S3 備份、跨區同步）可用 VPC Endpoint 繞過</li>
</ul>
<h2 id="設計責任">設計責任</h2>
<p>使用 NAT Gateway 時要決定：</p>
<ul>
<li><strong>數量</strong>：每個可用區一個（可用性優先）還是全 VPC 共享一個（成本優先）。每個 NAT 固定月費約 $32 加流量費 $0.045/GB</li>
<li><strong>高流量路徑</strong>：對 AWS 自家服務的流量（S3、DynamoDB）改用 Gateway Endpoint 直連，繞過 NAT 省流量費</li>
<li><strong>route table 關聯</strong>：每個 private subnet 的 route table 要明確指向哪個 NAT</li>
</ul>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">Subnet</a> — NAT 放在 public subnet、服務放在 private subnet</li>
<li><a href="/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC</a> — NAT 屬於 VPC 內部的出站路徑設施</li>
</ul>
]]></content:encoded></item><item><title>CIDR（Classless Inter-Domain Routing）</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/cidr/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/cidr/</guid><description>&lt;p>CIDR（Classless Inter-Domain Routing）用前綴長度表示一段 IP 地址範圍。&lt;code>10.0.0.0/16&lt;/code> 表示前 16 bit 是網路位址、後 16 bit 是主機位址，提供約六萬五千個可用位址。前綴越短、範圍越大：&lt;code>/16&lt;/code> 比 &lt;code>/24&lt;/code>（約 256 個位址）大 256 倍。&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC&lt;/a> 和 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">subnet&lt;/a> 的地址空間都用 CIDR 表示。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>CIDR 是 VPC 規劃的起點決策。建立 VPC 時指定的 CIDR 區塊決定了這個 VPC 能容納多少 subnet 和多少資源。這個決策在建立後難以修改——事後擴張意味著追加 secondary CIDR，而追加的網段在 routing 與服務相容性上有限制。&lt;/p>
&lt;p>在 infra 系列中，CIDR 規劃出現在&lt;a href="https://tarrragon.github.io/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">模組三：網路地基&lt;/a>的 VPC 段落。Terraform 的 &lt;code>cidrsubnet&lt;/code> 函式可以從 VPC 的 CIDR 自動切出 subnet 的子網段，避免手動計算。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>CIDR 規劃出問題的訊號有兩類。第一類是地址耗盡：subnet 切不出新的子網段、或 subnet 內的 IP 分配用完，新資源無法取得位址。第二類是網段衝突：需要透過 VPC peering、Transit Gateway 或 VPN 互連兩個 VPC 時，發現兩端的 CIDR 重疊，路由無法解析，peering 建立失敗。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>規劃 CIDR 時要決定：&lt;/p>
&lt;ul>
&lt;li>大小：單一環境用 &lt;code>/16&lt;/code> 通常足夠寬裕，切成 &lt;code>/20&lt;/code> 的 subnet 可分配 16 個子網段&lt;/li>
&lt;li>不重疊：多個環境（dev &lt;code>10.0.0.0/16&lt;/code>、staging &lt;code>10.1.0.0/16&lt;/code>、prod &lt;code>10.2.0.0/16&lt;/code>）用連續但不重疊的區段，為日後互連預留空間&lt;/li>
&lt;li>與地端的協調：如果未來可能接 VPN 回地端機房，CIDR 要避開地端已使用的私有網段&lt;/li>
&lt;/ul>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC&lt;/a> — 用 CIDR 區塊定義的邏輯隔離網段&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">Subnet&lt;/a> — 從 VPC CIDR 切出的子網段&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>CIDR（Classless Inter-Domain Routing）用前綴長度表示一段 IP 地址範圍。<code>10.0.0.0/16</code> 表示前 16 bit 是網路位址、後 16 bit 是主機位址，提供約六萬五千個可用位址。前綴越短、範圍越大：<code>/16</code> 比 <code>/24</code>（約 256 個位址）大 256 倍。<a href="/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC</a> 和 <a href="/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">subnet</a> 的地址空間都用 CIDR 表示。</p>
<h2 id="概念位置">概念位置</h2>
<p>CIDR 是 VPC 規劃的起點決策。建立 VPC 時指定的 CIDR 區塊決定了這個 VPC 能容納多少 subnet 和多少資源。這個決策在建立後難以修改——事後擴張意味著追加 secondary CIDR，而追加的網段在 routing 與服務相容性上有限制。</p>
<p>在 infra 系列中，CIDR 規劃出現在<a href="/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">模組三：網路地基</a>的 VPC 段落。Terraform 的 <code>cidrsubnet</code> 函式可以從 VPC 的 CIDR 自動切出 subnet 的子網段，避免手動計算。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>CIDR 規劃出問題的訊號有兩類。第一類是地址耗盡：subnet 切不出新的子網段、或 subnet 內的 IP 分配用完，新資源無法取得位址。第二類是網段衝突：需要透過 VPC peering、Transit Gateway 或 VPN 互連兩個 VPC 時，發現兩端的 CIDR 重疊，路由無法解析，peering 建立失敗。</p>
<h2 id="設計責任">設計責任</h2>
<p>規劃 CIDR 時要決定：</p>
<ul>
<li>大小：單一環境用 <code>/16</code> 通常足夠寬裕，切成 <code>/20</code> 的 subnet 可分配 16 個子網段</li>
<li>不重疊：多個環境（dev <code>10.0.0.0/16</code>、staging <code>10.1.0.0/16</code>、prod <code>10.2.0.0/16</code>）用連續但不重疊的區段，為日後互連預留空間</li>
<li>與地端的協調：如果未來可能接 VPN 回地端機房，CIDR 要避開地端已使用的私有網段</li>
</ul>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/vpc/" data-link-title="VPC（Virtual Private Cloud）" data-link-desc="雲端帳號內的一塊邏輯隔離私有網段，是所有網路切分的起點與容器">VPC</a> — 用 CIDR 區塊定義的邏輯隔離網段</li>
<li><a href="/blog/infra/knowledge-cards/subnet/" data-link-title="Subnet（子網路）" data-link-desc="VPC 內按可用區與暴露程度切出的子網段，決定資源有沒有一條通往網際網路的路徑">Subnet</a> — 從 VPC CIDR 切出的子網段</li>
</ul>
]]></content:encoded></item><item><title>Reverse Proxy</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/reverse-proxy/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/reverse-proxy/</guid><description>&lt;p>Reverse proxy 是一個坐在後端服務前面、代替它接收外部請求的中介層。外部 client 連的是 reverse proxy 的位址，reverse proxy 根據規則把請求轉發到實際處理的內部服務，再把回應傳回給 client。Client 不知道（也不需要知道）後面有幾台服務、跑在哪裡。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>nginx 和 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/alb/" data-link-title="ALB" data-link-desc="Application Load Balancer — 流量進入系統的第一站，負責 listener 路由、健康檢查與 TLS 終結">ALB&lt;/a> 都扮演 reverse proxy 角色。差別在層級：nginx 通常部署在應用層（跟應用伺服器同一台或同一個 VPC 內），ALB 是雲端平台提供的受管服務。兩者的核心功能相同——接收外部流量、轉發到後端、回傳結果。&lt;/p>
&lt;p>跟 forward proxy 的方向相反：forward proxy 代替 client 發送請求（client 在內網、proxy 幫它出去）；reverse proxy 代替 server 接收請求（server 在內網、proxy 幫它面對外部）。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>接手時如果 server 上跑著 nginx 但應用程式用的是 PHP-FPM 或 Node.js，nginx 多半扮演 reverse proxy——它接 HTTP/HTTPS 請求、轉發給後端的 application server。設定檔裡的 &lt;code>proxy_pass&lt;/code>（nginx）或 &lt;code>ProxyPass&lt;/code>（Apache）就是 reverse proxy 的轉發規則。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>reverse proxy 常承擔的功能：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>功能&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>TLS 終結&lt;/td>
 &lt;td>HTTPS 的加解密在 proxy 層處理，後端服務只收 HTTP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>負載平衡&lt;/td>
 &lt;td>把請求分配到多台後端（round-robin、least-connection）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>路由分流&lt;/td>
 &lt;td>依 URL path 導到不同後端服務（/api → backend、/ → frontend）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>靜態檔案快取&lt;/td>
 &lt;td>圖片、CSS、JS 由 proxy 直接回應、不轉發到後端&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>安全過濾&lt;/td>
 &lt;td>擋掉異常請求、限制請求速率、加安全標頭&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/alb/" data-link-title="ALB" data-link-desc="Application Load Balancer — 流量進入系統的第一站，負責 listener 路由、健康檢查與 TLS 終結">ALB&lt;/a>：雲端的受管 reverse proxy + 負載平衡器&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/nginx/" data-link-title="nginx" data-link-desc="高效能 Web Server 與 Reverse Proxy，以集中設定檔取代 Apache 的 .htaccess 分散設定">nginx&lt;/a>：最常見的 reverse proxy 軟體&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Reverse proxy 是一個坐在後端服務前面、代替它接收外部請求的中介層。外部 client 連的是 reverse proxy 的位址，reverse proxy 根據規則把請求轉發到實際處理的內部服務，再把回應傳回給 client。Client 不知道（也不需要知道）後面有幾台服務、跑在哪裡。</p>
<h2 id="概念位置">概念位置</h2>
<p>nginx 和 <a href="/blog/infra/knowledge-cards/alb/" data-link-title="ALB" data-link-desc="Application Load Balancer — 流量進入系統的第一站，負責 listener 路由、健康檢查與 TLS 終結">ALB</a> 都扮演 reverse proxy 角色。差別在層級：nginx 通常部署在應用層（跟應用伺服器同一台或同一個 VPC 內），ALB 是雲端平台提供的受管服務。兩者的核心功能相同——接收外部流量、轉發到後端、回傳結果。</p>
<p>跟 forward proxy 的方向相反：forward proxy 代替 client 發送請求（client 在內網、proxy 幫它出去）；reverse proxy 代替 server 接收請求（server 在內網、proxy 幫它面對外部）。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>接手時如果 server 上跑著 nginx 但應用程式用的是 PHP-FPM 或 Node.js，nginx 多半扮演 reverse proxy——它接 HTTP/HTTPS 請求、轉發給後端的 application server。設定檔裡的 <code>proxy_pass</code>（nginx）或 <code>ProxyPass</code>（Apache）就是 reverse proxy 的轉發規則。</p>
<h2 id="設計責任">設計責任</h2>
<p>reverse proxy 常承擔的功能：</p>
<table>
  <thead>
      <tr>
          <th>功能</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>TLS 終結</td>
          <td>HTTPS 的加解密在 proxy 層處理，後端服務只收 HTTP</td>
      </tr>
      <tr>
          <td>負載平衡</td>
          <td>把請求分配到多台後端（round-robin、least-connection）</td>
      </tr>
      <tr>
          <td>路由分流</td>
          <td>依 URL path 導到不同後端服務（/api → backend、/ → frontend）</td>
      </tr>
      <tr>
          <td>靜態檔案快取</td>
          <td>圖片、CSS、JS 由 proxy 直接回應、不轉發到後端</td>
      </tr>
      <tr>
          <td>安全過濾</td>
          <td>擋掉異常請求、限制請求速率、加安全標頭</td>
      </tr>
  </tbody>
</table>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/alb/" data-link-title="ALB" data-link-desc="Application Load Balancer — 流量進入系統的第一站，負責 listener 路由、健康檢查與 TLS 終結">ALB</a>：雲端的受管 reverse proxy + 負載平衡器</li>
<li><a href="/blog/infra/knowledge-cards/nginx/" data-link-title="nginx" data-link-desc="高效能 Web Server 與 Reverse Proxy，以集中設定檔取代 Apache 的 .htaccess 分散設定">nginx</a>：最常見的 reverse proxy 軟體</li>
</ul>
]]></content:encoded></item></channel></rss>