- date
- entry
- 023
- topic
- tooling
- rev
- —
你的防護措施從來沒有被觸發過
部署腳本開頭那道身分閘門,只有在身分錯的時候才會執行——而那條路徑,平常沒有人走。昨天我故意用錯的身分跑了一次,兩分鐘內發現兩個問題。
我在前一篇談 AWS 帳號時建議過一段閘門,放在任何會改動東西的腳本開頭:
EXPECTED_ACCOUNT=111111111111
ACTUAL=$(aws sts get-caller-identity --query Account --output text)
if [ "$ACTUAL" != "$EXPECTED_ACCOUNT" ]; then
echo "WRONG ACCOUNT: $ACTUAL" >&2
exit 1
fi
這段程式碼在我的部署腳本裡跑了很多次,每次都通過。它看起來運作良好——但「通過」是它唯一被驗證過的行為。
問題一:它放行 root
帳號 ID 識別的是帳號,不是身分。而 root 使用者、每一個 IAM user、每一個 role,都屬於同一個帳號。
$ aws sts get-caller-identity
{
"Account": "060795942439",
"Arn": "arn:aws:iam::060795942439:root"
}
Account 完全相符,閘門放行。也就是說:那段用來確保「不要跑在錯的地方」的檢查,對「用權限過大的身分執行」這件事完全沒有意見。
要擋住身分而不只是帳號,比對的對象要換成 ARN:
EXPECTED_ARN="arn:aws:iam::060795942439:user/deploy"
ARN=$(aws sts get-caller-identity --query Arn --output text) || exit 1
[ "$ARN" = "$EXPECTED_ARN" ] || { echo "身分不符: $ARN" >&2; exit 1; }
差別只有一個查詢欄位。但比對帳號時,一個誤用 root 的部署會安靜地成功;比對 ARN 時,它會停下來。
問題二:錯誤訊息自己爆掉
第二個問題更荒謬。我改寫後的訊息長這樣:
echo " 預期 $EXPECTED_ARN(profile: $AWS_PROFILE)。中止。" >&2
實際跑出來是:
ERROR: 目前身分是 arn:aws:iam::060795942439:root
./scripts/deploy.sh: line 29: EXPECTED_ARN?: unbound variable
全形括號 ( 緊接在變數名後面,bash 沒有把它當成變數名的結束符,於是去找一個叫
EXPECTED_ARN( 的變數。找不到,而腳本開著
set -u,於是整行變成 unbound variable 錯誤。
修法是用大括號界定:${EXPECTED_ARN}(profile: ${AWS_PROFILE})。半形括號不會有這個問題,所以中文訊息才踩得到。
結果上這次沒有釀成災難——閘門仍然擋下了執行,只是退出的方式是 bash 的錯誤而不是我寫的說明。但這正是重點:那一行只在「閘門要擋人」的時候執行,而在那之前,它從來沒有被執行過。
只在失敗時執行的程式碼,可以壞掉好幾年而沒有任何症狀——因為它的症狀只在你最需要它的那一刻出現。
負面測試就是故意做錯一次
找出這兩件事沒有用到任何工具,就是用錯的身分跑一次:
AWS_PROFILE=default ./scripts/deploy.sh
驗收標準有兩條,兩條都要滿足:
- 它有擋下來——沒有任何東西被寫到線上
- 它是用你寫的那句話擋下來的——不是 bash 的錯誤、不是 stack trace、不是空白
第 2 條是多數人會跳過的。看到「有擋住」就滿意了,但如果擋住的方式是崩潰,那麼下一個踩到的人拿到的是一則看不懂的訊息,而不是「你用錯身分了,該用哪個」。
還有哪些只在失敗時執行的程式碼
同一類的東西比想像中多。我會逐一實際觸發看看的是:
- 錯誤處理分支——尤其是「這裡理論上不會發生」的那些
- 告警通知——alert 規則設好了,但那條 webhook 真的送得出去嗎?收件者還在職嗎?
- rollback 路徑——上線流程演練過很多次,退版流程通常一次都沒有
- 404 與錯誤頁——它回的狀態碼對嗎
- 備份還原
最後一條值得單獨說:沒有還原過的備份不算備份,只是一個佔空間的檔案。備份腳本每天成功執行、每天寫出檔案、監控一片綠燈,這些都不構成「資料救得回來」的證據。唯一構成證據的是真的還原過一次。
做內部系統導入時,我把「還原演練」寫進交付項目而不是留在維運建議裡。理由很現實:只要它不是交付項目,它就永遠排在下一季。
成本很低,所以沒有藉口
這次的負面測試花了大約兩分鐘:改一個環境變數、跑一次、看四行輸出。而它找出的兩個問題,其中一個讓整道閘門形同虛設。
把它變成一個習慣不需要框架——寫完任何一段「出事時會擋下來」的程式碼,當下就讓它出事一次。那是它離被驗證最近的一刻,之後你就會忘記自己有寫過這段。
如果只記得一件事
檢查本身也需要被檢查。 一個從來沒有回報過問題的檢查,有兩種可能:一切都好,或者它壞了。這兩種在儀表板上長得一模一樣,而分辨它們的唯一方法,是故意製造一次它應該要抓到的問題。
修訂紀錄
- 首次發布