包包baobaolin.com
date
entry
036
topic
tooling
rev
—

aws login 的憑證錯誤:修法留著,我寫在 commit 裡的原因和 API 文件對不上

部署腳本連續呼叫 aws 時會間歇失敗。兩天裡我換了好幾個假說,最後的修法我留著,但寫下的原因和 AWS 的 API 文件對不上。

aws login 是 AWS CLI 2.32.0 起的登入方式:用瀏覽器登入主控台,CLI 拿到臨時憑證,這個流程不需要在機器上另存長期的 access key。至少在 2026 年 8 月這段除錯期間,我兩個站的部署腳本都跑在這種憑證上。

但從 8 月 22 日起,commit 開始記錄一種間歇失敗:CreateOAuth2Token 回「authorization grant is invalid, expired, revoked, or malformed」,同一道指令過一陣子手動打又會通。兩天裡我在兩個 repo 留下一串 commit,假說換了好幾次。這篇要談的是這段紀錄裡哪些站得住:假說怎麼演變、AWS 文件怎麼說、哪些是已記錄的、哪些只是推論,以及現在那個修法的代價。

兩天裡換了幾次假說

以下取自兩個 repo 的 git log(時間是 +08:00;8/22 的四列在官網 repo;8/23 的兩列兩個 repo 都做了同樣的改法,表上是另一個站的時間;9/16 的兩列只在另一個站;「假說」是 commit 當時寫的解釋):

時間當時的假說改了什麼
8/22 10:48和 token 更新撞在一起的競態身分確認最多試 3 次,失敗間隔 3 秒
8/22 11:43三次都失敗,等待不夠久最多 5 次,等待 3 秒起倍增,合計 45 秒
8/22 14:21失敗成群出現,是一段「時間窗」最多 6 次,等待 10 秒起倍增
8/22 17:07同上註解加上「先手動確認能通再部署」
8/23 14:57rotating refresh token:每次呼叫用掉舊的 grant拿掉重試,改用 export-credentials 匯出一次
8/23 19:45換發那一次本身會失敗,新 token 已寫回取憑證失敗時立刻再試一次
9/16 14:18換發需要一點時間寫回取憑證最多 4 次、間隔 3 秒(只有其中一個站)
9/16 16:13同上取憑證前先打一次唯讀 API(同一個站)

轉向發生在 8 月 23 日下午。另一個站的 commit 記錄:五個上傳各試五次,25 次全部失敗;之後一個一個單獨跑,每個都成功(那支腳本有 set -e,這 25 次是怎麼跑出來的,紀錄沒有交代)。官網那支同一天早上也是,commit 寫重試迴圈花了四分鐘、失敗六次,整支腳本重跑一次就過了(照程式碼,六次之間的等待合計至少 310 秒,四分鐘是當時的錯算)。commit 的解釋是:aws login 的 refresh token 會輪替,每次呼叫用掉舊的 grant、換發新的,連續呼叫時後面那支還拿著已經被用掉的 grant;重試沒用,因為每次重試拿的都是同一張死掉的 grant。

修法是在部署一開始呼叫一次 aws configure export-credentials --format env,把三個環境變數匯出,之後每支 aws 指令直接用這組固定的憑證。commit 記錄改完之後端對端跑過一次,第一次就通。

AWS 文件寫的不是這樣

寫這篇時我回去讀了兩份文件。AWS CLI 使用者指南〈Login for AWS local development using console credentials〉寫:

  • CLI 和 SDK 會「every 15 minutes as needed」自動更新快取的憑證;整個 session 最長到 IAM 主體設定的時間,上限 12 小時。
  • 臨時憑證和更新它們需要的資料,預設存在 ~/.aws/login/cache。

CreateOAuth2Token 的 API 參考(AWS CLI 參考的 signin create-o-auth2-token)寫得更直接:用授權碼換 token 不是冪等的,每個授權碼只能用一次;用 refresh token 更新是「Idempotent within token validity window」,同一個 refresh token 在有效期內呼叫多次,回的結果一致。回應裡的 access token 是 15 分鐘的臨時憑證,expiresIn 最大 900 秒。

兩句話各打掉我那個解釋的一半。快取憑證還能用的時候,aws 指令不需要去換 token,談不上「每次呼叫用掉一張 grant」;refresh 在有效期內是冪等的,「已經用過一次」本身就不能解釋第二支為什麼被拒。我寫在 commit 和腳本註解裡的機制,文件不支持。

