はじめに
GKEのpod数などの情報をStackDriver Monitoringに送信するコードをgoで書きました。
Datadogだとエージェントをk8sへ仕込めばもっと簡単に実現できるんですが、有料コースになってしまうのと、golangを久しぶりに書きたかったのでこの方法を試しました。
使用するライブラリ
- client-go
k8sクラスタの情報は通常kubectlで取得すると思います。
そのクライアントライブラリです。
- cloud-sdk
StackDriver Monitoringのgoライブラリです。
つまり、クラスタ情報をclient-goで取得し、cloud-sdkでStackDriverに送ります。
前準備
今回は定期的にバッチで起動するため、service accountの認証情報を使います。
- service accountの取得
IAM & Admin -> CREATE SERVICE ACCOUNT
roleはkuberntes engine viewer, monitoring editorを指定します。
そしてSAVEしたらjsonをDLできるので大事に取っておきます。
コード
こういう階層です。
sa.json
は先ほど控えておいたservice accountのjsonです。
gopath/src/github.com/hogehoge
|- main.go
`- sa.json
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: ×tamp.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を叩けるようにしています。
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クライアントを取得します。
// 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へ書き込み
func main() {
// metrics作成
var des *metric.MetricDescriptor
des = getMetric()
if des == nil {
des = createMetric()
}
// k8sの情報取得
podNum := getPodNum()
// metrics書き込み
addLogs(podNum)
}
実行
go run main.go
結果
うまくいmetricsを作成できていれば
StackDriver Monitoring -> Add Chart で下記のように作ったmetricsが候補として挙がります。
timeseriesも書き込みできていたらグラフも表示されます。
以上です。