Goを触ってみよう【aws-sdk-go】

# まえがき
　もう結構使っている人も多いと思われる**Go言語**ですが、最近になってようやっと触りだしたこのごろです。きっかけは`aws-sdk`で`Python`の[boto3ライブラリ](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/pricing.html#Pricing.Client.get_products)を使ってインスタンス情報を抜き出そうとした時にdict型(辞書型)に翻弄された結果`Go`に逃げただけです。
# やりたいこと
　上述の通り、[Goでaws-sdk](https://github.com/aws/aws-sdk-go)を利用して、インスタンス情報から`Instance-type`、`vCPU`、`Memory`を抜き出してファイルに出力する！です。
背景としてはプロビジョニングツールとしてTerraformを利用しているのですが、DBのCloudwatch metricsをいちいち手入力するのはつらみなので、テンプレートで出せるようにしたかったためです。
<br>
**突然のGopherくん！！！**
![Gopher](https://blog.golang.org/gopher/gopher.png)
&copy;[Renée French](http://reneefrench.blogspot.com/)
ref.) [The Go Gopher](https://blog.golang.org/gopher)

# Goのインストール
　いわずもがな、まずは環境を用意しなければならないです。
個人的にハマったポイントをあらかじめ挙げておきます。

- `$GOPATH`の設定
- `dep` or `Modules` 

### インストール
まずは[公式サイト](https://golang.org/doc/install)よろしく、インストールしましょう。

<details><summary>Windowsの場合はインストーラーをダウンロードしてポチポチ</summary>
https://golang.org/dl/</details>

<details><summary>MacやLinuxその他の場合はコマンドでもOK</summary><div>

```bash:Install.sh
tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz
#Macの場合はbrewでもOK
brew install go
```
</div></details>
Goのインストール完了後は`go get golang.org/dl/go$VERSION`で別バージョンをインストールすることも可能です。
### $GOPATHの設定
結論からいくとどこでもいいらしい…が、おかげで嵌まってしまった。
参考：[GOPATH は適当に決めて問題ない](https://qiita.com/yuku_t/items/c7ab1b1519825cc2c06f)

今のところ個人の範囲でしか使っていないのであまり使わないような気がしていますが、結局 [goenv](https://github.com/syndbg/goenv/blob/master/INSTALL.md) にお任せすることにしました。
* Macの場合`brew install goenv` でもインストールできます。

```bash:Goインストール
$ goenv install --list
Available versions:
  1.2.2
  1.3.0
=======多いので割愛=======
  1.11.3
  1.11.4
  1.12beta1
 #1.12はまだbetaになってました
$ goenv install 1.11.4
Downloading go1.11.4.darwin-amd64.tar.gz...
-> https://dl.google.com/go/go1.11.4.darwin-amd64.tar.gz
Installing Go Darwin 64bit 1.11.4...
Installed Go Darwin 64bit 1.11.4 to /Users/Tetsu/.goenv/versions/1.11.4
```

```bash:確認
$ goenv global 1.11.4
$ go version
go version go1.11.4 darwin/amd64
```
[Release](https://github.com/golang/go/releases)
### とりあえずHello World!
なにはともあれ、`Hello World!`をまずはやってみましょう。

以下のようなファイルを作って、

```go:hello.go
package main
import "fmt"

func main(){
    fmt.Printf("Hello World!\n")
}
```
実行

```bash:go&nbsp;run
$ go run hello.go
Hello World!
```

`build`してバイナリにしちゃう。

```bash:go&nbsp;build
$ go build hello.go
$ ls -l hello
-rwxr-xr-x  1 User  staff  2020040  5 25 16:09 hello
$ ./hello
Hello World!
```
`install` してモジュール化もできます。

```bash:go&nbsp;install
$ ls -l ~/go/src/test
-rw-r--r--  1 User  staff  76  5 25 16:09 hello.go
$ go install test
$ ls -l ~/go/bin/test
-rwxr-xr-x  1 User  staff  2020040  5 25 16:18 /Users/Tetsu/go/bin/test
$  ~/go/bin/test
Hello World! 
```
### depとModules
正直に言うとよくわかっていません。というよりこの後書くスクリプトが動けば良かったので、まだまだ必要な場面に出くわしていないだけです。。
[dep](https://github.com/golang/dep/blob/master/README.md)に関してはインストールすらまだな状態ですし、[Modules](https://github.com/golang/go/wiki/Modules)も**v1.12**から本格参戦っぽいので…今からやるのであればModulesの方が良いと思います。
そのうちこのあたりについては、また記事書こうと思います。。
# Goでaws-sdkを使ってみる
　まずはライブラリが必要なので`aws-sdk-go`を入手しましょう。
`aws-sdk-go-v2`もありますがまだ[preview](https://github.com/aws/aws-sdk-go-v2/blob/master/README.md)なのでやめておきました。
では[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/sdk-for-go/v1/developer-guide/setting-up.html)に従ってインストールしてみます。

```bash:go&nbsp;get
$ go get -u github.com/aws/aws-sdk-go
go: finding github.com/aws/aws-sdk-go v1.16.3
go: finding github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
go: downloading github.com/aws/aws-sdk-go v1.16.3
```
実はこのときに`Modules`を利用して`aws-sdk-go`のインストールを実施してます。
参考：[go1.11のmodulesの使い方について](https://qiita.com/yagi5/items/82989a5ecda70a614c27)

```bash:go&nbsp;mod
$ mkdir tmp
$ cd tmp
tmp $
tmp $ go mod init github.com/own/test
go: creating new go.mod: module github.com/own/test
tmp $ ls -l
-rw-r--r--  1 User  staff  28  5 18 13:53 go.mod
tmp $ cat go.mod
module github.com/own/test
```
その後作成したスクリプトを実行すると…

```bash:go&nbsp;run&nbsp;rds_instance_parser.go
tmp $ go run $FILE_PATH/rds_instance_parser.go -args value.tf
go: downloading github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
tmp $ ls -l
total 24
-rwxr-xr-x  1 User  staff  3826  5 18 13:58 value.tf
-rw-r--r--  1 User  staff    83  5 18 13:54 go.mod
-rw-r--r--  1 User  staff   408  5 18 13:54 go.sum
```
スクリプトの成果物と`go.sum`ファイルが出来上がっていました。
中身はこんな感じ。

```bash:go.sum
tmp $ cat go.sum
github.com/aws/aws-sdk-go v1.16.3 h1:esEQzoR8SVXtwg42nRoR/YLftI4ktsZg6Qwr7jnDXy8=
github.com/aws/aws-sdk-go v1.16.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
```
どうやら上記スクリプト実行での依存関係にあるモジュールのバージョンをチェックした結果が`go.sum`ファイルに書き込まれているっぽい。
`go.mod`にも以下が追記がされていたので、 こちらに必要なモジュールを追記しておけば依存解決もできるのかな？

```bash:go.mod
$ cat go.mod
module github.com/you/hello
# これが追記されていた
require github.com/aws/aws-sdk-go v1.16.3 // indirect
```
# おまけ
<details><summary>ちなみに作ったスクリプトはこんな感じです。</summary><div>

```go:rds_instance_parser.go
package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"strings"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/pricing"
)

func getProduct(nextToken string) (interface{}, interface{}, interface{}, *string, bool, error) {
	svc := pricing.New(session.New(&aws.Config{Region: aws.String("us-east-1")}))
	input := &pricing.GetProductsInput{
		Filters: []*pricing.Filter{
			{
				Field: aws.String("location"),
				Type:  aws.String("TERM_MATCH"),
				Value: aws.String("US East (N. Virginia)"),
			},
		},
		ServiceCode:   aws.String("AmazonRDS"),
		FormatVersion: aws.String("aws_v1"),
		MaxResults:    aws.Int64(1),
		NextToken:     aws.String(nextToken),
	}
	result, err := svc.GetProducts(input)
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case pricing.ErrCodeInternalErrorException:
				fmt.Println(pricing.ErrCodeInternalErrorException, aerr.Error())
			case pricing.ErrCodeInvalidParameterException:
				fmt.Println(pricing.ErrCodeInvalidParameterException, aerr.Error())
			case pricing.ErrCodeNotFoundException:
				fmt.Println(pricing.ErrCodeNotFoundException, aerr.Error())
			case pricing.ErrCodeInvalidNextTokenException:
				fmt.Println(pricing.ErrCodeInvalidNextTokenException, aerr.Error())
			case pricing.ErrCodeExpiredNextTokenException:
				fmt.Println(pricing.ErrCodeExpiredNextTokenException, aerr.Error())
			default:
				fmt.Println(aerr.Error())
			}
		} else {
			// Print the error, cast err to awserr.Error to get the Code and
			// Message from an error.
			fmt.Println(err.Error())
		}
	}
	switch result.NextToken {
	case nil:
		return nil, nil, nil, nil, false, nil
	default:
		instanceType := result.PriceList[0]["product"].(map[string]interface{})["attributes"].(map[string]interface{})["instanceType"]
		if instanceType == nil {
			nToken := result.NextToken
			return nil, nil, nil, nToken, true, errors.New("InstanceType is nil")
		}
		vcpu := result.PriceList[0]["product"].(map[string]interface{})["attributes"].(map[string]interface{})["vcpu"]
		memory := result.PriceList[0]["product"].(map[string]interface{})["attributes"].(map[string]interface{})["memory"]
		nToken := result.NextToken
		return instanceType, vcpu, memory, nToken, true, nil
	}
}

func main() {
	type dbInstanceType struct {
		Name   interface{}
		Vcpu   interface{}
		Memory interface{}
	}
	type dbInstanceTypeList []dbInstanceType
	var dbInstanceTypes dbInstanceTypeList

	var nextToken = ""
	for i := 0; ; {
		i++
		instanceType, vcpu, memory, nToken, next, err := getProduct(nextToken)
		if err != nil {
			nextToken = *nToken
			continue
		}
		if next != true {
			break
		}
		dbInstanceTypeInfo := dbInstanceType{
			Name:   instanceType,
			Vcpu:   vcpu,
			Memory: memory,
		}
		dbInstanceTypes = append(dbInstanceTypes, dbInstanceTypeInfo)
		nextToken = *nToken
	}
	results := make([]dbInstanceType, 0, len(dbInstanceTypes))
	encountered := map[interface{}]bool{}
	for i := 0; i < len(dbInstanceTypes); i++ {
		if !encountered[dbInstanceTypes[i]] {
			encountered[dbInstanceTypes[i]] = true
			results = append(results, dbInstanceTypes[i])
		}
	}

	maxLength := 0
	for i := 0; i < len(results); i++ {
		if len(results[i].Name.(string)) > maxLength {
			maxLength = len(results[i].Name.(string))
		}
	}

	instanceSpecs := ""
	for i := 0; i < len(results); i++ {
		NAME := results[i].Name.(string)
		MEMORY := results[i].Memory.(string)
		// In the API, db.r5.4xlarge has a memory of 192 GiB, but correctly it is 128 GiB
		if NAME == "db.r5.4xlarge" {
			MEMORY = "128 GiB"
		}
		MEMORY = strings.Replace(MEMORY, " GiB", "", 1)
		s, _ := strconv.ParseFloat(MEMORY, 64)
		s = s * (1024 * 1024 * 1024)
		MEMORY = strconv.FormatFloat(s, 'f', 0, 64)
		if len(NAME) < maxLength {
			NAME = NAME + strings.Repeat(" ", maxLength-len(NAME))
		}
		VCPU := results[i].Vcpu.(string)
		SPACE := strings.Repeat(" ", 4-len(VCPU))
		instanceSpec := "    " + NAME + " = {cpu_cores = " + VCPU + "," + SPACE + "memory = " + MEMORY + "}\n"
		instanceSpecs = instanceSpecs + instanceSpec
	}

	content := []byte(
		"locals {\n" +
			"  instance_types = {\n" +
			instanceSpecs +
			"  }\n" +
			"}\n",
	)
	ioutil.WriteFile(os.Args[len(os.Args)-1], content, os.ModePerm)
}
```
一部出力結果抜粋

```json:value.tf
 instance_types = {
     db.m4.4xlarge   = {cpu_cores = 16,  memory = 68719476736}
     db.r4.4xlarge   = {cpu_cores = 16,  memory = 130996502528}
     db.r3.xlarge    = {cpu_cores = 4,   memory = 32749125632}
 }
```

</div></details>

中身の解説は別の記事で書きます。
簡単に説明だけしておくと[PricingのGetProducts](https://docs.aws.amazon.com/sdk-for-go/api/service/pricing/#Pricing.GetProducts)というライブラリを利用して、バージニア北部からRDSインスタンスのスペックを抜き出して整形しているだけです。（バージニア北部から抜き出しているのは、対応しているインスタンスタイプが多いからです。）
冒頭Goに逃げたと書きましたが実はPythonでもできています。
…が、ごちゃごちゃしているので比較記事でもそのうち書きます。
# おしまい
標準ライブラリでできることがかなり多いと思いました。（今回はほぼ使ってないけど。）
バックエンド系とかこのへんで実装してみたいなぁ…そのうち書きます。
### 宿題
次回予告的なのです。

- **dep** と **Modules**
- **Modules**について([go.modとgo.sum](https://github.com/golang/go/wiki/Modules#faqs--gomod-and-gosum))
- PythonとGoの比較
- Goのバックエンドへの実装