GCEのInstance Group ManagerをGolang SDKで使う

一部を除き、GCPの各種サービスのGolang SDKはAPI定義であるJSON Schemaから自動生成されたものである。
API定義と生成されるSDKに癖があり、実際にこれを用いる際にいくつかハマる箇所があるため、ここでいくつか例を出しながら解説したいと思う。

実際のJSON SchemaとGolangのコードは[ここ](https://github.com/google/google-api-go-client)でみれる。

# GCEで出てくる概念の説明

## Instance Group Managerとは

GCEで作成出来るInstanceをManageしてくれるもの、というそのままな説明になる。
具体的に出来ることとして
- 死活管理
- 負荷分散
- オートスケール
- Instance Templateの使用

などがある。詳細は以下
[Instance Groups](https://cloud.google.com/compute/docs/instance-groups/)

## Instance Templateとは

Instanceの設定を行うもの。
設定できるものとしては
- OSやMachine Type
- Disk
- 起動時のscript
- Network

などがある。詳細は以下
[Creating Instance Templates](https://cloud.google.com/compute/docs/instance-templates)

# 具体例

具体的にGolang SDKを用いて以上のResourceを作成してみようと思う。
流れとしてはGCP上にInstance Templateを作成し、それを指定してManaged Instance Groupを作成する、という流れになる。

## サンプル

```go:main.go

import (
	"errors"
	"fmt"
	"strings"
	"time"

	"golang.org/x/net/context"
	"golang.org/x/oauth2/google"
	compute "google.golang.org/api/compute/v1"
)

func readError(op *compute.Operation) error {
	if op.Error == nil {
		return nil
	}

	errs := []string{}
	for _, err := range op.Error.Errors {
		errs = append(errs, err.Message)
	}
	return errors.New(strings.Join(errs, ","))
}

func main() {
	err := func() error {
        s, err := ioutil.ReadFile("/path/to/credential.json") // GoogleのIAMで作成するCredentialのJSON
        if err != nil {
          return err
        }

		c, err := google.JWTConfigFromJSON(s, compute.ComputeScope)
		if err != nil {
			return err
		}
		ctx := context.Background()
		client := c.Client(ctx)
		service, err := compute.New(client) // 上記のCredentialにより作成したこのServiceを全リクエストで用いる
		if err != nil {
			return err
		}

		startupScript := `
#!/bin/bash
sudo apt-get update && sudo apt-get install nginx
`

		temp := &compute.InstanceTemplate{
			Name: "sample-template",
			Properties: &compute.InstanceProperties{
				Tags: &compute.Tags{
					Items: []string{
						"https-server", // GoogleのFirewallはタグによってルールをインスタンスに設定する形で用いる。
						"http-server",
					},
				},
				NetworkInterfaces: []*compute.NetworkInterface{
					{
						AccessConfigs: []*compute.AccessConfig{
							{
								Name: "external-IP",　
								Type: "ONE_TO_ONE_NAT",
							},
						},
						Network: "global/networks/default",
					},
				},
				Disks: []*compute.AttachedDisk{
					{
						Boot: true,
						InitializeParams: &compute.AttachedDiskInitializeParams{
							SourceImage: "projects/debian-cloud/global/images/debian-8-jessie-v20170308",
						},
					},
				},
				MachineType: "n1-standard-1",
				Metadata: &compute.Metadata{
					Items: []*compute.MetadataItems{
						{
							Key:   "startup-script", // 起動時に走らせるscriptはこの名前でメタデータに登録する。
							Value: &startupScript,
						},
					},
				},
			},
		}

		op, err := compute.NewInstanceTemplatesService(service).
			Insert(project, temp).
			Do()
		if err != nil {
			return err
		}
		if err := readError(op); err != nil {
			return err
		}

        // templateが登録されるまでラグがあるので待つ必要がある
		for {
			time.Sleep(1 * time.Second)

			b := false
			ts, err := compute.NewInstanceTemplatesService(service).
				List(project).
				Do()
			if err != nil {
				return err
			}
			for _, t := range ts.Items {
				if "sample-template" == t.Name {
					b = true
				}
			}
			if !b {
				continue
			}
			break
		}

		grp := &compute.InstanceGroupManager{
			InstanceTemplate: "global/instanceTemplates/sample-template",
			TargetSize:       1,
			Name:             "sample-group-manager",
			BaseInstanceName: "sample-group",
		}
		op, err = compute.NewInstanceGroupManagersService(service).
			Insert(project, zone, grp).
			Do()
		if err != nil {
			return err
		}
		if err := readError(op); err != nil {
			return err
		}
		return nil
	}()
	if err != nil {
		fmt.Println(err)
		return
	}
}


```

これでsample-templateをもったsample-group-managerが作成され、
sample-templateで記述された通りのinstanceが設定通り一台立ち上がる

## 解説

### Service
Golang SDKには(他のSDKもそうなのかもしれないが)Serviceという概念がある。
Service Account毎に発行されるCredential情報を持ったJSONを渡す事でこれが作成され、
各リクエスト毎に存在する特殊Serviceをこれで毎回作成する形になる。

### Request Body

各リクエストのプロパティについては、正直Golang SDKには一切情報が無いので各APIのリファレンスを見る必要がある。

[Instance Templates](https://cloud.google.com/compute/docs/reference/latest/instanceTemplates)
[Instance Group Manager](https://cloud.google.com/compute/docs/reference/latest/instanceGroupManagers)

経験上GCPのサービスをGolang SDKで操作しようとしたときに、どのプロパティがRequiredされていて、どのようなValidationが掛かっているかは、APIリファレンスにしかない場合が多い気がする。自動生成という性質上難しいのかもしれないが、かなり扱いづらい。
完全に別件だが、同様に自動生成されているCloud Vision APIのGolang SDKのGodocはサンプルコードがPython、Java、Objective-Cなどで構成され、(Golangは無い）[他言語のサンプルコードによってGodocが破壊されている。](https://godoc.org/google.golang.org/api/vision/v1#Color)

### Metadata

Instance TemplateにはMetadataを仕込むことが出来る。
これはこれでいろいろ便利であるのだが、ここは割愛する。([この記事](http://qiita.com/soundTricker/items/c8f86bf4a2c10c11d723)が詳しい)
要点として、ここにstatup-scriptという名前でscriptを仕込むことが出来、これがサーバーの起動時、もしくは再起動時に走るという事だ。
詳細は以下
[Running Startup Scripts](https://cloud.google.com/compute/docs/startupscript)

### Operation
Insert, Delete系のリクエストに関しては、Operationという返り値になる。
複数の依存関係のあるResourceを作成する場合は、各ベンダーのサービスの特性をよく理解して使う必要がある。Templateの作成が反映されるまでに微妙に時間が掛かったり掛からなかったりするので、再現が難しいエラーなどを踏んだ覚えがある。


# まとめ

GCPのGolang SDKに関しては学習コストが結構必要だったので、小規模なものならgcloudで頑張って構築した方が良いと思われる。
これは自分がGolang SDKを使用するにあたり、これを使用している人をあまり見かけなかったことが原因でもあるように見えるので、とりあえず自分が解説をしてみた。

正直プラクティスも何もわからないので、誰か教えてくれると助かる。