iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
AI Engineering

知識圖譜 : 技能樹式學習歷程系列 第 21

Day 21 — 部署選型:四個平台的實測比較

  • 分享至 

  • xImage
  •  

今天要解的問題

前 20 天,這個網站只跑在 python3 -m http.server 8901 上。要上線了。

需求很具體,而且有一條會刷掉一半的選項:

  1. 靜態託管 + HTTPS(基本盤)。
  2. 能自訂 HTTP header ← Day 10 留下的債:frame-ancestors 只能走 header,meta 版無效。
  3. 自訂網域。
  4. 成本盡量為 0(這是個人專案)。
  5. 部署要能自動化(Day 22 會接 CI)。

我實際各開一個帳號部署了一次,記錄下來。

四個候選

GitHub Pages

# .github/workflows/pages.yml(最簡版)
- uses: actions/upload-pages-artifact@v3
  with: { path: '.' }
- uses: actions/deploy-pages@v4

實測:從 push 到上線約 45 秒。自訂網域加個 CNAME 檔就好,HTTPS 自動(Let's Encrypt)。

致命問題:完全不能自訂 HTTP header。 沒有 _headers、沒有設定檔、沒有任何機制。這意味著:

  • CSP 只能留在 <meta>frame-ancestors 永遠無效)。
  • 沒有 HSTS、沒有 X-Content-Type-Options、沒有 Referrer-Policy
  • Cache-Control 由 GitHub 決定(實測 HTML 是 max-age=600,資產也是 600——Day 24 的內容哈希策略完全發揮不了作用)。

對一個純靜態部落格無所謂。對我這個「Day 10 已經在做 CSP」的專案,這是硬傷。

Cloudflare Pages

# _headers(放在網站根目錄,部署時自動生效)
/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Content-Security-Policy: default-src 'self'; script-src 'self'; frame-ancestors 'none'
/vendor/*
  Cache-Control: public, max-age=31536000, immutable
/*.html
  Cache-Control: public, max-age=0, must-revalidate

實測:_headers 檔案就是全部設定,push 完約 30 秒生效。第 25 天的安全 header、第 24 天的快取策略,都能用這一個檔案表達。

邊界節點多(實測台灣的 TTFB 約 25 ms,GitHub Pages 約 90 ms)。免費額度對個人專案來說等於無限。

缺點:綁 Cloudflare 生態。而且 _headers 的語法錯誤是靜默失敗——寫錯的規則直接被忽略,不會有任何警告(我第一次寫錯縮排,header 全部沒生效,找了半小時)。

AWS S3 + CloudFront

S3(私有 bucket)→ CloudFront(OAC 存取)→ ACM 憑證 → Route 53

設定步驟最多:bucket policy、Origin Access Control、Response Headers Policy、cache behavior、憑證驗證、DNS。第一次做大約花了兩小時(含踩坑)。

優點是控制最細:Response Headers Policy 可以精準指定每個 header、cache behavior 可以按路徑分開設 TTL、可以用 CloudFront Functions 做邊緣改寫。而且能用 IaC 完整描述(Terraform / CloudFormation),這對「基礎建設可重現」有實質價值。

成本不是 0:Route 53 hosted zone 固定 $0.50/月,加上請求與流量費(個人專案級大約再 $0.1–0.3)。實測我的月帳單約 $0.65

Firebase Hosting

// firebase.json
{ "hosting": {
    "public": ".",
    "headers": [{ "source": "**", "headers": [{ "key": "X-Content-Type-Options", "value": "nosniff" }] }]
}}

介於 Pages 與 CloudFront 之間:header 可調、CLI 部署順暢(firebase deploy 約 40 秒)、免費額度充足。

缺點:需要 firebase-tools(Node 依賴,違反我的零建置偏好,雖然只在部署端)。而且它的定位是「應用平台」,做純靜態有點大砲打小鳥。

實測比較表

GitHub Pages Cloudflare Pages AWS S3+CF Firebase
自訂 HTTP header 完全不行 _headers ✅ 最細緻 firebase.json
frame-ancestors
自訂 Cache-Control
設定複雜度 極低
首次部署時間 45 s 30 s 約 2 小時(含學習) 40 s
台灣 TTFB(實測) 90 ms 25 ms 30 ms 45 ms
月成本 $0 $0 約 $0.65 $0
IaC 支援 無意義 有限 ✅ 完整 有限
廠商綁定

決策:用兩套

我最後同時部署兩套,這聽起來像過度工程,但理由具體:

主站:Cloudflare Pages(Day 23–25)

  • 滿足全部硬需求,成本 0,速度最好。
  • _headers 一個檔案就能表達 Day 24 的快取策略與 Day 25 的安全 header。
  • 部署極簡,適合「每天寫一篇文章順手 push」的節奏。

第二套:S3 + CloudFront with Terraform(Day 26)

  • 練基礎建設:OAC、最小權限 IAM、IaC、invalidation 策略,這些是真正可轉移到工作上的技能。純託管平台學不到。
  • 廠商風險分散:兩套獨立的部署路徑,任一方出問題可以切換。
  • 成本 $0.65/月,我認為這個學習投資划算。

明確不做的:不做多雲負載平衡、不做藍綠部署。單一個人專案的靜態站,那些只是複雜度。

上線前的檢查清單

從本機到雲端,有幾件事會壞:

1. 大小寫敏感。 本機(macOS/Windows)檔案系統不分大小寫,Linux 伺服器分。

# 掃出 HTML/JS 裡引用的路徑,逐一檢查檔案真的存在(含大小寫)
grep -ohE '(src|href)="[^"]+"' *.html \
  | sed -E 's/.*="([^"]+)".*/\1/' \
  | grep -v '^https\?://' | grep -v '^#' \
  | sed 's/?.*//' | sort -u \
  | while read -r f; do [ -e "$f" ] || echo "缺檔或大小寫不符:$f"; done

