Web Day 44 專案:文件與交接
執行需求:CPU 可跑。Stack、監控、備份、壓測都做完了,今天要把這套預約管理系統變成「可以被別人接手」的狀態。今天要做四件事:第一,寫一份 README.md(開發者上手指南、安裝指令、常用操作);第二,寫一份 runbook.md(事件處置手冊:磁碟滿、PostgreSQL 沒起來、API 5xx 飆升時的 step-by-step 復原流程);第三,整理一份 OpenAPI 規格摘要(從 FastAPI 0.116 的 /openapi.json 自動生成);第四,把 Day 40 的驗收清單(acceptance checklist)升級為正式上線文件,列為 PR 模板的一部分。所有文件都用 Markdown 寫,UTF-8 編碼,繁體中文(術語沿用 README 的台灣用語對照表);資料仍是虛構示範。今天是 Day 45 系列總結前的倒數第二天,把整套 stack 的可維運性做最後一次打磨。
引言
「寫文件」是工程師最常拖延的工作,原因是「現在沒時間」與「以後再寫」。但真正出事的時候,文件就是工程師最重要的武器:一份結構完整的 runbook 能讓接手的人凌晨三點不必打電話給原作者就能復原系統;一份更新過的 README 能讓新人第一天就上手。這些文件不是「bonus」,而是 production 系統的基本配備。
貫穿專案的預約管理系統是小型服務業線上預約平台(情境涵蓋攝影棚、健身教練、諮詢工作室,所有資料都是虛構示範)。經過 Day 35–43,我們已經有了:可跑的 stack(Docker Compose v2、PostgreSQL 17、FastAPI 0.116、Caddy 2.8);可監控(/metrics、JSON log);可備份(每日 pg_dump + 還原演練);可壓力測試(locust + 純 Python);可驗收(8 支 E2E + 驗收清單)。這些成果沒有文件,就只是「在原作者電腦裡跑得動」;有了文件,才是「可以被別人接手」的系統。
今天的內容分四段:第一段說明文件化策略(哪些文件、給誰看、如何維護);第二段寫 README.md 與 RUNBOOK.md 的範本;第三段用 Python 腳本從 /openapi.json 自動產生 API 規格摘要;第四段把 Day 40 的驗收清單升級為正式上線清單與 PR 模板。讀完這篇你會了解:為什麼寫文件是「為未來的我」投資、runbook 與 README 的本質差別、FastAPI 自動 OpenAPI 的優勢、怎麼讓驗收清單變成 commit-friendly 格式。
原理解念:README、runbook、API 規格、驗收清單
README 與 runbook 是兩種本質不同的文件:README 是「正常情況下」,給第一次接觸這個專案的開發者讀,回答「這是什麼」、「怎麼跑起來」、「怎麼測試」、「怎麼貢獻」。runbook 是「不正常情況下」,給值班的工程師在事件發生時讀,回答「症狀是什麼」、「原因可能是什麼」、「要怎麼處理」、「處理完要記什麼」。前者是 onboarding,後者是 incident response。兩者不能合併,因為讀者的心智模式不同。
API 規格摘要的目的是「不看程式碼也能對齊合約」。FastAPI 0.116 內建的 /openapi.json 是 JSON Schema 標準,包含了所有路由、請求參數、回應格式。我們可以再加一層 Markdown 摘要(路徑、方法、用途、auth 需求),幫助前端、後端、PM 三種角色快速對齊。這比寫死 docs/API.md 維護成本低:摘要從 OpenAPI 自動生成,永遠跟程式碼同步。
驗收清單(acceptance checklist)是寫在 PR 模板裡、人手一份要逐項勾的東西。它把「上線必須做的事」變成可重現的流程。Day 40 我們示範了它的雛型,今天把它升級成更完整的版本,包含開發者自查、DBA 簽核、SRE 簽核三個角色。這不是為「怕有人忘記」而設計,而是為「用最低的成本攔截最多的錯誤」而設計。
完整實作:README、RUNBOOK、API 摘要、驗收清單
先寫 README.md。我們的 README 採標準結構:標題、一句話描述、快速開始、開發流程、測試、部署、版本、授權:
# 對應的 README.md(純文字示範;以下內容是實際 repo 的 README)
README = """# 預約管理系統(booking-system)
小型服務業線上預約平台後端 API。情境涵蓋攝影棚、健身教練、諮詢工作室;所有資料皆為虛構示範。
## 技術棧
- Python 3.13、FastAPI 0.116、SQLModel 0.0.24、SQLAlchemy 2.0.41、Pydantic 2.11
- PostgreSQL 17、Caddy 2.8、HTMX 2.0、uv 0.7
- Docker Compose v2、pytest 8.4、httpx 0.28、Alembic 1.16
## 快速開始
1. 複製環境變數範本:\`cp .env.example .env\`
2. 啟動 stack:\`docker compose up -d\`
3. 跑 migrations:\`docker compose exec api alembic upgrade head\`
4. 開瀏覽器:[http://localhost/admin/dashboard](http://localhost/admin/dashboard)
預設 admin 帳號:admin@example.com / admin-pass
## 開發流程
\`\`\`bash
uv sync # 安裝依賴
uv run pytest -v # 跑測試
uv run ruff check # linter
uv run alembic upgrade head
uv run uvicorn booking_system.main:app --reload
\`\`\`
## 測試
- \`tests/test_e2e.py\`:8 支核心 API 端對端測試
- \`tests/test_perf_health.py\`:健康檢查 P95 < 50ms smoke
- \`tests/test_migration.py\`:Alembic upgrade head 驗證
- \`tests/test_admin_dashboard.py\`:HTMX 後台互動測試
## 部署
- 開發用 docker compose up -d
- staging / production 建議升級為 Kubernetes 或 AWS ECS
- 細節請見 RUNBOOK.md
## 文件
- \`docs/API.md\`:API 路由摘要(從 /openapi.json 自動生成)
- \`docs/RUNBOOK.md\`:事件處置手冊
- \`docs/acceptance.md\`:上線驗收清單(PR 模板的一部分)
## 授權
MIT License(示範專案)。
"""
print(README.strip())
這個 README 把「快速開始」放最前面(每個 README 都該如此)、再放開發流程、測試、部署、文件、授權。專案名、版本、授權這三樣務必放,這是 GitHub 顯示專案摘要時的關鍵資訊。
接下來是 RUNBOOK.md:
RUNBOOK = """# RUNBOOK — 預約管理系統
> 事件處置手冊。當你看到告警或接到客戶投訴,第一件事是查這份文件。
## 1. 健康檢查失敗(/health 回 503 或逾時)
**症狀**:Prometheus 告警「up == 0」超過 1 分鐘;K8s liveness probe 失敗。
**處理**:
1. SSH 進主機,確認 container 還在:\`docker compose ps\`
2. 查看 container log:\`docker compose logs --tail=200 api\`
3. 若是 process crash,重啟:\`docker compose restart api\`
4. 若是 DB 連不上,先確認 db container:\`docker compose exec db pg_isready -U booking\`
5. 重啟後跑 smoke:\`curl -s http://localhost/health\`
## 2. API 5xx 比率飆升(>1% 連續 5 分鐘)
**症狀**:\`booking_errors_total\` 上升;客戶回報操作失敗。
**處理**:
1. 看 Grafana「過去 1 小時 P95」是哪條 endpoint 出包
2. 查 log:\`docker compose logs api | grep -i error | tail -50\`
3. 若全是 DB 錯誤,先查 \`pg_stat_activity\` 看是否有 long query:\`docker compose exec db psql -U booking -c 'SELECT pid, state, query FROM pg_stat_activity;'\`
4. 若 DB 沒事、API 才有事,看 \`/metrics\` 找哪個 path 爆量
5. 必要時啟動「kill switch」:\`docker compose scale api=0\` 然後 \`=1\` 把損面降到最小
## 3. PostgreSQL 磁碟滿
**症狀**:\`pg_dump\` 失敗;DB 寫入報 disk full。
**處理**:
1. SSH 進主機,\`df -h\` 看哪個分割區滿了
2. 若 backup volume 滿,先 prune:\`find /var/backups/booking -mtime +14 -delete\`
3. 若 data volume 滿,看 audit_logs:\`SELECT COUNT(*) FROM audit_logs WHERE created_at < NOW() - INTERVAL '90 days';\`,彙整歸檔或刪除
4. 清完後 \`docker compose exec db vacuumdb -U booking -d booking\`,釋放空間
5. 預防:把 archive 寫進 \`docs/RUNBOOK.md\` 的「每月歸檔」段落
## 4. 通知(simulated)沒寄出
**症狀**:客戶反映沒收到確認信;\`notification.sent\` log 沒新增。
**處理**:
1. 看 Notification 表:\`SELECT id, status, attempt_count, last_error FROM notifications WHERE status != 'sent' ORDER BY created_at DESC LIMIT 20;\`
2. 若全是 queued 沒送出,看 APScheduler 還活著:\`docker compose logs api | grep scheduler\`
3. 必要時 \`docker compose exec api uv run python -c 'from booking_system.scheduler import send_upcoming_reminders; import asyncio; asyncio.run(send_upcoming_reminders())'\`
## 5. 還原演練(disaster recovery)
**症狀**:磁碟損壞、database 損毀、需要從備份還原。
**處理**:
1. 找最新備份:\`ls -t /var/backups/booking/booking-*.sql.gz | head -1\`
2. 還原到臨時 DB:\`python scripts/restore_drill.py <備份檔> postgresql://booking:booking-dev@localhost:5432/booking-drill\`
3. 確認資料完整:\`psql -d booking-drill -c 'SELECT COUNT(*) FROM bookings;'\`
4. 切換 DNS 指向臨時 DB:\`docker compose exec caddy caddy reload\`
5. 修好正式 DB 後再切回
"""
print(RUNBOOK.strip())
這個 runbook 涵蓋五種常見事件:健康檢查失敗、5xx 飆升、磁碟滿、通知沒寄出、災難還原。每個事件包含「症狀 + 處理」。處理步驟都附具體指令,讓值班工程師照著打。我們刻意把每個場景切成 5 步以內,避免手忙腳亂時找不到下一行。
API 規格摘要自動生成器(從 FastAPI 的 /openapi.json 抽出):
# scripts/docs_gen_api_summary.py
# 從 /openapi.json 抽出每條路由的 method、path、auth、回應,寫成 Markdown
import json
import sys
from urllib.request import urlopen
def fetch_schema(base_url: str) -> dict:
return json.loads(urlopen(f"{base_url}/openapi.json", timeout=5).read())
def render_markdown(schema: dict) -> str:
lines = [
f"# API 規格摘要(自動從 {schema.get('info', {}).get('title', '')} 生成)",
"",
f"version: {schema.get('info', {}).get('version', 'unknown')}",
"",
"## 路由",
"",
]
for path, methods in sorted(schema["paths"].items()):
for method, op in methods.items():
if method.upper() not in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
continue
tag = (op.get("tags") or [""])[0]
summary = op.get("summary") or op.get("description") or ""
sec = [r for r in op.get("responses", {}) if r.startswith("2")]
lines.append(f"### {method.upper()} {path}")
lines.append(f"- 所屬群組:{tag}")
lines.append(f"- 用途:{summary}")
if sec:
lines.append(f"- 成功回應:{', '.join(sec)}")
lines.append("")
return "\n".join(lines) + "\n"
if __name__ == "__main__":
base = sys.argv[1] if len(sys.argv) > 1 else "http://localhost"
out = sys.argv[2] if len(sys.argv) > 2 else "docs/API.md"
schema = fetch_schema(base)
Path = __import__("pathlib").Path
Path(out).write_text(render_markdown(schema), encoding="utf-8")
print(f"已寫出 {out}({len(schema['paths'])} 條路徑)")
這支腳本從 /openapi.json 抽出 routes,寫成 Markdown。每次 deploy 前跑一次,docs/API.md 就會跟程式碼同步。我們刻意用純 Python 標準庫(json、urllib、pathlib),不引入額外依賴;這樣在 CI 任何環境都能跑。
驗收清單(升級版)與 PR 模板:
# 對應的 docs/acceptance.md(升級版)
ACCEPTANCE_MD = """
# 預約管理系統 上線驗收清單(v2)
## A. 開發者自查(PR 作者填)
- [ ] 本地端 `uv run pytest -v` 全部通過
- [ ] 本地端 `uv run ruff check` 沒有錯誤
- [ ] 本機 `docker compose up -d` 跑得起來
- [ ] 本機 `docker compose exec api alembic upgrade head` 無錯誤
- [ ] `curl http://localhost/health` 回 200
- [ ] 本次 PR 變更的所有 endpoint 都有對應測試
- [ ] 沒有未轉義的小於、大於、& 符號在程式碼區塊裡
- [ ] 沒有新增的固定金鑰 / mock token / 假網址
## B. 程式碼審查(Reviewer 填)
- [ ] 對應的 issue / spec 連結已附在 PR 描述
- [ ] 改動限制在討論範圍內,沒有無關重構
- [ ] 資料庫 migration 與模型定義一致
- [ ] 沒有新增 hardcoded URL / 假資料 / 模擬帳號
- [ ] 對 notification 路徑改動,確認 send 仍是模擬(無真實 SMTP / SMS 帳號洩漏)
## C. DBA 簽核(合併進 main 前)
- [ ] Alembic migration 跑過並可 downgrade
- [ ] 新增的索引列在 \`docs/INDEXES.md\`
- [ ] 預期資料量級已寫進備份大小估算
## D. SRE 簽核(部署到 production 前)
- [ ] Prometheus scrape 設定已更新
- [ ] runbook 已對應本次變更
- [ ] smoke test 計畫加進 deployment checklist
- [ ] on-call 知道這次部署的特性
"""
# 對應的 .github/PULL_REQUEST_TEMPLATE.md
PR_TEMPLATE = """
## 變更摘要
- 改了什麼(一句話)
## 對應議題 / 規格
- 連結:
## 變更類型
- [ ] 新功能
- [ ] bug fix
- [ ] 文件 / 註解
- [ ] 重構(無功能改變)
- [ ] 部署 / 維運
## 驗收(請逐項勾)
- [ ] `uv run pytest -v` 通過
- [ ] `uv run ruff check` 通過
- [ ] `curl /health` 回 200
- [ ] OpenAPI 摘要已更新(`python scripts/docs_gen_api_summary.py`)
"""
print(ACCEPTANCE_MD.strip())
print("---")
print(PR_TEMPLATE.strip())
這份升級版驗收清單分成 A/B/C/D 四個角色,每個角色的勾選條目不同:開發者自查範圍小(自己能驗證的)、Reviewer 看程式品質、DBA 看資料庫、SRE 看維運。對小型團隊來說可以簡化為 A+B+小段 C,但「分類」這個觀念本身就有價值:它逼你在合併前想清楚「誰要看這份 PR」。
常見錯誤與踩雷
第一個常見錯誤是「README 過於冗長」。一份 README 應該 5 分鐘內讀完;如果太長,請把細節拆到 docs/ 子目錄。我們今天的版本刻意控制在 80 行以內,重要段落都有「快速開始」段落讓新手第一時間看到。
第二個常見踩雷是「runbook 只有症狀、沒有指令」。只寫「檢查 DB」不夠,要寫「docker compose exec db pg_isready -U booking」。我們今天的範例每個處理步驟都有具體指令,方便值班工程師直接複製貼上。另一個常見變體是 runbook 寫了指令、但指令在 production 環境不能用(例如把 dev container 當主機)。對應處理:在 production 環境跑一遍所有 runbook 指令、發現不合實際的立刻修。
第三個是「OpenAPI 摘要沒有自動生成」。手刻 docs/API.md 一定會跟程式碼不同步,最常見的狀況是「新增了 endpoint 但忘記更新文件」。我們今天的 scripts/docs_gen_api_summary.py 把 OpenAPI 自動產生成 Markdown,CI 上每次 build 跑一次即可。我們的 `/openapi.json 來自 FastAPI 自動產生,永遠跟程式碼同步。
第四個是「驗收清單只給開發者」。如果只有開發者自查,沒有 Reviewer、DBA、SRE 的檢查環節,那些需要領域專業的問題(例如「這條 SQL 會不會 lock 表」、「這個 endpoint 會不會讓監控告警誤判」)就不會被發現。我們的 A/B/C/D 四角色版本就是強迫「該看的人要看」。
效能與實務提醒
文件品質比文件數量重要。一份精心寫的 50 行 README 遠勝過 500 行拼貼而成的 wiki。我們建議每個專案都把 README、RUNBOOK、API 摘要、驗收清單四份文件定為「必讀」,其餘細節留給程式碼與 commit history。
文件要跟程式碼一起進版控。我們刻意把 docs/API.md 與 docs/acceptance.md 放進 Git 儲存庫,PR 修改時一起 review;docs/API.md 從 /openapi.json 自動生成,docs/acceptance.md 與 RUNBOOK.md 是人手編輯但有 commit history 可追蹤。
runbook 的更新成本不可忽略。一份好的 runbook 在 production 一年會被更新 10–20 次(每次事件後都會調整),這表示「runbook 不是寫一次就結束」。我們建議把 runbook 與 Day 42 的監控指標綁定:每次新增告警時,同步更新 runbook 中對應的處置段落;每次事故復盤後,把新發現的處置步驟寫進 runbook。這樣 runbook 才能真的在凌晨三點救人。
最後,文件化不是工程師的「bonus」,而是 senior 與 junior 最大的差別之一:junior 工程師寫完程式碼就提交、senior 工程師則會花兩小時把程式碼變成「可以被接手」。今天的這份文件化工作,也是把一個人做的專案升級為「團隊可以共同維運」的關鍵。
底下是一支「CI 跑一次就把所有文件生成」的整合腳本:
# scripts/build_docs.py
# 在 CI 跑一次:把 docs/API.md 生成、跑驗收清單的自動可驗項、寫入 docs/BUILD.md
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
def run(cmd: list[str]) -> str:
"""執行指令並回傳 stdout,失敗就 raise。"""
print(f"$ {' '.join(cmd)}")
r = subprocess.run(cmd, capture_output=True, text=True, check=True)
return r.stdout
def build_api_summary(base_url: str) -> None:
run([
sys.executable, "scripts/docs_gen_api_summary.py",
base_url, "docs/API.md",
])
def build_acceptance_record() -> None:
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
Path("docs/BUILD.md").write_text(
f"# 自動驗收紀錄\n\n- 日期:{today}\n- API 摘要:已更新\n",
encoding="utf-8",
)
def main() -> int:
build_api_summary("http://localhost")
build_acceptance_record()
print("所有文件已生成")
return 0
if __name__ == "__main__":
sys.exit(main())
這支腳本可以加進 scripts/build_docs.py,並在 CI 的 deploy job 跑一次:先起 stack、跑 build_docs.py、再寫進 docs/BUILD.md。如此一來,每次 deploy 都會留下「這次 deploy 有哪些文件更新」的可追蹤記錄。
# 在 Day 32 的 GitHub Actions workflow 加這段(節錄)
CI_STEP = """
- name: Build docs
run: |
docker compose up -d
sleep 5
python scripts/build_docs.py
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: docs
path: docs/
"""
print(CI_STEP.strip())
這兩段把「文件即程式」的觀念落實進 CI:deploy job 一跑就生成新文件,之後上傳成 artifact 留給 PR review 與發佈用。等 Day 45 系列收尾後,我們會在延伸資源中列出完整的 GitHub Actions 範本。
小結
今天把整套預約管理系統變成「可以被別人接手」的狀態。我們寫了 README.md(5 分鐘內可讀完的開發者指南)、RUNBOOK.md(五種常見事件的 step-by-step 處置手冊)、寫了 scripts/docs_gen_api_summary.py 從 OpenAPI 自動產生 API 規格摘要、把 Day 40 的驗收清單升級為 A/B/C/D 四角色版本、加上一支 scripts/build_docs.py 把整個文件生成流程自動化。所有文件都用 Markdown + UTF-8,與 README 規範一致;資料延續 Day 35–43 的虛構示範。明天 Day 45 是系列最後一篇,會回顧 45 天從「寫腳本」到「上線維運」的學習路徑,並列出延伸學習方向。
從「會寫程式」到「能被接手」是另一個層次的工程師價值。我們今天做的 4 份文件在小型團隊裡可能太多、在大型團隊裡可能不夠;但「先把這 4 份寫好、再依需求擴充」這條思路本身是有用的。我們預期接手這個 stack 的人在第一個月能上手(靠 README)、第一個事件發生時能復原(靠 runbook)、第二個月需要修改 API 時能對齊合約(靠 docs/API.md)。文件不是限制工程師,而是讓更多人能一起把專案做好。
結語
今天的重點是把「在原作者電腦裡跑得動的程式」變成「可以被別人接手」。README、runbook、API 摘要、驗收清單這四份文件看似瑣碎,但它們對 production 系統的可維運性有決定性的影響。明天 Day 45 我們會回顧整個 45 天學習路徑,從 Day 1 的 FastAPI 起步到 Day 44 的文件交接,把「從零到 production」的整套能力整理成一份索引,並列出幾個延伸學習的方向(分散式追蹤、k8s、observability stack 等)。整個系列至此告一段落,但你的後端工程師之路才剛剛開始。我們這 45 天做的不是「速成」而是「打底」:你會了 FastAPI 0.116、SQLModel、HTMX、Docker Compose、Prometheus、pg_dump、locust,這些能力堆起來就像工具箱,後續遇到任何後端問題都可以從裡面找答案。明天的系列總結會把這些內容再索引一次,幫助你日後快速複習。
另外寫文件這種事有一個小祕訣:每天花十分鐘把昨天處理的事寫成 1–2 句 commit message,每週花半小時把這週遇到的事寫成 runbook 更新。六個月後你就會有一份實戰級的 runbook,這是免費的(只要你每天持續寫)。我們今天寫的這份 runbook 是「地基」,未來每次事件都會讓它長出新的枝葉。明天 Day 45 開始,讓我們一起回顧 45 天的全景。
延伸資源
- GitHub README 最佳實踐(2025):
https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-readmes,README 應該放什麼、怎麼分類。 - Google SRE Book — Runbook 指南(2017,跨年代經典):
https://sre.google/sre-book/eliminating-toil/,runbook 的撰寫原則與「可被替代」的概念。 - FastAPI OpenAPI 官方說明(0.116,2025):
https://fastapi.tiangolo.com/advanced/extending-openapi/,自訂 OpenAPI metadata 與 tags。 - Conventional Commits(2024):
https://www.conventionalcommits.org/zh-hant/,commit 訊息的標準格式,能讓文件與版本記錄整齊對應。 - Keep a Changelog(2024):
https://keepachangelog.com/zh-TW/1.1.0/,CHANGELOG.md 的標準寫法。
留言
張貼留言