AWSのAPIリクエストをGoから実行する


# 
GoからAWSの各種操作をしようとする際、対応する公式のSDKがないので自前でAPIを叩いて行う必要があります。
Goの勉強がてら東京リージョンのec2インスタンスの一覧を取得するコードを書いてみました。

基本的にAPIの公式ドキュメントを参照していますが、
それ以外にvagrantなどの作者としてもおなじみのmitchelhさん作成のawsライブラリの実装を参考にしました。
https://github.com/mitchellh/goamz


##APIのリクエスト構造
###エンドポイント
使用するリージョンにあわせて選択します。
今回はec2の東京リージョンなので、`https://ec2.ap-northeast-1.amazonaws.com/`を使用します。

エンドポイント一覧：http://docs.aws.amazon.com/general/latest/gr/rande.html#emr_region

###クエリパラメータ
クエリパラメータには実行するすべてのリクエストで共通の必須パラメータと実施する操作固有のパラメータを指定します。

#### 共通パラメータ

すべてのリクエストで共通のパラメータです。アクションの指定とクエリ認証が主です。
認証方式にはSignatureVersion2とSignatureVersion4があるらしいのですが、以降はクエリ認証にSignatureVersion2を使用する前提で進みます。
（SignatureVersion4はあまり調べてない。。）

以下、指定必須のパラメータです。

* **Action**: AWSに対する操作を指定。今回は`DescribeInstance`を使用。
アクション一覧：http://docs.aws.amazon.com/AWSEC2/latest/APIReference/query-apis.html

* **Version**: APIバージョン。

* **AWSAccessKeyId**: AccessKeyIDを指定。

* **Expires**: シグネチャの期限（たぶん・・）。ExpiresかTimestampのいずれかは必須。

* **Timestamp**: シグネチャ作成日時（たぶん・・）。ExpiresかTimestampのいずれかは必須。

* **Signature**: シグネチャ。作成方法は後述。

* **SignatureMethod**: シグネチャ作成に使用するアルゴリズム。HmacSHA256かHmacSHA1を指定。

* **SignatureVersion**: 認証に使うバージョン。2を指定。

参考：http://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Common-Parameters.html

#### 固有パラメータ

各操作ごとに必要なものがあれば指定します。



## シグネチャの生成方法

HmacSHA256の場合、Canonical文字列を生成しシークレットキーと一緒にハッシュ値を計算し、base64エンコードします。

Canonical文字列は以下のルールで生成します。
`リクエストメソッド + \n + ホスト名 + \n + パス + \n + クエリパラメータ`
クエリパラメータはキーの辞書順で&で結合されている必要があり、かつ各パラメータがURLエンコードされていなければなりません。

```text:canonical-string
GET\n
ec2.ap-northeast-1.amazonaws.com\n
/\n
AWSAccessKeyId=AccessKeyId&Action=DescribeInstances&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2014-11-01T15%3A19%3A30&Version=2014-09-01
```

## コード

ここまでの流れを踏まえたサンプルコードとなります。

```go:サンプル
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"time"
)

func main() {

	host := "https://ec2.ap-northeast-1.amazonaws.com/"

	accessKey := "Access Key ID"     // アクセスキー
	secretKey := "Secret Access Key" // シークレットキー

	// Signature以外の必須共通パラメータ
	params := map[string]string{
		"Action":           "DescribeInstances",
		"Version":          "2014-09-01",
		"AWSAccessKeyId":   accessKey,
		"Timestamp":        time.Now().UTC().Format(time.RFC3339),
		"SignatureVersion": "2",
		"SignatureMethod":  "HmacSHA256",
	}

	// Action固有のパラメータがある場合はシグネチャ生成前に設定
	params["MaxResults"] = "5"

	endpoint, err := url.Parse(host)
	if err != nil {
		log.Fatal(err)
	}

    // シグネチャ生成
	signature := getSignature(secretKey, endpoint.Host, params)

	params["Signature"] = signature

	// 
	endpoint.RawQuery = joinParams(params)

	// リクエスト
	resp, err := http.Get(endpoint.String())
	if err != nil {
		log.Fatal(err)
	}

	body, _ := ioutil.ReadAll(resp.Body)
	defer resp.Body.Close()

	// 結果を出力
	fmt.Println(string(body))

}

// URLエンコードされたパラメータを生成する
func joinParams(params map[string]string) string {
	p := make(url.Values, len(params))
	for k, v := range params {
		p[k] = []string{v}
	}
	return p.Encode()
}

// シグネチャを生成し取得する
func getSignature(secretKey string, host string, params map[string]string) string {

	payload := "GET\n" + host + "\n" + "/\n" + joinParams(params)

	hash := hmac.New(sha256.New, []byte(secretKey))
	hash.Write([]byte(payload))
	signature := make([]byte, base64.StdEncoding.EncodedLen(hash.Size()))
	base64.StdEncoding.Encode(signature, hash.Sum(nil))

	return string(signature)
}

```

上記を実行すると、以下のようなXML形式の結果が得られます。