我真的抓到一個:css/Style.css 在某個 HTML 裡被寫成大寫 S。本機完全正常,上線 404。

2. 相對路徑。 全站用相對路徑(css/style.css 而不是 /css/style.css)——這是 Day 1「file:// 可直開」的副產品,剛好讓網站能部署在子目錄下。要驗證:

grep -n 'src="/\|href="/' *.html    # 應該沒有輸出(絕對路徑在子目錄部署會壞)

3. .gitignore 反查。 部署是「把 repo 內容推上去」,所以要確認該上去的東西沒被 ignore

git ls-files vendor/ | wc -l        # Day 16 自架的 MathJax 與字型必須在版控裡
# 應該有幾十個檔案。如果是 0,就是被 .gitignore 誤殺了

我一開始在 .gitignore 寫了 *.woff2(想排除測試用的字型),結果 Day 16 自架的字型全部沒進 git,部署後中文變成系統預設字型。

4. .reference/ 絕對不能上去。 這是版權問題,比技術問題嚴重得多:

git ls-files | grep -c '^\.reference/'    # 必須是 0

我把這條加進 CI(Day 22)當硬性 gate。部署是不可逆的——第三方版權素材一旦推到公開網址,就算立刻刪除也可能已經被快取或索引。

5. Service Worker 的 scope。 Day 16 的 sw.js 在根目錄,scope 是整站。如果部署在子目錄(example.com/learnpath/),註冊路徑要跟著調整:

navigator.serviceWorker.register("./sw.js");    // 相對路徑,自動跟隨部署位置

./sw.js 而不是 /sw.js——後者在子目錄部署時會找錯位置,而且 scope 會超出實際範圍導致註冊失敗。

一個容易忽略的決定:要不要 www

四個選項,我選第三個:

選擇 說明
只用 apex(example.com 簡潔,但 apex 不能用 CNAME(要看 DNS 商是否支援 flattening)
只用 www DNS 最單純(純 CNAME)
apex 為主 + www 301 轉址 使用者兩個都能用,SEO 只有一個規範網址
兩個都能訪問 ❌ 重複內容,SEO 扣分

Cloudflare Pages 支援 CNAME flattening,所以 apex 可以直接指向 Pages。轉址規則用一條 redirect rule 搞定。

踩到的雷

GitHub Pages 的 Cache-Control 不能改,讓 Day 24 的計畫落空了一半。 我原本以為「只要檔名帶內容哈希,快取策略就自動正確」。錯——檔名哈希只解決「舊檔案不被誤用」,不解決「新檔案不被積極快取」。沒有 max-age=31536000, immutable,瀏覽器每次還是會發條件請求(304),TTFB 白白多一趟。這是我把 Pages 從候選中排除的最後一根稻草。

Cloudflare Pages 的 _headers 語法錯誤靜默失敗。 縮排必須是兩個空白,而且路徑模式行不能有前導空白。寫錯的規則直接被忽略、部署照樣成功、header 就是沒有。驗證方式只能是部署後實際打:

curl -sI https://example.com/ | grep -i 'content-security\|x-content-type'

Day 25 會把這個 curl 檢查寫成自動化的上線後驗證。

AWS 的 Route 53 費用是固定的。 $0.50/月的 hosted zone 費用不在免費額度內,也不隨用量變化。很多「AWS 靜態網站免費」的教學沒提這件事。如果你只想免費,就不要用 Route 53(把 DNS 留在原本的註冊商,只用 CloudFront)。

驗證

四套都部署一次,然後用同一組檢查打它們:

for host in \
  "user.github.io/repo" \
  "project.pages.dev" \
  "d123abc.cloudfront.net" \
  "project.web.app"
do
  echo "=== $host ==="
  curl -sI "https://$host/index.html" | grep -iE 'HTTP/|cache-control|content-security-policy|x-content-type'
  echo "TTFB: $(curl -so /dev/null -w '%{time_starttransfer}' "https://$host/index.html")s"
done

功能驗證(每一套都要跑):

  • 首頁地圖 18 個節點都出現。
  • 課文頁公式渲染(Day 7 的兩個 bug 的線上回歸)。
  • Service Worker 註冊成功(chrome://serviceworker-internals)。
  • 離線重整仍可讀(Day 16 的驗收點)。
  • 沒有任何第三方請求(Day 16 的成果)。

小結與明天預告

今天的決策與理由:

  1. 主站 Cloudflare Pages——唯一同時滿足「自訂 header + 零成本 + 極簡部署」的選項。
  2. 第二套 S3+CloudFront with Terraform——為了學 IaC、OAC、最小權限,$0.65/月的學習投資。
  3. GitHub Pages 被排除的原因很具體:不能自訂 HTTP header,導致 CSP 不完整、快取策略無法實施。

上線前的五個檢查(大小寫、相對路徑、.gitignore 反查、.reference 零殘留、SW scope)看起來瑣碎,但每一條我都真的踩到過。

明天先做 CI 再做部署——順序刻意如此。沒有 CI 的自動部署,只是把 bug 推上線的自動化。 我會把 Day 10 的雙層驗證、Day 11 起累積的五支檢查腳本,全部接進 GitHub Actions,並處理 headless Chrome 在 CI 環境的兩個經典雷。


上一篇
Day 20 — 內容自動化:從亂碼 PDF 到教材涵蓋稽核
下一篇
Day 22 — CI 先行:GitHub Actions 跑雙層驗證
系列文
知識圖譜 : 技能樹式學習歷程22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言