包包baobaolin.com
date
entry
026
topic
infrastructure
rev
—

AWS CLI 簡寫語法裡的逗號:運氣好報錯,運氣不好拆成兩個值

AWS CLI 的簡寫語法用逗號分隔,而值裡也可能有逗號。參數是字串時它會報錯;參數是清單時,它把值拆成兩個照樣送出,沒有任何警告。

AWS CLI 有不少結構型參數可以用兩種寫法:JSON,或它自己的簡寫語法(shorthand),例如 Key=value,Key2=value2。簡寫好打,AWS 文件裡也有很多範例用它。

但簡寫用逗號分隔,而值本身也可能含有逗號。這篇要談的是逗號沒加保護、落在值裡面時會發生什麼。我測到兩種結果:欄位型別是字串時,CLI 報錯、什麼都不送;欄位型別是清單時,它把值拆成兩個,照樣準備送出,沒有任何針對拆值的錯誤或警告。依序回答四件事:簡寫怎麼解析、會報錯的情況、不會報錯的情況、以及怎麼在不碰任何 AWS 帳號的前提下重現。

簡寫是怎麼解析的

AWS CLI 使用者指南〈Using shorthand syntax in the AWS CLI〉的定義是:結構參數的格式是「以逗號分隔的 key-value 清單」。頂層的清單參數(例如 --instance-ids id1 id2)用空白分隔;結構裡面的清單欄位,像本文後面的 Values=a,b,則用逗號分隔。指南也提醒簡寫本身是字串,要照所在終端機的規則加引號與跳脫。

也就是說,有兩層解析:先是 shell 拆參數,再是 CLI 依簡寫語法找分隔符號。shell 的引號在第一層就被用掉了。'Comment=a, b' 用單引號包起來,shell 不會動它,但 CLI 拿到的字串裡仍然有一個沒受保護的逗號。

情況一:欄位是字串,CLI 報錯

我的 taiwanrep.com 部署腳本會用 update-function 更新一支 CloudFront Function,--function-config 需要一段說明文字(Comment)和 runtime。下面用同樣吃 FunctionConfig 的 create-function 重現,假設說明裡放了逗號:

aws cloudfront create-function --name t --function-code fileb://fn.js \
  --function-config 'Comment=www to apex, directory index,Runtime=cloudfront-js-2.0'
aws: [ERROR]: An error occurred (ParamValidation): Parameter validation failed:
Invalid type for parameter FunctionConfig.Comment, value: ['www to apex', 'directory index'], type: <class 'list'>, valid types: <class 'str'>

結束碼是 252。

錯誤訊息把解析結果印出來了:Comment 變成 ['www to apex', 'directory index'],一個兩元素的清單。簡寫的語法解析本身成功了,失敗的是後面的型別驗證:Comment 只收字串,請求根本沒有送出。這是好的情況。

情況二:欄位本來就是清單,CLI 照送

換一個本來就接受多個值的欄位。ec2 describe-instances --filters 的 Values 是字串清單。假設有一個標籤值是 R&D, Taipei:

aws ec2 describe-instances --filters 'Name=tag:Team,Values=R&D, Taipei'

開 --debug 看它準備送出的請求參數:

'Filter.1.Name': 'tag:Team', 'Filter.1.Value.1': 'R&D', 'Filter.1.Value.2': 'Taipei'

一個值變成兩個,逗號後面的空白也被吃掉了。CLI 沒有針對這次拆值產生任何錯誤或警告(最後那個錯誤是連不上本機 endpoint,下一節說明)。describe-instances 的 CLI 參考文件寫明,同一個篩選器的多個值以 OR 連接(官方範例 Values=t2.micro,t3.micro 正是靠逗號分隔)。照這個規則,這個查詢比對的是標籤為 R&D 或 Taipei 的機器,標籤真的是 R&D, Taipei 的那台反而對不上(這是依文件推得,我沒有對真實帳號執行)。同樣的拆法如果發生在寫入類指令的清單欄位上,請求裡一樣會多出一個值,服務端怎麼處理取決於那支 API,我沒有驗證。

型別驗證擋住了第一個錯。第二個錯剛好型別正確,CLI 這一層不會擋。

不碰 AWS 也能重現

上面的結果都是在沒有任何真實憑證、也沒有連到 AWS 的情況下跑出來的。做法是用 env -i 清掉繼承的環境變數,憑證用 AWS 文件裡的範例金鑰,讓 CLI 讀不到我自己的設定檔,並把 endpoint 指到本機一個連不上的 port:

env -i PATH="$PATH" HOME="$HOME" \
  AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null \
  AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE \
  AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
  AWS_DEFAULT_REGION=us-east-1 AWS_MAX_ATTEMPTS=1 \
  aws ec2 describe-instances --filters 'Name=tag:Team,Values=R&D, Taipei' \
  --endpoint-url http://127.0.0.1:9 --debug

--debug 的輸出很長,我是接 2>&1 | grep -o "'body': {[^}]*}" 只取參數那段;要看結束碼時就不要接 pipe,否則 $? 會是 grep 的。

型別驗證失敗的會停在 ParamValidation,結束碼 252。通過驗證的會走到傳送那一步,然後得到 Could not connect to the endpoint URL: "http://127.0.0.1:9/...",結束碼 255。所以在這個設定裡,「連線失敗」代表參數已經通過 CLI 這一層;--debug 的輸出裡可以看到它原本要送的參數。

環境是 aws-cli 2.34.23。

修法:寫進腳本的一律用 JSON

同一個參數,三種寫法都能讓 Comment 保持一個字串(都用上面的方法實測過):

# 1. JSON,用單引號包住
--function-config '{"Comment":"www to apex, directory index","Runtime":"cloudfront-js-2.0"}'

# 2. 簡寫裡替值加雙引號,外面再包單引號
--function-config 'Comment="www to apex, directory index",Runtime=cloudfront-js-2.0'

# 3. 從檔案讀
--function-config file://function-config.json

EC2 的篩選器也一樣,JSON 寫法送出去是一個值:

--filters '[{"Name":"tag:Team","Values":["R&D, Taipei"]}]'
# 'Filter.1.Name': 'tag:Team', 'Filter.1.Value.1': 'R&D, Taipei'

第 2 種能用,--debug 看到的 body 是 <Comment>www to apex, directory index</Comment>;在簡寫裡用反斜線跳脫逗號(Values=R&D\, Taipei)我也測到會保留成一個值。但這兩種都要求讀的人同時記得兩層引號或跳脫各自保護什麼,我在上面那份使用者指南頁面裡也沒有找到這兩種寫法的說明。taiwanrep.com 的 deploy.sh 用的是第 1 種:

--function-config '{"Comment":"www to apex 301 and directory index","Runtime":"cloudfront-js-2.0"}'

我的分法:在終端機裡打一次就丟的指令,用簡寫無妨;會進版控、會被別人改的腳本,結構參數一律寫 JSON。在 JSON 裡,字串裡面的逗號就是字串內容,不會被當成分隔符號。

如果只記得一件事

簡寫裡沒有受保護的逗號都是分隔符號,不管你心裡把它當成什麼。回到開頭的兩種結果:報錯的那種其實是運氣好。真正要防的是型別剛好對上的情況,而最直接能看到它的地方,是 --debug 裡那份準備送出的參數。

修訂紀錄

  1. 首次發布