client-go, gcloud-sdkでk8sクラスタのmetricsをstackdriver monitoringへ送信する

# はじめに
GKEのpod数などの情報をStackDriver Monitoringに送信するコードをgoで書きました。
[Datadog](https://docs.datadoghq.com/agent/basic_agent_usage/kubernetes/)だとエージェントをk8sへ仕込めばもっと簡単に実現できるんですが、有料コースになってしまうのと、golangを久しぶりに書きたかったのでこの方法を試しました。

# 使用するライブラリ

- client-go

https://github.com/kubernetes/client-go

k8sクラスタの情報は通常[kubectl](https://kubernetes.io/docs/reference/kubectl/overview/)で取得すると思います。
そのクライアントライブラリです。

- cloud-sdk

https://godoc.org/cloud.google.com/go/monitoring/apiv3

StackDriver Monitoringのgoライブラリです。

つまり、クラスタ情報をclient-goで取得し、cloud-sdkでStackDriverに送ります。

[![alt](http://www.plantuml.com/plantuml/svg/SoWkIImgAStDuT9mztRLLD2rKz18pibCpIlHJS_Nv4Am8LBfoJdvfIcwnIdPG3a41o8PN9AOdErIb5cMMgJWcv-NMP9Vb9cNhidba9gN0hG10000)](http://www.plantuml.com/plantuml/svg/SoWkIImgAStDuT9mztRLLD2rKz18pibCpIlHJS_Nv4Am8LBfoJdvfIcwnIdPG3a41o8PN9AOdErIb5cMMgJWcv-NMP9Vb9cNhidba9gN0hG10000)


# 前準備
今回は定期的にバッチで起動するため、service accountの認証情報を使います。

- service accountの取得

IAM & Admin -> CREATE SERVICE ACCOUNT

roleはkuberntes engine viewer, monitoring editorを指定します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/12060/fc884a6b-094f-d222-855b-3c861a621a86.png" width="50%"/>

そしてSAVEしたらjsonをDLできるので大事に取っておきます。

# コード

こういう階層です。
`sa.json`は先ほど控えておいたservice accountのjsonです。

```:ディレクトリ階層
gopath/src/github.com/hogehoge
  |- main.go
  `- sa.json
```

```go:main.go
package main

import (
	"cloud.google.com/go/monitoring/apiv3"
	"context"
	"fmt"
	"github.com/golang/protobuf/ptypes/timestamp"
	"google.golang.org/genproto/googleapis/api/label"
	"google.golang.org/genproto/googleapis/api/metric"
	"google.golang.org/genproto/googleapis/api/monitoredres"
	monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
	"k8s.io/client-go/tools/clientcmd"
	"log"
	"os"
	"os/exec"
	"time"
)

const (
	GAUGE                          = 1
	INT64                          = 2
	STRING                         = 0
)

type Config struct {
	ProjectId   string
	MetricType  string
	MetricName  string
	Description string
	DisplayName string
	Labels      string
	Cluster     string
        K8sConfig   string
}

var config *Config

var client *monitoring.MetricClient

func init() {
	// config
	config = &Config{}

	config.ProjectId = os.Getenv("PROJECT_ID")
        config.K8sConfig = os.Getenv("K8S_CONFIG")
	config.Cluster = os.Getenv("CLUSTER")
	config.MetricType = "custom.googleapis.com/pod_num"
	config.MetricName = fmt.Sprintf(
		"projects/%s/metricDescriptors/%s", config.ProjectId, config.MetricType)
	config.Description = "number of pods"
	config.DisplayName = "number of pods"

	// client-goをservice accountで使用するためにシェルを起動
	exec.Command(
		"gcloud", "config", "set", "container/use_v1_api", "false")
	exec.Command(
		"gcloud", "auth", "activate-service-account", "--key-file="+os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))

	// get metrics client
	var err error
	ctx := context.Background()
	client, err = monitoring.NewMetricClient(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	// metrics作成
	var des *metric.MetricDescriptor
	des = getMetric()
	if des == nil {
		des = createMetric()
	}

	// k8sの情報取得
	podNum := getPodNum()

	// metrics書き込み
	addLogs(podNum)
}

// メトリクスの存在確認
func getMetric() *metric.MetricDescriptor {
	req := &monitoringpb.GetMetricDescriptorRequest{
		Name: config.MetricName,
	}

	ctx := context.Background()
	res, err := client.GetMetricDescriptor(ctx, req)
	if err != nil {
		return nil
	}

	return res
}

// メトリクス作成
func createMetric() *metric.MetricDescriptor {
	ld := label.LabelDescriptor{Key: "test_metrics", ValueType: STRING, Description: "test"}
	req := &monitoringpb.CreateMetricDescriptorRequest{
		Name: config.MetricName,
		MetricDescriptor: &metric.MetricDescriptor{
			Name:        config.MetricName,
			Type:        config.MetricType,
			Labels:      []*label.LabelDescriptor{&ld},
			MetricKind:  GAUGE,
			ValueType:   INT64,
			Description: config.Description,
			DisplayName: config.DisplayName,
		},
	}

	ctx := context.Background()
	res, err := client.CreateMetricDescriptor(ctx, req)
	if err != nil {
		log.Fatal(err)
	}

	return res
}

// pod数取得
func getPodNum() int64 {
	config, err :=
		clientcmd.BuildConfigFromFlags("", config.K8sConfig)
	if err != nil {
		log.Fatal(err)
	}

	clientSet, err := kubernetes.NewForConfig(config)
	if err != nil {
		log.Fatal(err)
	}

	pods, err := clientSet.CoreV1().Pods("default").List(metav1.ListOptions{})
	if err != nil {
		log.Fatal(err)
	}

	return int64(len(pods.Items))
}

// メトリクスへの書き込み
func addLogs(podNum int64) {
	pt := monitoringpb.Point{
		Interval: &monitoringpb.TimeInterval{
			EndTime: &timestamp.Timestamp{
				Seconds: time.Now().Unix(),
			},
		},
		Value: &monitoringpb.TypedValue{
			Value: &monitoringpb.TypedValue_Int64Value{Int64Value: podNum},
		},
	}
	ts := monitoringpb.TimeSeries{
		Metric: &metric.Metric{
			Type: config.MetricType,
			Labels: map[string]string{
				"cluster": config.Cluster,
			},
		},
		Resource: &monitoredres.MonitoredResource{
			Type: "global",
			Labels: map[string]string{
				"project_id": config.ProjectId,
			},
		},
		MetricKind: GAUGE,
		ValueType:  INT64,
		Points:     []*monitoringpb.Point{&pt},
	}
	req := &monitoringpb.CreateTimeSeriesRequest{
		Name:       config.MetricName,
		TimeSeries: []*monitoringpb.TimeSeries{&ts},
	}

	ctx := context.Background()
	err := client.CreateTimeSeries(ctx, req)
	if err != nil {
		log.Fatal(err)
	}
}
```

以下、コード解説します。

いきなりシェルを呼んでいるんですが、gcloudの認証情報をアクティベートしてk8sapiを叩けるようにしています。

```go
func init() {
           :
	// client-goをservice accountで使用するためにシェルを起動
	exec.Command(
		"gcloud", "config", "set", "container/use_v1_api", "false")
	exec.Command(
		"gcloud", "auth", "activate-service-account", "--key-file="+os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))
```

この処理を経ると
最終的に欲しい`~/.kube/config`というkubectlの認証ファイルができます。

cloud-sdkのapiクライアントを取得します。

```go
	// get metrics client
	var err error
	ctx := context.Background()
	client, err = monitoring.NewMetricClient(ctx)
```

StackDriver monitoringは`custom metrics`という箱に`time series`という名のレコードを突っ込めばmetricsの情報が差し込まれます。
https://cloud.google.com/monitoring/api/v3/metrics

なので、下記のような順序で処理をしています。

- custom metricsが無かったら作る
- k8s情報（ここではpod数）の取得
- metricsへ書き込み

```go
func main() {
	// metrics作成
	var des *metric.MetricDescriptor
	des = getMetric()
	if des == nil {
		des = createMetric()
	}

	// k8sの情報取得
	podNum := getPodNum()

	// metrics書き込み
	addLogs(podNum)
}
```

# 実行

```bash
go run main.go
```

# 結果

うまくいmetricsを作成できていれば

StackDriver Monitoring -> Add Chart で下記のように作ったmetricsが候補として挙がります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/12060/ec53ebd8-38f9-5a74-e1cb-79a96a61e0cb.png" width=50% />

timeseriesも書き込みできていたらグラフも表示されます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/12060/0ea789aa-85c1-0817-66e8-28c7c5b7bfcb.png" width=50% />

以上です。