- date
- entry
- 001
- topic
- infrastructure
- rev
- —
S3 + CloudFront 的目錄 key 陷阱:為什麼 /blog/ 會變成首頁
這個壞法的討厭之處在於它回 200。監控不會叫、curl 看起來正常,只有人真的點進去才會發現內容是錯的。
把靜態站放上 S3 + CloudFront,最常見的架法是 bucket 全私有、用 OAC 讓 CloudFront
以 REST origin 取檔。這個組合安全、便宜、也是 AWS 建議的做法。它有一個代價:CloudFront 不會替你把 /blog/ 解析成 blog/index.html。
S3 的靜態網站託管端點(website endpoint)會做這件事,REST 端點不會。而只要你用了 OAC
把 bucket 關成私有,你用的就是 REST 端點。於是 /blog/ 這種目錄形式的網址會拿到
NoSuchKey。
為什麼它會偽裝成 200
多數人會在 distribution 上設自訂錯誤回應,把 403/404 導到 /index.html 並回
200——這是 SPA 的標準做法,前端路由才能運作。兩件事湊在一起就成了陷阱:
/blog/在 REST origin 找不到 key- 錯誤回應規則把它換成首頁,並且回 200
結果是使用者打開 /blog/ 看到的是首頁,網址列卻停在 /blog/。搜尋引擎抓到的也是首頁內容。因為狀態碼是
200,任何以狀態碼為準的健康檢查都不會響。
回 404 的壞很容易發現,回 200 的壞可以活好幾個月。
修法:把 index.html 額外寫進目錄 key
最直接的做法是在部署時,把每個 index.html 同時上傳到「目錄 key」——含斜線與不含斜線各一份:
cd dist
find . -mindepth 2 -name index.html | while read -r f; do
rel="${f#./}"
dir="${rel%index.html}"
for key in "$dir" "${dir%/}"; do
aws s3api put-object \
--bucket "$BUCKET" --key "$key" --body "$f" \
--content-type "text/html; charset=utf-8" \
--cache-control "public, max-age=0, must-revalidate"
done
done
兩個 key 都要寫:blog/ 對應使用者輸入的帶斜線網址,blog
對應不帶斜線的版本。少寫一個就會有一半的連結壞掉。
另一條路是改用 CloudFront Functions 在 viewer request 階段把結尾是 /
的路徑補上 index.html。比較乾淨,但多一個要維護的函式;檔案數不多時上面那段迴圈夠用。
真正的坑在 sync --delete
補完目錄 key 之後會出現第二個問題:那些 key 在你本機的 dist/ 裡不是檔案。所以下次部署跑
aws s3 sync dist/ s3://bucket/ --delete 時,sync 認為它們是多餘物件,全部刪掉。
正確的順序因此變成:
sync --delete(會刪掉上一輪補的目錄 key)- 重新寫入所有目錄 key
- CloudFront invalidation
這個順序有個脆弱處:第 2 步中途失敗,站上就會留下一批 404。我遇過一次
AWS session 在第 2 步跑到一半過期,結果是幾個客戶的正式頁面直接消失,而前面的檔案都已經同步完成。腳本用了
set -e,所以連第 3 步的 invalidation 都沒跑。
事後補救不難(重跑第 2 步就好),但要先知道有哪些頁面掛了。我現在的做法是部署後跑一次全站比對:把
dist/ 裡每個 index.html 的 <title>
抓出來,逐一請求線上對應網址,比對標題是否一致。不一致的就是壞的。
這件事比看起來重要,因為它同時檢出兩種壞法:目錄 key 缺失(回首頁或 404),以及部署根本沒生效(回舊標題)。
如果只記得一件事
用狀態碼判斷靜態站健康與否是不夠的。在 SPA fallback 存在的情況下,200 只代表 CloudFront 有東西可以回你,不代表它回的是對的東西。要比對內容。
修訂紀錄
- 首次發布