已記錄、推論、不知道

把這段紀錄拆開:

  • 已記錄:CLI 回報失敗的操作是 CreateOAuth2Token;曾觀察到失敗成批出現、隔一陣子單獨打會通;8 月 23 日改成匯出一次之後,兩支腳本各有一次端對端成功的紀錄;9 月 16 日其中一個站又加了延遲重試,之後再加上先打一次唯讀 API,註解記錄加了之後一次就通,沒有量過成功率。
  • 推論:失敗可能和快取憑證接近到期、需要更新的時刻有關,而不是每一次呼叫;匯出之後部署期間不再交換 token,這可能是修法有效的原因,但沒有證實。9 月那條「換發需要一點時間寫回」也屬於這一類,沒有量過。
  • 不知道:伺服器到底拒絕了什麼。那句錯誤訊息把 invalid、expired、revoked、malformed 全列出來,從字串選不出是哪一個。是有效期已過、快取讀到一半、還是別的原因,我手上的紀錄沒有失敗當下的除錯輸出可以判斷。API 參考還提到 refresh token 帶 cnf.jkt(綁定一把金鑰的指紋),它和失敗有沒有關係,我也不知道。

下一次失敗,我要留的是那一刻的完整指令加 --debug(例如 aws --debug sts get-caller-identity)、CLI 版本、時間和快取憑證的到期時間,輸出只留請求與錯誤那幾行,token 和簽章先遮掉;而不是再寫一個新的解釋。

匯出一次的代價

修法我留著,因為不管原因是哪一個,它都讓部署期間不必再交換 token。但它有一個代價,原本的註解沒寫:匯出的是當下那一組臨時憑證,API 參考寫這個 login 流程簽發的憑證最長 900 秒,而匯出時剩下的時間可能更短。腳本裡已經印出 AWS_CREDENTIAL_EXPIRATION,但只印、不檢查。

這不只是理論。官網 9 月 10 日那個 commit 記錄,8 月時 CI 的 Sync to S3 步驟要 782 秒,因為要對 293 個子目錄各打兩次 put-object;同一個迴圈也讓 9 月 10 日的本機部署在中途遇到憑證過期而中斷。那個 commit 把目錄索引移到 CloudFront Function 之後,整支本機部署(含 build 與檢查)量到 64 秒。export-credentials 在快到期時會不會先換一組新的再匯出,文件沒寫,我也沒測過。下面是我打算加進腳本的守門,目前還沒合進去:

unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN AWS_CREDENTIAL_EXPIRATION
creds=$(aws configure export-credentials --format env) || {
  echo "取不到憑證,先執行 aws login" >&2; exit 1; }
eval "$creds"
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_CREDENTIAL_EXPIRATION
python3 - <<'PY' || exit 1
import os, sys, datetime as dt
exp = os.environ.get("AWS_CREDENTIAL_EXPIRATION")
if not exp:
    sys.exit("沒有到期時間,無法確認憑證夠用")
left = (dt.datetime.fromisoformat(exp.replace("Z", "+00:00"))
        - dt.datetime.now(dt.timezone.utc)).total_seconds()
print(f"憑證剩 {left:.0f} 秒")
if left < 300:
    sys.exit("憑證剩不到 300 秒,重新取得後再跑")
PY

先 unset(包括舊式的 AWS_SECURITY_TOKEN)是因為環境裡如果留著舊的憑證變數,CLI 會優先用它們,守門就會看著舊的到期時間放行;取憑證失敗時 $(…) 的結束碼要先接住,否則 eval "" 會照樣成功。300 秒是我依那次 64 秒的量測抓的餘裕,部署變長就要跟著調。這段我用一個假的 aws 指令測過三種情況(剩 12 分鐘放行、剩 2 分鐘擋下、取憑證失敗擋下),沒有在真正的失敗當下跑過。

如果只記得一件事

修法能用,不代表寫下的原因是對的。開頭那個錯誤,兩天裡的 commit 換了好幾個假說,最後一個寫得最篤定,也最經不起文件檢查。commit 訊息記錄的是我當時相信什麼;腳本裡那幾段註解應該改標成「推論」、附上這兩份文件;修法本身留著,再加上檢查到期時間的那幾行。

修訂紀錄

  1. 首次發布