---
schemaVersion: 1
slug: levelup-20260213-03-one-domain-multi-services
ticketId: Lv-03
lang: zh-tw
title: 一個 Domain，多個 API Service：從地基蓋到屋頂
summary: 你剛進一間公司，要把 3 個 FastAPI service 塞到同一個 domain。從 DNS 地基開始，一路往上蓋 Ingress、Istio、FastAPI、Swagger，每層一張手繪風圖，搭配 PTT 說故事風格帶你搞懂整棟架構。
originalDate: 2026-02-13
translatedDate: 2026-02-13
source: Level-Up 系列
sourceUrl: https://gu-log.vercel.app/posts/levelup-20260213-03-one-domain-multi-services
author: null
authorshipNote: null
canonicalUrl: https://gu-log.vercel.app/posts/levelup-20260213-03-one-domain-multi-services
status: published
replacementTicketId: null
replacementUrl: null
---

# 一個 Domain，多個 API Service：從地基蓋到屋頂

> **來源:** [Level-Up 系列](https://gu-log.vercel.app/posts/levelup-20260213-03-one-domain-multi-services)

> **Mogu 內心戲：**
>
> AI 時代的學習哲學：你不需要背 YAML 怎麼縮排、nginx.conf 的每一行怎麼寫。那些東西 AI 30 秒就能生出來，你背它幹嘛？
>
> 人類真正該掌握的是兩件事：**概念**（每一層是什麼、為什麼存在）和 **trade-off**（什麼時候該用什麼、用錯了會怎樣）。
>
> 所以這篇文章不會教你一行行設定 Ingress YAML 或 Istio VirtualService。我只教你「每層是什麼」和「什麼時候該用」。Implementation details？丟給 AI 就好。你是 Tech Lead，不是 YAML 工程師。

歡迎來到 Level-Up 系列第三篇。

**情境**：你剛進一間新公司，第一天 Tech Lead 就丟給你一個任務：

> 「我們有三個 FastAPI service —— `user-service`、`order-service`、`notification-service`。現在它們各自跑在不同 port，我需要你把它們全部放到 `api.mycompany.com` 這個 domain 底下。」

你心想：三個 service、一個 domain？聽起來簡單吧？

然後你開始 Google，發現牽涉到 DNS、Ingress、Istio、URL prefix、Swagger… 每個關鍵字都是一個兔子洞。

別慌。這篇文章就是那棟大樓的藍圖。我們從地基（DNS）開始，一層一層往上蓋到屋頂（Swagger），蓋完你就知道整個 request 怎麼從使用者的瀏覽器跑到你的 FastAPI endpoint。

Let’s build 🏗️

---

## 🏗️ Floor 0：起點 — 三個 service，三個 port

**Level 0 / 7 — 一個 Domain，多個 Service — 0% 完成**

在一切開始之前，先看看你手上有什麼。

你有三個 FastAPI service，各自跑在不同的 port：

- `user-service` → `localhost:8001`
- `order-service` → `localhost:8002`
- `notification-service` → `localhost:8003`

開發環境一切正常。三個 terminal 分別跑 `uvicorn`，想測哪個就打哪個 port。爽。

但是，你總不能叫使用者記住三個 port 吧？

> 「欸，你要查訂單的話打 `api.mycompany.com:8002`，要收通知的話打 `api.mycompany.com:8003`，記得不要打錯喔！」

這聽起來就像一間餐廳有三個門，吃牛排走左邊那個門、吃義大利麵走中間、喝飲料走右邊。客人走錯門就吃不到東西。

正常的做法是：**一個大門進去，裡面再分流**。

```mermaid
%%{init: {"look": "handDrawn"}}%%
graph TD
U["👤 使用者"] -->|":8001"| S1["user-service"]
U -->|":8002"| S2["order-service"]
U -->|":8003"| S3["notification-service"]
style U fill:#ffeaa7,stroke:#fdcb6e
style S1 fill:#dfe6e9,stroke:#b2bec3
style S2 fill:#dfe6e9,stroke:#b2bec3
style S3 fill:#dfe6e9,stroke:#b2bec3
```

_現狀：使用者要記住三個 port，很痛苦_

所以我們的目標是：

```plaintext
api.mycompany.com/users/...          → user-service
api.mycompany.com/orders/...         → order-service
api.mycompany.com/notifications/...  → notification-service
```

**一個 domain，用 URL path 分流到不同 service。**

要達成這件事，我們需要蓋好幾層樓。從最底層的 DNS 開始。

**Quiz:** 為什麼不能直接讓使用者用不同 port 存取不同 service？

- A. 因為 port 數量有限制，最多只能開 3 個
- B. 因為使用者不該知道 service 的內部架構，而且多 port 難管理、防火牆也不好設
- C. 因為不同 port 的速度不同
- D. 因為瀏覽器只支援 port 80 和 443

**Answer:** B — 暴露多個 port 意味著使用者要記住你的內部架構、防火牆要開多個口、SSL 憑證要配多份。正確做法是用一個入口統一接收，內部再分流。這也是 encapsulation（封裝）的精神。

---

## 🏗️ Floor 1：DNS — 門牌號碼

**Level 1 / 7 — 一個 Domain，多個 Service — 14% 完成**

蓋房子第一步：你的地址要能被找到。

你跟公司申請了 `api.mycompany.com` 這個 domain。但 domain name 只是一串人類看得懂的文字，電腦不認識。電腦認識的是 **IP 位址**，像 `34.120.56.78`。

**DNS（Domain Name System）** 就是負責把 domain name 翻譯成 IP 位址的系統。

> **📮 門牌號碼的比喻**
>
> DNS 就像郵局的地址簿。
>
> 你說「寄到台北市信義區松壽路 1 號」，郵差不是真的看地名找路，他是查地址簿找到經緯度（IP），然後直接導航過去。
>
> `api.mycompany.com` = 地址（人類看的） `34.120.56.78` = 經緯度（電腦看的） DNS = 地址簿

當使用者在瀏覽器打 `api.mycompany.com/users/123`，第一件事不是連到你的 server，而是去問 DNS：

> 「`api.mycompany.com` 的 IP 是什麼？」

DNS 回答：`34.120.56.78`。

然後瀏覽器才真正連到 `34.120.56.78` 這台 server。

```mermaid
%%{init: {"look": "handDrawn"}}%%
sequenceDiagram
participant B as 👤 瀏覽器
participant D as 📖 DNS Server
participant S as 🖥️ Server<br/>34.120.56.78
B->>D: api.mycompany.com 的 IP 是？
D-->>B: 34.120.56.78
B->>S: GET /users/123
S-->>B: 200 OK
```

_DNS 把 domain name 翻譯成 IP，瀏覽器才知道要連去哪_

### 你需要做什麼？

去你的 DNS 管理介面（Cloudflare、Route 53、GoDaddy 等），新增一筆 **A Record**：

```plaintext
api.mycompany.com  →  A  →  34.120.56.78
```

就這樣。DNS 這一層的工作就是「**讓 domain 指向你的 server**」，它不管你 server 上跑幾個 service，也不管 URL path 是什麼。它只負責把人導到正確的大樓門口。

> **Mogu 偷偷講：**
>
> DNS 還有一種叫 CNAME 的記錄，是「指向另一個 domain」而不是直接指向 IP。常見於用 CDN 或 cloud provider 的時候。但概念一樣 —— 最終都是為了讓 domain name 能被解析成一個 IP。

**Quiz:** DNS 在整個架構中負責什麼？

- A. 根據 URL path 把 request 分配到不同 service
- B. 把 domain name 翻譯成 IP 位址，讓瀏覽器知道要連去哪台 server
- C. 加密使用者和 server 之間的通訊
- D. 驗證使用者的身份

**Answer:** B — DNS 的唯一工作是把 domain name 解析成 IP。它不知道你的 URL path 是什麼，也不知道你有幾個 service。分流是上層的工作（Ingress / reverse proxy）。

---

## 🏗️ Floor 2：Ingress — 大樓的大門警衛

**Level 2 / 7 — 一個 Domain，多個 Service — 29% 完成**

OK，DNS 把使用者帶到你的大樓門口了（IP 位址）。

但大樓門口不能只有一扇門直通一個房間。你有三個 service，使用者打不同的 URL path 應該進不同的房間。

這就是 **Ingress** 的工作。

> **🏢 大樓警衛的比喻**
>
> Ingress 就是大樓一樓的警衛。
>
> 每個人進來，警衛看一下你要去哪：
>
> - 「我要找 `/users`」→ 警衛：「7 樓左轉，user-service」
> - 「我要找 `/orders`」→ 警衛：「12 樓右轉，order-service」
> - 「我要找 `/notifications`」→ 警衛：「3 樓，notification-service」
>
> 他不關心你進去之後要做什麼，他只負責看你的**目的地（URL path）**然後**指路（routing）**。

在 Kubernetes 的世界裡，**Ingress** 是一個 API 物件，定義了「哪些外部 request 要被導到哪個 internal service」。

它的邏輯大概長這樣（概念版，不是真的 YAML）：

```plaintext
如果 path 開頭是 /users      → 轉給 user-service
如果 path 開頭是 /orders     → 轉給 order-service
如果 path 開頭是 /notifications → 轉給 notification-service
```

```mermaid
%%{init: {"look": "handDrawn"}}%%
graph TD
U["👤 使用者<br/>api.mycompany.com"] --> I["🚧 Ingress<br/>大門警衛"]
I -->|"/users/*"| S1["user-service"]
I -->|"/orders/*"| S2["order-service"]
I -->|"/notifications/*"| S3["notification-service"]
style U fill:#ffeaa7,stroke:#fdcb6e
style I fill:#74b9ff,stroke:#0984e3
style S1 fill:#dfe6e9,stroke:#b2bec3
style S2 fill:#dfe6e9,stroke:#b2bec3
style S3 fill:#dfe6e9,stroke:#b2bec3
```

_Ingress 根據 URL path prefix 把 request 分配到不同 service_

### Ingress 不是自己跑的

一個很重要的概念：**Ingress 本身只是一個「規則定義」**，它需要一個 **Ingress Controller** 來真正執行這些規則。

常見的 Ingress Controller：

- **NGINX Ingress Controller** — 最老牌、最多人用
- **Traefik** — 自動化程度高、設定比較直覺
- **HAProxy** — 效能取向

Ingress Controller 就是那個「真的站在門口指路的警衛」，Ingress 是「警衛手上的名單」。

> **Mogu 嘀咕一下：**
>
> 很多人第一次接觸 Kubernetes 的時候都搞混 Ingress 和 Ingress Controller。Ingress 是你寫的規則（YAML），Ingress Controller 是執行規則的那個程式。你不能只寫規則不裝 Controller，就像你不能只寫員工手冊但不請警衛一樣。

### Ingress 也處理 TLS

Ingress 通常也負責 **TLS termination** —— 就是 HTTPS 的那個 S。

使用者用 `https://api.mycompany.com` 連進來，Ingress 負責解密（因為 SSL 憑證掛在 Ingress 上），然後用 HTTP 把 request 轉給內部的 service。

所以你的 FastAPI service 不需要自己處理 SSL，交給 Ingress 就好。

**Quiz:** 以下哪個描述最正確？

- A. Ingress 是一個執行路由的程式
- B. Ingress 是路由規則的定義，需要 Ingress Controller 來執行
- C. Ingress 只能處理 HTTP，不能處理 HTTPS
- D. 每個 service 都需要自己的 Ingress

**Answer:** B — Ingress 是 Kubernetes 的 API 物件，定義了路由規則。但它需要 Ingress Controller（如 NGINX）來實際執行這些規則。Ingress 也可以處理 TLS（HTTPS），通常 SSL 憑證就掛在 Ingress 上。

---

## 🏗️ Floor 3：Istio — 大樓的智慧管理系統

**Level 3 / 7 — 一個 Domain，多個 Service — 43% 完成**

Ingress 讓你可以根據 URL path 分流。很好，夠用了嗎？

如果你的需求只是「path A 去 service A、path B 去 service B」，那 Ingress 就夠了。收工回家。

但如果你的需求是這些呢？

- 新版 API 只先開放 10% 的 traffic（**canary deployment**）
- service A 呼叫 service B 的時候自動重試 3 次（**retry policy**）
- service 之間的通訊全部加密（**mTLS**）
- 我想看每個 request 的延遲、錯誤率（**observability**）
- 某個 service 掛了，自動把 traffic 導走（**circuit breaking**）

這些事情 Ingress 做不到。你需要更強大的東西：**Istio**。

> **🏢 大樓智慧管理系統的比喻**
>
> 如果 Ingress 是大門警衛，那 Istio 就是整棟大樓的**智慧管理系統**。
>
> 警衛只管「誰從門口進來、去哪一層」。但智慧管理系統管的是整棟大樓內部的一切：
>
> - 電梯流量控制（traffic management）
> - 每層樓的進出紀錄（observability）
> - 樓層之間的門禁（mTLS）
> - 某層樓失火自動封鎖（circuit breaking）
>
> Ingress 只管外面進來的 traffic（**North-South traffic**），Istio 連大樓內部 service 之間的 traffic 都管（**East-West traffic**）。

### Istio 的核心概念：Sidecar

Istio 最特別的設計是 **sidecar proxy**。

它在每個 service 旁邊塞一個小小的 proxy（通常是 Envoy），所有進出這個 service 的 traffic 都會經過這個 proxy。

```plaintext
不是這樣：  User → Service A → Service B

而是這樣：  User → [Proxy A] → Service A → [Proxy A] → [Proxy B] → Service B
```

這些 proxy 會自動做加密、重試、記錄延遲… 你的 code 完全不用改。

```mermaid
%%{init: {"look": "handDrawn"}}%%
graph TD
U["👤 使用者"] --> GW["🌐 Istio Gateway"]
GW --> VS["📋 VirtualService<br/>路由規則"]
VS -->|"/users/*"| PA["🔲 Envoy Proxy"]
VS -->|"/orders/*"| PB["🔲 Envoy Proxy"]
VS -->|"/notifications/*"| PC["🔲 Envoy Proxy"]
PA --> S1["user-service"]
PB --> S2["order-service"]
PC --> S3["notification-service"]
style U fill:#ffeaa7,stroke:#fdcb6e
style GW fill:#a29bfe,stroke:#6c5ce7
style VS fill:#a29bfe,stroke:#6c5ce7
style PA fill:#fd79a8,stroke:#e84393
style PB fill:#fd79a8,stroke:#e84393
style PC fill:#fd79a8,stroke:#e84393
style S1 fill:#dfe6e9,stroke:#b2bec3
style S2 fill:#dfe6e9,stroke:#b2bec3
style S3 fill:#dfe6e9,stroke:#b2bec3
```

_Istio 在每個 service 旁邊放一個 Envoy proxy，管理所有 traffic_

### Istio 取代 Ingress？

嚴格來說，如果你用了 Istio，**Istio Gateway + VirtualService** 可以取代 Kubernetes Ingress 的功能。它做的事一樣（根據 host 和 path 分流），但功能更強大。

不過「可以取代」不代表「一定要取代」。這個 trade-off 我們留到 Boss Floor 再講。

> **Mogu 碎碎念：**
>
> Istio 的學習曲線是出了名的陡。很多團隊裝了 Istio 之後才發現，其實他們 80% 的需求用 Ingress 就搞定了。剩下 20% 的 canary deployment，用 Argo Rollouts 也能做。所以裝 Istio 之前請三思。

**Quiz:** Istio sidecar proxy 的運作方式是？

- A. 在每個 service 的程式碼中插入 proxy 邏輯
- B. 在每個 service 旁邊部署一個獨立的 proxy，攔截所有進出的 traffic
- C. 在 Kubernetes cluster 入口放一個集中式 proxy
- D. 直接修改 Kubernetes 的網路層

**Answer:** B — Istio 的 sidecar 模式是在每個 Pod 旁邊注入一個 Envoy proxy container。所有進出該 service 的 traffic 都會被這個 proxy 攔截，由它來執行加密、重試、觀測等功能。你的 application code 完全不用改。

---

## 🏗️ Floor 4：FastAPI × Swagger × root\_path — 每個 service 的「室內裝潢」

**Level 4 / 7 — 一個 Domain，多個 Service — 57% 完成**

好，外部的大樓結構蓋好了（DNS → Ingress/Istio）。從這層開始，我們要處理每個 service **內部**的事情。

這一層東西比較多，所以我們拆成四個小節慢慢來。

---

### 🔹 Floor 4-0：FastAPI 是什麼？— 每個 service 的骨架

在蓋「室內裝潢」之前，先搞清楚你的 service 是用什麼蓋的。

**FastAPI** 是一個 Python web framework，專門用來寫 API。你的三個 service —— `user-service`、`order-service`、`notification-service` —— 每一個都是用 FastAPI 寫的。

一個最簡單的 FastAPI service 長這樣：

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {"id": user_id, "name": "Alice"}
```

就這樣。五行 code，你就有一個 API endpoint：`GET /users/123` 會回傳 `{"id": 123, "name": "Alice"}`。

但 FastAPI 最殺手級的功能不是「寫 API 很快」—— 而是它**自動幫你生成一份互動式 API 文件**。

> **📋 菜單的比喻**
>
> 想像你開了一間餐廳。FastAPI 不只幫你把廚房建好（API 邏輯），還**自動印了一份菜單**放在門口（API 文件）。
>
> 這份菜單叫做 **Swagger UI**，客人可以：
>
> - 看到所有菜色（所有 API endpoint）
> - 看到每道菜需要什麼食材（每個 endpoint 需要什麼參數）
> - **直接在菜單上試吃**（點 “Try it” 按鈕直接發 request）
>
> 你不用自己寫菜單，FastAPI 看你的 code 自動生成。

啟動 service 之後，打開 `http://localhost:8001/docs`，你會看到這個畫面：

```mermaid
%%{init: {"look": "handDrawn"}}%%
graph LR
C["🐍 你的 Python code<br/>@app.get('/users/{user_id}')"] -->|"FastAPI 自動生成"| S["📄 Swagger UI<br/>http://localhost:8001/docs"]
S --> T["🖱️ Try it 按鈕<br/>直接測試 API"]
style C fill:#55efc4,stroke:#00b894
style S fill:#fdcb6e,stroke:#f39c12
style T fill:#fd79a8,stroke:#e84393
```

_FastAPI 看你的 code，自動生成互動式 Swagger UI 文件_

**Swagger UI** 是開發者的好朋友 —— 前端工程師看它知道要怎麼串 API、你自己開發時可以直接在上面測試。

到目前為止一切美好。但當你的 service 被放到 Ingress 後面（也就是我們 Floor 2 和 3 建的那個大樓結構），問題就來了。

---

### 🔹 Floor 4-1：路徑錯位 — Ingress 轉進來的 path，FastAPI 認不認得？

你的 `user-service` 在本地跑的時候，endpoint 是：

```plaintext
http://localhost:8001/users/123    ✅ 正常運作
```

現在掛到 Ingress 後面，使用者打的是：

```plaintext
https://api.mycompany.com/users/123
```

Ingress 看到 `/users` 開頭，知道要轉給 `user-service`。但問題來了：

> **Ingress 轉給 FastAPI 的 path 到底是 `/users/123` 還是 `/123`？**

這取決於 Ingress 有沒有做 **path rewrite**（路徑改寫）。

#### 情況一：不改寫 path

Ingress 把 `/users/123` **原封不動**地轉給 `user-service`。

FastAPI 收到的是 `/users/123`，那你的 code 要這樣寫：

```python
@app.get("/users/{user_id}")  # ← 帶 /users prefix
def get_user(user_id: int):
    return {"id": user_id}
```

#### 情況二：改寫 path（strip prefix）

Ingress 把 `/users/123` 的 `/users` prefix 剝掉，只把 `/123` 轉給 `user-service`。

FastAPI 收到的是 `/123`，code 可以寫得比較乾淨：

```python
@app.get("/{user_id}")  # ← 不需要 /users prefix
def get_user(user_id: int):
    return {"id": user_id}
```

> **🏠 房間門牌的比喻**
>
> 就像你住在「信義路 100 號 **5 樓之 3**」。
>
> - **不改寫**：郵差把整個地址「信義路 100 號 5 樓之 3」原封不動交給你。你的 code 要認得完整地址。
> - **改寫（strip prefix）**：大樓管理員收到信之後，把「5 樓」拿掉，只寫「之 3」轉給你。你只需要認得「之 3」就好。
>
> 「改寫」讓每個 service 更獨立 —— 它不需要知道自己在大樓的哪一層。

```mermaid
%%{init: {"look": "handDrawn"}}%%
graph TD
R["👤 使用者<br/>GET /users/123"] --> I["🚧 Ingress"]
I -->|"不改寫<br/>FastAPI 收到 /users/123"| F1["🟢 FastAPI<br/>@app.get('/users/{user_id}')"]
I -->|"改寫 strip prefix<br/>FastAPI 收到 /123"| F2["🟢 FastAPI<br/>@app.get('/{user_id}')"]
style R fill:#ffeaa7,stroke:#fdcb6e
style I fill:#74b9ff,stroke:#0984e3
style F1 fill:#55efc4,stroke:#00b894
style F2 fill:#55efc4,stroke:#00b894
```

_Ingress 有沒有做 path rewrite，決定了 FastAPI endpoint 怎麼寫_

**大多數團隊的做法是「改寫」**（strip prefix），因為這樣每個 service 不用知道自己掛在哪個 path 底下，更好維護。

到這裡 API routing 已經通了。但故事還沒完 —— 你打開 Swagger UI 試了一下，發現怪怪的…

**Quiz:** Ingress 做 path rewrite（strip prefix）的好處是？

- A. 讓 API 跑更快
- B. 讓每個 service 不需要知道自己的 URL prefix，更獨立、更好維護
- C. 自動處理 SSL 加密
- D. 不用寫任何 Python code

**Answer:** B — Strip prefix 讓 service 只需要關心自己的 endpoint（如 /{user\_id}），不需要知道自己被掛在 /users 底下。如果以後要把 prefix 從 /users 改成 /accounts，只需要改 Ingress 設定，service 的 code 完全不用動。

---

### 🔹 Floor 4-2：root\_path — 「嘿 FastAPI，你被掛在 /users 底下」

你選了「Ingress 做 path rewrite」，API routing 一切正常。

然後你打開 `https://api.mycompany.com/users/docs` 看 Swagger UI，點了 `GET /{user_id}` 的 **“Try it”** 按鈕，填了 `user_id = 123`，按下 **Execute**…

```plaintext
❌ 404 Not Found
```

蛤？API 明明能用，為什麼 Swagger 的 “Try it” 壞了？

看一下 Swagger UI 實際送出的 request：

```plaintext
GET https://api.mycompany.com/123    ← 😱 少了 /users！
```

正確應該是：

```plaintext
GET https://api.mycompany.com/users/123    ← ✅ 這才對
```

**問題在哪？**

FastAPI 生成 Swagger UI 的時候，它不知道自己被 Ingress 掛在 `/users` 底下。它以為自己就是住在根目錄 `/`。所以 Swagger 的 “Try it” 會直接打 `/123`，而不是 `/users/123`。

> **📮 回信地址的比喻**
>
> 你搬到大樓 5 樓之後，寫信的回信地址還是寫「之 3」（你的房間號），忘了加「信義路 100 號 5 樓」。
>
> 收到信的人按回信地址寄回來，信就寄丟了 —— 因為只寫「之 3」郵差不知道是哪棟大樓哪一層的「之 3」。
>
> 你需要告訴自己：「我住在 5 樓，回信地址要加上 5 樓」。
>
> 在 FastAPI 裡，`root_path` 就是做這件事 —— **告訴 FastAPI 你住在哪一層**。

**解法**：設定 `root_path`。

```python
app = FastAPI(root_path="/users")  # ← 告訴 FastAPI：你被掛在 /users 底下

@app.get("/{user_id}")
def get_user(user_id: int):
    return {"id": user_id}
```

加了 `root_path="/users"` 之後：

- **API routing 完全不受影響** — `@app.get("/{user_id}")` 還是只 match `/123`
- **Swagger UI 知道了** — “Try it” 會正確地送 `GET /users/123`

```mermaid
%%{init: {"look": "handDrawn"}}%%
graph TD
subgraph "❌ 沒設 root_path"
  S1["Swagger UI<br/>以為 base = /"] -->|"Try it"| R1["GET /123<br/>404 Not Found 💀"]
end
subgraph "✅ 設了 root_path=/users"
  S2["Swagger UI<br/>知道 base = /users"] -->|"Try it"| R2["GET /users/123<br/>200 OK ✅"]
end
style S1 fill:#fab1a0,stroke:#e17055
style R1 fill:#fab1a0,stroke:#e17055
style S2 fill:#55efc4,stroke:#00b894
style R2 fill:#55efc4,stroke:#00b894
```

_root\_path 讓 Swagger UI 知道真實的 base URL，Try it 才會打對地方_

## 延伸閱讀

- [Lv-10: 一個 URL 的旅程 — 從你按 Enter 到畫面出現，瀏覽器到底在幹嘛](https://gu-log.vercel.app/posts/levelup-20260223-10-url-journey-browser-internals/)
- [MP-23: Deno Sandbox：把 API Secret 藏在看不見的地方](https://gu-log.vercel.app/posts/mp-23-20260204-simonw-deno-sandbox/)
- [MP-187: Gemini API 終於能設花費上限了，CI 跟 agents 比較敢放手玩](https://gu-log.vercel.app/posts/mp-187-20260317-simonw-gemini-api-ci-agents/)

> **Mogu 認真說：**
>
> `root_path` 是 ASGI 標準（不是 FastAPI 獨有的）。它只影響 OpenAPI schema 裡的 `servers` 欄位，不影響 routing。簡單說：**root\_path 是給文件看的，不是給 router 看的。**
>
> 技術上，`root_path` 會讓 FastAPI 生成的 `openapi.json` 裡出現 `"servers": [{"url": "/users"}]`。Swagger UI 讀到這個就知道所有 endpoint 都要加上 `/users` prefix。

---

### 🔹 Floor 4-3：完整拼圖 — 推薦做法 + 常見雷

把前面幾小節串起來，**大多數團隊的推薦做法**是：

1. **Ingress 做 path rewrite**（strip `/users` prefix）
2. **FastAPI 設 `root_path="/users"`**（讓 Swagger 知道真實路徑）
3. **Endpoint 不帶 prefix**（乾淨好維護）

```python
# user-service/main.py
from fastapi import FastAPI

app = FastAPI(root_path="/users")

@app.get("/")          # 外面打 /users/        → Ingress strip → FastAPI 收到 /
@app.get("/{user_id}") # 外面打 /users/123     → Ingress strip → FastAPI 收到 /123
def get_user(user_id: int):
    return {"id": user_id}

# Swagger UI：https://api.mycompany.com/users/docs
# Try it GET /users/123 → ✅ 正確！
```

#### 常見雷：忘記設 root\_path

| 症狀 | 原因 |
| --- | --- |
| API 正常但 Swagger “Try it” 404 | `root_path` 沒設，Swagger 送錯 URL |
| Swagger 頁面打得開但 endpoint 列表是空的 | `openapi.json` 的路徑也被 prefix 影響了 |
| 本地開發沒問題、部署後 Swagger 壞掉 | 本地沒有 Ingress，部署後才有 path prefix 差異 |

> **Mogu 嘀咕一下：**
>
> 這個坑真的非常經典。幾乎每個第一次把 FastAPI 放到 reverse proxy 後面的人都會踩到。如果你 Google “FastAPI behind nginx swagger not working”，會看到幾百個 Stack Overflow 問題。答案永遠是：**設 root\_path**。

**Quiz:** 你的 FastAPI service 被 Ingress 掛在 /users 底下（有 strip prefix）。API 正常但 Swagger 的 Try it 回 404。最可能的原因是？

- A. Ingress 的 TLS 設定有問題
- B. FastAPI 沒有設 root\_path，Swagger 不知道真實的 base URL
- C. DNS 沒有指向正確的 IP
- D. Python 版本太舊

**Answer:** B — 這是經典的 root\_path 問題。API routing 正常代表 Ingress 和 FastAPI 的 path 都對了。但 Swagger UI 不知道 base URL 是 /users，所以 Try it 送出的 request 少了 /users prefix，打到不存在的 endpoint 就 404 了。設 root\_path='/users' 即可修復。

---

## 🏗️ Floor 5：Swagger / OpenAPI — 屋頂上的導覽地圖

**Level 5 / 7 — 一個 Domain，多個 Service — 71% 完成**

大樓蓋好了，使用者可以正確地打到每個 service。但還有一個問題：

> 「欸，你們家 API 有文件嗎？我怎麼知道有哪些 endpoint、要帶什麼參數？」

FastAPI 內建 Swagger UI —— 每個 service 都自動生成一份互動式 API 文件：

- `api.mycompany.com/users/docs` → user-service 的 Swagger
- `api.mycompany.com/orders/docs` → order-service 的 Swagger
- `api.mycompany.com/notifications/docs` → notification-service 的 Swagger

但你有沒有覺得哪裡怪怪的？

**三個 Swagger，三個入口。** 每次要查 API 都要記不同的網址。前端工程師會想打你。

> **🗺️ 導覽地圖的比喻**
>
> 就像一棟百貨公司，每一層都有自己的樓層介紹，但沒有一張**整棟大樓的樓層導覽圖**。
>
> 你想知道「賣鞋子的在哪」，要一層一層走上去看。煩死。
>
> 我們需要的是一張掛在一樓大廳的「**全館樓層指南**」，列出所有樓層的所有商店。

### 統一 Swagger 的做法

核心概念是：每個 FastAPI service 都會在 `/openapi.json` 自動生成一份 **OpenAPI spec**（JSON 格式的 API 描述檔）。

你可以寫一個 **API Gateway service** 或用工具把這些 spec 合併成一份，然後用統一的 Swagger UI 呈現：

```plaintext
api.mycompany.com/docs
  ├── 包含 user-service 的所有 endpoint
  ├── 包含 order-service 的所有 endpoint
  └── 包含 notification-service 的所有 endpoint
```

```mermaid
%%{init: {"look": "handDrawn"}}%%
graph TD
D["📄 統一 Swagger UI<br/>/docs"] --> |"拉取"| O1["/users/openapi.json"]
D --> |"拉取"| O2["/orders/openapi.json"]
D --> |"拉取"| O3["/notifications/openapi.json"]
O1 --> S1["user-service"]
O2 --> S2["order-service"]
O3 --> S3["notification-service"]
style D fill:#fdcb6e,stroke:#f39c12
style O1 fill:#dfe6e9,stroke:#b2bec3
style O2 fill:#dfe6e9,stroke:#b2bec3
style O3 fill:#dfe6e9,stroke:#b2bec3
style S1 fill:#55efc4,stroke:#00b894
style S2 fill:#55efc4,stroke:#00b894
style S3 fill:#55efc4,stroke:#00b894
```

_統一 Swagger UI 從每個 service 拉取 OpenAPI spec，合併呈現_

### 常見做法

1. **各自獨立**：每個 service 有自己的 `/docs`，不合併。適合 service 之間差異大、團隊獨立運作的情況。
2. **手動合併**：寫一個 gateway 去拉各 service 的 `openapi.json` 然後合併。
3. **用工具**：像 [Swagger UI 的 `urls` 選項](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/) 可以在一個頁面切換多個 spec。
4. **API Gateway 整合**：Kong、AWS API Gateway 等自帶統一文件功能。

> **Mogu 碎碎念：**
>
> 說實話，很多團隊根本不合併 Swagger，就讓每個 service 各自有 /docs。因為合併的維護成本不低，而且 spec 格式不一致的時候合併會爆炸。如果你的團隊只有 3-5 個 service，各自獨立就好，不用硬合併。人生苦短。

**Quiz:** 統一 Swagger UI 的核心做法是？

- A. 把所有 service 的程式碼合併成一個
- B. 把每個 service 自動生成的 OpenAPI spec 收集起來，用統一的 UI 呈現
- C. 在 DNS 設定不同的子域名
- D. 讓前端工程師手動寫 API 文件

**Answer:** B — 每個 FastAPI service 都會自動生成 /openapi.json。統一 Swagger 的做法就是把這些 spec 收集起來（拉取或合併），用一個統一的 Swagger UI 頁面呈現。不需要改動 service 的程式碼。

---

## 🏗️ Boss Floor：Trade-off 分析

**Level 6 / 7 — 一個 Domain，多個 Service — 86% 完成**

恭喜你蓋到頂樓了！現在你知道每一層是什麼。

但 Tech Lead 真正在意的不是「這個東西是什麼」，而是「**什麼時候該用什麼**」。

這就是 trade-off 的世界。

### Trade-off 1：Ingress vs Istio

|  | 純 Ingress | Istio |
| --- | --- | --- |
| **路由** | ✅ path / host 分流 | ✅ 更細緻（header、weight） |
| **TLS** | ✅ 外部 TLS | ✅ 外部 + 內部 mTLS |
| **Canary** | ❌ 要搭配其他工具 | ✅ 原生支援 weight-based routing |
| **Observability** | ❌ 自己接 | ✅ 內建 metrics、tracing |
| **學習曲線** | 🟢 低 | 🔴 高 |
| **資源消耗** | 🟢 低 | 🔴 每個 Pod 多一個 sidecar |
| **Debug 難度** | 🟢 單純 | 🔴 多了一層 proxy 要查 |

**結論**：

- **3-5 個 service、小團隊** → 純 Ingress。別裝 Istio，你會後悔。
- **10+ 個 service、需要 canary、mTLS、統一 observability** → 上 Istio，但要有專人維護。
- **中間地帶** → 用 Ingress + Argo Rollouts（canary）+ Linkerd（比 Istio 輕量的 service mesh）。

> **Mogu 的 murmur：**
>
> 我看過太多團隊「因為 Istio 很潮」就裝了，結果 debug 一個 503 要查 Envoy proxy 的 log、Istio control plane 的設定、destination rule 的 mTLS mode… 原本 5 分鐘能解決的問題變成 2 小時。技術選型不是追流行，是解決問題。

### Trade-off 2：URL prefix 設計原則

你的 URL 要怎麼切？這裡有幾個常見的 pattern：

**Pattern A：按 resource 切**（最常見）

```plaintext
/users/...
/orders/...
/notifications/...
```

每個 prefix 對應一個 service。直覺、好懂。

**Pattern B：按版本切**

```plaintext
/v1/users/...
/v2/users/...
```

version prefix 在最前面。適合需要同時維護多版本 API 的情況。

**Pattern C：按 domain 切**

```plaintext
users.api.mycompany.com/...
orders.api.mycompany.com/...
```

用 subdomain 而不是 path prefix。好處是每個 service 完全獨立（不同 DNS、不同 TLS cert），壞處是 DNS 和 cert 管理變複雜。

**怎麼選？**

- 大多數情況 → **Pattern A**。簡單、一個 Ingress 搞定。
- 需要版本管理 → **Pattern B**。但考慮用 header（`Accept: application/vnd.myco.v2+json`）可能比 URL 版本更優雅。
- service 團隊完全獨立、想各自管理部署 → **Pattern C**。

### Trade-off 3：要不要統一 Swagger？

|  | 各自獨立 | 統一合併 |
| --- | --- | --- |
| **維護成本** | 🟢 零 | 🔴 需要維護合併邏輯 |
| **使用者體驗** | 🟡 要記多個網址 | 🟢 一個入口看所有 API |
| **適合** | 內部用、service 少 | 對外 API、service 多 |

### 完整架構回顧

最後，讓我們把整棟大樓從下到上看一遍：

```mermaid
%%{init: {"look": "handDrawn"}}%%
graph TD
U["👤 使用者<br/>api.mycompany.com/users/123"] --> DNS["📖 DNS<br/>domain → IP"]
DNS --> ING["🚧 Ingress / Istio Gateway<br/>path routing + TLS"]
ING -->|"/users/*"| US["🟢 user-service<br/>FastAPI + root_path"]
ING -->|"/orders/*"| OS["🟠 order-service<br/>FastAPI + root_path"]
ING -->|"/notifications/*"| NS["🔵 notification-service<br/>FastAPI + root_path"]
US --> SW["📄 各自的 /docs<br/>Swagger UI"]
OS --> SW
NS --> SW
style U fill:#ffeaa7,stroke:#fdcb6e
style DNS fill:#fab1a0,stroke:#e17055
style ING fill:#74b9ff,stroke:#0984e3
style US fill:#55efc4,stroke:#00b894
style OS fill:#ffeaa7,stroke:#fdcb6e
style NS fill:#a29bfe,stroke:#6c5ce7
style SW fill:#fd79a8,stroke:#e84393
```

_完整架構：DNS → Ingress → FastAPI → Swagger，每一層各司其職_

**Quiz:** 你的團隊有 3 個 FastAPI service，5 個工程師。Tech Lead 問你要不要用 Istio。你的建議是？

- A. 當然要用，Istio 功能強大，越早導入越好
- B. 先用 Ingress 就好，3 個 service 用 Istio 是殺雞用牛刀，維護成本不值得
- C. 不用任何東西，直接暴露 port
- D. 等 service 數量超過 100 個再導入 Istio

**Answer:** B — 3 個 service + 5 人團隊，Ingress 完全足夠。Istio 的學習曲線和維護成本很高（每個 Pod 多一個 sidecar、debug 複雜度增加），在 service 數量少的時候弊大於利。當 service 數量成長到 10+ 且有 canary、mTLS 等需求時，再考慮導入 Istio 或更輕量的 Linkerd。

---

## 🎓 恭喜通關！

**Level 7 / 7 — 一個 Domain，多個 Service — 100% 完成**

你從 Floor 0 的「三個 port」一路蓋到了 Boss Floor 的「trade-off 分析」。回顧一下每一層學了什麼：

| Floor | 學到了什麼 |
| --- | --- |
| **Floor 0** | 多 port 暴露 → 不好管理，需要統一入口 |
| **Floor 1** | DNS 把 domain 翻譯成 IP，是最底層的地基 |
| **Floor 2** | Ingress 根據 URL path 分流到不同 service |
| **Floor 3** | Istio 是進階的 service mesh，管 traffic、mTLS、observability |
| **Floor 4** | FastAPI 的 `root_path` 讓 Swagger 路徑正確顯示 |
| **Floor 5** | OpenAPI spec 可以統一或各自獨立呈現 |
| **Boss Floor** | 小團隊用 Ingress 就好，Istio 等真的需要再上 |

記住這篇的核心心法：

> **概念搞懂、trade-off 想清楚，implementation details 讓 AI 幫你寫。**

下一篇 Level-Up 見 🍄 ( •̀ ω •́ )✧
