タイトルについて
妄想ですが、
何らかの理由によりEC2をStopさせたいにも関わらず、
Webコンソールにアクセスできず、
AWS CLI とか AWS SDK とかインストールできず、
サポートに電話することもままらない状況だけど、
運良く AWS_ACCESS_KEY と AWS_SECRET_ACCESS_KEY は知っていて、
たまたま curl
(と他に必要なコマンド群)は使える環境であれば何とかできるTipsです。
普通、そんな状況にはならない。
少しまじめに
AWS CLI や AWS SDK を使わないケース自体は↓のようなものがあります
- 使用する必要のある機能が、AWS SDK または CLI でサポートされていないため。これは一般的ではありませんが、新しいサービス機能がリリースされてから、すべての AWS SDK でその機能がサポートされるまで、多少時間がかかることが 1 つのシナリオです。
- AWS へのリクエストの送信方法や返される応答を完全に制御する必要があるため。
http://docs.aws.amazon.com/ja_jp/general/latest/gr/signing_aws_api_requests.html
とはいえ、それはそれで curl
を使わず Python
なり Ruby
なりでやれば良いと思いますが...
はじめに
AWS(Amazon Web Service)はリソースを操作するための API を公開しています。
その API を叩くにはもちろん認証が必要で、その認証のために署名を計算してクエリーにくっつける必要があります。
その辺の処理は普通、AWS CLI なり AWS SDK なりがやってくれるので、
私達は AWS_ACCESS_KEY と AWS_SECRET_ACCESS_KEY を環境変数なり ~/.aws/credentials
に書いておくだけで済みます。
が、本記事では、その署名の計算からbashで行い、curlでAWSのAPIを直で叩いて操作してみよう、という内容です。
N番煎じな気がしますが書いてみました。
注意
AWSの署名には2種類あり1、今回は古い方(Version22)を使用しています。
環境
- Mac OS X El Capitan
- bash 4.3
使用するコマンド
curl
openssl
base64
やってみる
スクリプト全体はページ最後に載せています。
AWS_ACCESS_KEY と AWS_SECRET_ACCESS_KEY の読み込み
私の場合、 ~/.aws./credentials
に書いているので
ak="$(cat ~/.aws/credentials | grep 'aws_acc' | cut -d' ' -f3)"
sk="$(cat ~/.aws/credentials | grep 'secret' | cut -d' ' -f3)"
などとして読み込みます。複数のAWSアカウントのキーを持っていることは考慮してません。
環境変数にしている人はそのまま環境変数を使うと簡単でしょう。
APIをリクエストするベースのクエリーを作る
今回はEC2をStopさせたいので↓な感じです。
query=(
"AWSAccessKeyId='${ak}'"
"Action='StopInstances'"
"InstanceId.1='i-xxxxxxxx'"
"SignatureMethod='HmacSHA256'"
"SignatureVersion='2'"
"Timestamp='$(date "+%Y-%m-%dT%T%z")'"
"Version='2015-10-01'"
)
事前に読み込んでおいた AWS_ACCESS_KEY を使用します。
Actionを変えれば、SecurityGroup の一覧を表示することも Tag を付与することもできます。3
署名を作る
署名Version2は、APIのリクエスト内容をハッシュ化し、Base64でエンコードすることで求められます。
APIのリクエスト内容とは
ここで言う、APIのリクエスト内容とは、
- リクエストメソッド
- リクエスト先ドメイン名
- リクエスト先パス
- リクエストクエリー
です。
これらの情報をまるっと集めてハッシュ化すれば良いわけです。
body=(
# リクエストメソッド
"GET"
# リクエスト先ドメイン名
"ec2.amazonaws.com"
# リクエスト先パス
"/"
# リクエストクエリー
"$(curl -s -w '%{url_effective}\n' -G '' $(echo ${query[@]} | xargs -n1 | sort | sed "s/.*/--data-urlencode \"&\"/g" | xargs) | sed 's!^/\?!!')"
)
body=$(echo -n "${body[@]}" | tr ' ' '\n')
リクエストクエリーだけ何やらしてますが、
実はハッシュ化に使用するリクエストクエリーにはルールがあって、
- キー名のバイト順でソートされている事
- URIエンコードされていること
となります。
最後に、 body=$(echo -n "${body[@]}" | tr ' ' '\n')
として、改行が区切り文字になるようにして元のデータは完成です。
以下、リクエストクエリー部分のシェルを簡単に説明。
バイト順でソート
# とりあえずクエリー全部表示
echo ${query[@]} \
| \ # tr ' ' '\n' と似た感じ
xargs -n1 \
| \ # sortする
sort \
| \ # curlで--data-urlencodeオプション使うので用意してあげる
sed 's/.*/--data-urlencode "&"/g' \
| \ # tr '\n' ' ' と似た感じ
xargs
URIエンコード
curl
の-w
と-G
と--data-urlencode
を使って一撃。4
$ curl -s -w '%{url_effective}\n' $(↑のバイト順でソートして --data-urlencode オプション付与するやつ) | sed 's!^/\?!!'
AWSAccessKeyId=xxxxxxxxxx&Action=StopInstances&InstanceId.1=i-xxxxxxxx&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2016-06-20T00%3A07%3A32%2B0900&Version=2015-10-01
署名を求める
まずはopenssl
を利用してハッシュ化します。
ここで、事前に読み込んでおいた AWS_SECRET_ACCESS_KEY を使用します。
echo -n ${body} | openssl dgst -sha256 -binary -hmac "${sk}"
-n
で末尾に改行が入らないようにしています。
あとは base64 でエンコード。
echo -n ${body} | openssl dgst -sha256 -binary -hmac "${sk}" | base64
おしまいです。
署名は、Sigunature
というキー名でクエリーに付与するので、query
に追加しておきましょう。
query+=(
"Signature=$(echo -n "${body}" | openssl dgst -sha256 -binary -hmac "${sk}" | base64)"
)
curlでリクエストする
いよいよです。
curl -s -G 'https://ec2.amazonaws.com/' $(echo ${query[@]} | xargs -n1 | sort | sed 's/.*/--data-urlencode "&"/g' | xargs)
補足
このままだと1つ問題があって、 query
の value に空白を含むとうまくいきません。
$ curl -s -G '' -w '%{url_effective}\n' $(echo -n "key=value 1" | sed 's/.*/--data-urlencode "&"/g')
/?"key=value
HTTP://1"/?"key=value
--data-urlencode "key=value 1"
と認識して欲しいのですが、
--data-urlencode key=value 1
となってしまうためうまくいきません。
色々考えましたが、eval
を使う方法しかわかりませんでした。。。
$ eval curl -s -G "''" -w '%{url_effective}\\n' $(echo -n "key=value 1" | sed 's/.*/--data-urlencode "&"/g')
/?key=value%201
実行すると
レスポンスは xml で返ってきます。
<?xml version="1.0" encoding="UTF-8"?>
<StopInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2015-10-01/">
<requestId>9492a5ed-506e-4c2d-9909-36af3f35cd19</requestId>
<instancesSet>
<item>
<instanceId>i-4fc074c9</instanceId>
<currentState>
<code>64</code>
<name>stopping</name>
</currentState>
<previousState>
<code>16</code>
<name>running</name>
</previousState>
</item>
</instancesSet>
</StopInstancesResponse>
まとめ
というわけでスクリプトの全体像です。
#!/usr/bin/env bash
function main() {
local ak="$(cat ~/.aws/credentials | grep 'aws_acc' | cut -d' ' -f3)"
local sk="$(cat ~/.aws/credentials | grep 'secret' | cut -d' ' -f3)"
local domain="ec2.amazonaws.com"
local action="StopInstances"
# API request query
local query=(
"AWSAccessKeyId='${ak}'"
"Action='${action}'"
"SignatureMethod='HmacSHA256'"
"SignatureVersion='2'"
"Timestamp='$(date "+%Y-%m-%dT%T%z")'"
"Version='2015-10-01'"
)
# Additional query
query+=("InstanceId.1='i-4fc074c9'")
local body=(
"GET"
"${domain}"
"/"
"$(eval curl -s -w '%{url_effective}\\n' -G "''" $(echo "${query[@]}" | xargs -n1 | sort | sed 's/.*/--data-urlencode \\"&\\"/g' | xargs) | sed 's!/?!!')"
)
body="$(echo "${body[@]}" | tr ' ' '\n')"
query+=(
"Signature=$(echo -n "${body}" | openssl dgst -sha256 -binary -hmac "${sk}" | base64)"
)
eval curl -s -w '\\n' -G "https://${domain}" $(echo "${query[@]}" | xargs -n1 | sort | sed 's/.*/--data-urlencode \\"&\\"/g' | xargs)
}
main
-
http://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-2.html ↩
-
http://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-2.html
この方法では、AWS API を叩いて操作できる内容に制限があるようです。 ↩ -
http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/APIReference/API_Operations.html ↩
-
http://qiita.com/yasuhiroki/items/a569d3371a66e365316f#url%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B%E3%81%A0%E3%81%91 ↩