```xml:結果（サンプル）
<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2014-09-01/">
  <requestId>fdcdcab1-ae5c-489e-9c33-4637c5dda355</requestId>
    <reservationSet>
      <item>
        <reservationId>r-1a2b3c4d</reservationId>
        <ownerId>111122223333</ownerId>
        <groupSet>
          <item>
            <groupId>sg-1a2b3c4d</groupId>
            <groupName>my-security-group</groupName>
          </item>
        </groupSet>
        <instancesSet>
          <item>
            <instanceId>i-1a2b3c4d</instanceId>
            <imageId>ami-1a2b3c4d</imageId>
            <instanceState>
              <code>16</code>
              <name>running</name>
            </instanceState>
            <privateDnsName/>
            <dnsName/>
            <reason/>
            <keyName>my-key-pair</keyName>
            <amiLaunchIndex>0</amiLaunchIndex>
            <productCodes/>
            <instanceType>c1.medium</instanceType>
            <launchTime>YYYY-MM-DDTHH:MM:SS+0000</launchTime>
            <placement>
              <availabilityZone>us-west-2a</availabilityZone>
              <groupName/>
              <tenancy>default</tenancy>
            </placement>
            <platform>windows</platform>
            <monitoring>
              <state>disabled</state>
            </monitoring>
            <subnetId>subnet-1a2b3c4d</subnetId>
            <vpcId>vpc-1a2b3c4d</vpcId>
            <privateIpAddress>10.0.0.12</privateIpAddress>
            <ipAddress>46.51.219.63</ipAddress>
            <sourceDestCheck>true</sourceDestCheck>
            <groupSet>
              <item>
                <groupId>sg-1a2b3c4d</groupId>
                <groupName>my-security-group</groupName>
              </item>
            </groupSet>
            <architecture>x86_64</architecture>
            <rootDeviceType>ebs</rootDeviceType>
            <rootDeviceName>/dev/sda1</rootDeviceName>
            <blockDeviceMapping>
              <item>
                <deviceName>/dev/sda1</deviceName>
                <ebs>
                  <volumeId>vol-1a2b3c4d</volumeId>
                  <status>attached</status>
                  <attachTime>YYYY-MM-DDTHH:MM:SS.SSSZ</attachTime>
                  <deleteOnTermination>true</deleteOnTermination>
                </ebs>
              </item>
            </blockDeviceMapping>
            <virtualizationType>hvm</virtualizationType>
            <clientToken>ABCDE1234567890123</clientToken>
            <tagSet>
              <item>
                <key>Name</key>
                <value>Windows Instance</value>
              </item>
            </tagSet>
            <hypervisor>xen</hypervisor>
            <networkInterfaceSet>
              <item>
                <networkInterfaceId>eni-1a2b3c4d</networkInterfaceId>
                <subnetId>subnet-1a2b3c4d</subnetId>
                <vpcId>vpc-1a2b3c4d</vpcId>
                <description>Primary network interface</description>
                <ownerId>111122223333</ownerId>
                <status>in-use</status>
                <macAddress>1b:2b:3c:4d:5e:6f</macAddress>
                <privateIpAddress>10.0.0.12</privateIpAddress>
                <sourceDestCheck>true</sourceDestCheck>
                <groupSet>
                  <item>
                    <groupId>sg-1a2b3c4d</groupId>
                    <groupName>my-security-group</groupName>
                  </item>
                </groupSet>
                <attachment>
                  <attachmentId>eni-attach-1a2b3c4d</attachmentId>
                  <deviceIndex>0</deviceIndex>
                  <status>attached</status>
                  <attachTime>YYYY-MM-DDTHH:MM:SS+0000</attachTime>
                  <deleteOnTermination>true</deleteOnTermination>
                </attachment>
                <association>
                  <publicIp>198.51.100.63</publicIp>
                  <ipOwnerId>111122223333</ipOwnerId>
                </association>
                <privateIpAddressesSet>
                  <item>
                    <privateIpAddress>10.0.0.12</privateIpAddress>
                    <primary>true</primary>
                    <association>
                      <publicIp>198.51.100.63</publicIp>
                      <ipOwnerId>111122223333</ipOwnerId>
                    </association>
                  </item>
                  <item>
                    <privateIpAddress>10.0.0.14</privateIpAddress>
                    <primary>false</primary>
                    <association>
                      <publicIp>198.51.100.177</publicIp>
                      <ipOwnerId>111122223333</ipOwnerId>
                    </association>
                  </item>
                </privateIpAddressesSet>
              </item>
            </networkInterfaceSet>
          </item>
        </instancesSet>
      </item>
    </reservationSet>  
</DescribeInstancesResponse>
```

以上です。
APIの仕様の詳細については正直理解があいまいなので、間違いがあるかもしれませんが、実行はできるかと思います。

結果はXMLで返ってくるのでパースして構造体にマッピングする必要がありますが、
ネストが深く構造体を作るのが大変だったので、XMLからGoの構造体を出力するツールを作成しました。
後日公開します。