もしもAWSのキーは分かっているけどcurlしか使えない状況でEC2をStopする必要が出た時のために

# :information_source: タイトルについて

妄想ですが、
何らかの理由によりEC2をStopさせたいにも関わらず、
Webコンソールにアクセスできず、
AWS CLI とか AWS SDK とかインストールできず、
サポートに電話することもままらない状況だけど、
運良く AWS_ACCESS_KEY と AWS_SECRET_ACCESS_KEY は知っていて、
たまたま `curl`（と他に必要なコマンド群）は使える環境であれば何とかできるTipsです。

~~普通、そんな状況にはならない。~~

# :watermelon: 少しまじめに

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` なりでやれば良いと思いますが...

# :notebook: はじめに
 
AWS(Amazon Web Service)はリソースを操作するための API を公開しています。
その API を叩くにはもちろん認証が必要で、その認証のために署名を計算してクエリーにくっつける必要があります。
その辺の処理は普通、AWS CLI なり AWS SDK なりがやってくれるので、
私達は AWS_ACCESS_KEY と AWS_SECRET_ACCESS_KEY を環境変数なり `~/.aws/credentials` に書いておくだけで済みます。

が、本記事では、その署名の計算からbashで行い、curlでAWSのAPIを直で叩いて操作してみよう、という内容です。
N番煎じな気がしますが書いてみました。

# :warning: 注意

AWSの署名には2種類あり[^1]、今回は古い方（Version2[^2]）を使用しています。
[^1]:  http://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-2.html
[^2]: http://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-2.html
この方法では、AWS API を叩いて操作できる内容に制限があるようです。

# :computer: 環境

- Mac OS X El Capitan
	- bash 4.3

## 使用するコマンド

- `curl`
- `openssl`
- `base64`


# やってみる

スクリプト全体はページ最後に載せています。

## AWS_ACCESS_KEY と AWS_SECRET_ACCESS_KEY の読み込み

私の場合、 `~/.aws./credentials` に書いているので

```bash
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]
[^3]: http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/APIReference/API_Operations.html


## 署名を作る

署名Version2は、APIのリクエスト内容をハッシュ化し、Base64でエンコードすることで求められます。

### APIのリクエスト内容とは

ここで言う、APIのリクエスト内容とは、

- リクエストメソッド
- リクエスト先ドメイン名
- リクエスト先パス
- リクエストクエリー

です。
これらの情報をまるっと集めてハッシュ化すれば良いわけです。

```bash
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')
```

リクエストクエリーだけ何やらしてますが、
実はハッシュ化に使用するリクエストクエリーにはルールがあって、

1. キー名のバイト順でソートされている事
2. URIエンコードされていること

となります。
最後に、 `body=$(echo -n "${body[@]}" | tr ' ' '\n')` として、改行が区切り文字になるようにして元のデータは完成です。

以下、リクエストクエリー部分のシェルを簡単に説明。

### バイト順でソート

```bash
    # とりあえずクエリー全部表示
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]

```bash
$ 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
```

[^4]: 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


### 署名を求める

まずは`openssl`を利用してハッシュ化します。
ここで、事前に読み込んでおいた AWS_SECRET_ACCESS_KEY を使用します。

```bash
echo -n ${body} | openssl dgst -sha256 -binary -hmac "${sk}"
```

`-n`で末尾に改行が入らないようにしています。
あとは base64 でエンコード。

```bash
echo -n ${body} | openssl dgst -sha256 -binary -hmac "${sk}" | base64
```

おしまいです。

署名は、`Sigunature`というキー名でクエリーに付与するので、`query`に追加しておきましょう。

```bash
query+=(
  "Signature=$(echo -n "${body}" | openssl dgst -sha256 -binary -hmac "${sk}" | base64)"
)
```

## curlでリクエストする

いよいよです。

```bash
curl -s -G 'https://ec2.amazonaws.com/' $(echo ${query[@]} | xargs -n1 | sort | sed 's/.*/--data-urlencode "&"/g' | xargs)
```

## 補足

このままだと１つ問題があって、 `query` の value に空白を含むとうまくいきません。

```bash
$ 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` を使う方法しかわかりませんでした。。。

```bash
$ 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>
```

# まとめ

というわけでスクリプトの全体像です。

```bash
#!/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

```