接手專案,先看帳單
因為老闆信用卡到期了要換新卡,我順便看了一下 AWS 帳單金額,發現比預期高。之前詢問外包商技術長(已離職),得到的回覆是:「服務都已經從新加坡遷移到台北了,除了 S3 有保留做備份,其他都刪除了。」
身為工程師,最不能接受的就是「應該是這樣」。我決定親自盤點。
名詞解釋
在繼續之前,先解釋一下會提到的 AWS 服務:
| 服務 | 說明 | 費用特性 |
|---|---|---|
| S3 (Simple Storage Service) | 物件儲存服務,用來存放檔案、圖片、影片 | 按儲存容量和請求次數計費 |
| NAT Gateway | 讓私有子網路的資源能存取網際網路 | 按小時計費,即使沒流量也要錢 |
| Elastic IP | 固定的公開 IP 位址 | 使用中免費,未關聯則收費 |
| VPC (Virtual Private Cloud) | 虛擬私有網路,隔離你的雲端資源 | VPC 本身免費,但相關資源收費 |
| Network Load Balancer | 負載平衡器,分散流量到多台伺服器 | 按小時和處理的資料量計費 |
| ECR (Elastic Container Registry) | Docker 映像檔儲存庫 | 按儲存容量計費 |
重點是:有些資源即使沒有流量,只要存在就會收費。NAT Gateway 和未關聯的 Elastic IP 就是典型的「隱形殺手」。
盤點遺留資源
# 檢查 EKS 叢集(Kubernetes 服務)
aws eks list-clusters --region ap-southeast-1
# 結果:空的 ✓
# 檢查 RDS(資料庫)
aws rds describe-db-instances --region ap-southeast-1
# 結果:空的 ✓
# 檢查 NAT Gateway
aws ec2 describe-nat-gateways --region ap-southeast-1 \
--filter "Name=state,Values=available"
# 結果:2 個還在跑
完整盤點結果:
| 資源類型 | 數量 | 月費估算 |
|---|---|---|
| NAT Gateway | 2 | ~$65 |
| Network LB | 1 | ~$16 |
| Elastic IP (未關聯) | 2 | ~$7 |
| VPC | 4 | - |
| ECR Repository | 5 | - |
| S3 Bucket | 1 | ~$2 |
| 合計 | ~$90/月 |
EKS 和 RDS 確實刪了,但網路層的資源全部還在。這就是口頭交接的風險——沒有文件記錄,就會有遺漏。
清理閒置資源
確認這些資源沒有在用(DNS 已指向台北、Target Group 是空的),開始清理:
# 刪除 Load Balancer
aws elbv2 delete-load-balancer --load-balancer-arn $LB_ARN
# 刪除 NAT Gateway
aws ec2 delete-nat-gateway --nat-gateway-id nat-07fb8958f69654f50
# 釋放 Elastic IP(NAT Gateway 刪除後才能釋放)
aws ec2 release-address --allocation-id eipalloc-xxxxx
# 刪除 VPC(需按相依性順序)
# Subnet → Internet Gateway → Route Table → Security Group → VPC
S3 的部分,先確認台北資料完整:
# 台北 S3:72.9 GiB,316,000 個檔案
# 新加坡 S3:49.6 GiB,2,300 個檔案
# 台北資料更完整,可以清理新加坡的
aws s3 rb s3://prod-s3-singapore --force
發現 Strapi 的 URL 儲存機制
清理完成後,我主動檢查網站功能,發現部分圖片載入失敗。打開 DevTools 查看:
GET https://prod-s3-singapore.s3.ap-southeast-1.amazonaws.com/image_abc123.png
→ 404 Not Found
有些 URL 還是指向舊的 S3。
排查資料庫
這個專案使用 Strapi CMS,檔案資訊存在 PostgreSQL。先檢查主要的 files 表:
SELECT COUNT(*) FROM files
WHERE url LIKE '%ap-southeast-1%';
-- 結果:0
主要 URL 欄位是乾淨的。但問題出在 Strapi 的縮圖機制。
Strapi 的多層 URL 儲存
Strapi 上傳圖片時,會自動產生多種尺寸的縮圖。這些 URL 存在 formats 欄位(JSONB 格式):
{
"thumbnail": {
"url": "https://prod-s3-singapore.s3.../thumbnail_abc.png",
"width": 156,
"height": 156
},
"small": {
"url": "https://prod-s3-singapore.s3.../small_abc.png",
"width": 500,
"height": 500
}
}
系統性搜尋後,找到 URL 分散在四個地方:
| 位置 | 筆數 | 說明 |
|---|---|---|
files.url | 0 | 主要 URL(遷移時已處理) |
files.formats | 419 | 縮圖 URL(JSONB) |
layout_d_singles.content | 12 | 頁面內容(HTML) |
layout_d_singles.content_searchable | 12 | 搜尋索引 |
這就是遷移時的技術債——只更新了表面的 URL,沒有處理到 JSONB 內的巢狀資料和富文本內容。
批次修復
針對不同的資料類型,使用對應的更新語法:
-- JSONB 欄位:需要轉型處理
UPDATE files
SET formats = REPLACE(
formats::text,
'prod-s3-singapore.s3.ap-southeast-1.amazonaws.com',
'prod-s3-taipei.s3.ap-east-2.amazonaws.com'
)::jsonb
WHERE formats::text LIKE '%ap-southeast-1%';
-- UPDATE 419
-- TEXT 欄位:直接替換
UPDATE layout_d_singles
SET content = REPLACE(
content,
'prod-s3-singapore.s3.ap-southeast-1.amazonaws.com',
'prod-s3-taipei.s3.ap-east-2.amazonaws.com'
)
WHERE content LIKE '%ap-southeast-1%';
-- UPDATE 12
重啟服務清除快取:
kubectl rollout restart deployment/strapi-prod
kubectl rollout restart deployment/web-prod
全部功能恢復正常。
Strapi 遷移檢查清單
基於這次經驗,整理出 Strapi 專案遷移時需要檢查的 URL 位置:
-- 1. 主要 URL
SELECT COUNT(*) FROM files WHERE url LIKE '%舊網域%';
-- 2. 縮圖 URL(JSONB)
SELECT COUNT(*) FROM files WHERE formats::text LIKE '%舊網域%';
-- 3. 富文本內容(各種 layout 表)
SELECT COUNT(*) FROM layout_a_singles WHERE content LIKE '%舊網域%';
SELECT COUNT(*) FROM layout_b_singles WHERE block_1 LIKE '%舊網域%';
SELECT COUNT(*) FROM layout_c_singles WHERE content LIKE '%舊網域%';
SELECT COUNT(*) FROM layout_d_singles WHERE content LIKE '%舊網域%';
-- 4. 搜尋索引
SELECT COUNT(*) FROM layout_d_singles WHERE content_searchable LIKE '%舊網域%';
遷移的正確順序
下次遷移時,記得按這個順序:
- 確認新區域資料完整
- 搜尋並更新所有舊 URL ← 這步最容易漏掉
- 測試所有功能正常
- 最後才刪除舊資源
總結
這次技術債清理的收穫:
- 每月省下 $90:清掉遺留的 NAT Gateway、LB、EIP
- 完成遷移收尾:修復 500+ 筆遺漏的 URL
- 建立檢查清單:未來 Strapi 遷移有標準流程
接手專案時,帳單是最好的健檢報告。有疑慮就自己查,不要只靠口頭確認。
