Goで作る Kubernetes Podで稼働するコンテナメトリクスを取得するプログラム

::: note info
これは [ZOZO Advent Calendar 2023](https://qiita.com/advent-calendar/2023/zozo) カレンダー Vol.5 の 5日目の記事です。
:::

本記事では、Go言語でKubernetes Podで実行中のコンテナメトリクスを取得する方法をご紹介します。

Kubernetesでは、アドオンのコンポーネントであるmetrics-serverをデプロイすることで、Metrics APIを介してクラスタ内で実行中のPodやNodeのリソース使用量を取得することができます。  
こちらのMetrics APIを利用して対象のPodで稼働中のコンテナメトリクスを取得するプログラムをGo言語で作成しつつ、各処理について説明します。

# `k8s.io/metrics`モジュールの紹介
Kubernetes Organization配下のリポジトリである`metrics`リポジトリでは、`k8s.io/metrics`モジュールを提供しています。  
このモジュールには、Kubernetes Metrics APIに関連する型定義とクライアントが実装されています。

本記事で扱うプログラムではこちらの`k8s.io/metrics`モジュールを利用して実行中のコンテナメトリクスを取得します。

# 事前準備
プログラムを動かす前準備として、まず各環境でのMetrics APIをの有効化が必要です。  
本記事ではKubernetesクラスタの動作環境としてminikubeを利用しているため、次のコマンドでmetrics-serverを有効化します。
```bash
minikube addons enable metrics-server
```
Podをデプロイした後で、以下のコマンドを実行し、結果が取得できればMetrics APIが動作しています。
```
kubectl top pods
NAME         CPU(cores)   MEMORY(bytes)
run-stress   944m         1100Mi
```

メトリクス取得対象のコンテナを稼働させるPodのマニフェストは次になります。

```sample_pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: run-stress
spec:
  containers:
  - name: stress-ng
    image: ubuntu:20.04
    command: 
    - "/bin/bash"
    - "-c"
    - |
      apt-get update && apt-get install -y stress-ng
      stress-ng --cpu 1 --vm 1 --vm-bytes 1G
    resources:
      limits:
        cpu: "1"
        memory: "2G"
```

コンテナでの実行コマンドとして`stress-ng`コマンドを利用しています。  
こちらのコマンドを利用することで対象のPodに一定の負荷をかけることができます。

`sample_pod.yaml`では`stress-ng`コンテナの`resources.limits`で`cpu: 1, memory: 2G`を指定した上で、`stress-ng`コマンドの実行引数で`cpu: 1, memory: 1G`を指定しています。  
そのため、コンテナのCPU、メモリ使用量の予測値は`cpu: 1vCPU, memory: 1G`になります。

マニフェストの作成後は次のコマンドでメトリクス取得対象のPodを作成してください。
```bash
kubectl apply -f sample_pod.yaml
```

これまでの手順により、稼働中のコンテナのメトリクスを取得する前準備が完了しました。

まずは`kubectl top`コマンドで、上記の手順でデプロイしたコンテナのメトリクスを取得してみます。  
Podが作成されてしばらく経ってから、次のように`k top`コマンドを実行すると稼働中のコンテナのCPU、メモリ使用量を取得することができます。
```bash
k top pod run-stress --containers                                    
POD          NAME        CPU(cores)   MEMORY(bytes)
run-stress   stress-ng   999m         1100Mi
```
またコマンドの実行結果から、ほぼ想定通りの使用量が取得できていることが確認できました。

次節では、Go言語で同様のコンテナメトリクスを取得するプログラムの実装例をご紹介します。

# `k8s.io/metrics`モジュールを使ったGoプログラムの実装例
`k8s.io/metrics`モジュールを使って、Goで対象のPodで稼働中のコンテナメトリクスを取得するプログラムの実装例は以下になります。
```fetch_metrics.go
package main

import (
	"context"
	"flag"
	"fmt"
	"os"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/clientcmd"
	metricsClient "k8s.io/metrics/pkg/client/clientset/versioned"
)

var (
	kubeconfig string
	podName    string
)

func main() {
    // コマンド引数の受け取り
	flag.StringVar(&podName, "name", "", "Fetch metrics target Pod name")
	flag.StringVar(&kubeconfig, "kubeconfig", "", "kubeconfig path")
	flag.Parse()

    // クライアントの初期化
	config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
	clientset, _ := metricsClient.NewForConfig(config)

    // Podメトリクスの取得
	podMetrics, err := clientset.MetricsV1beta1().PodMetricses("default").Get(context.TODO(), podName, metav1.GetOptions{})

	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	for _, containerMetrics := range podMetrics.Containers {
		cpu := containerMetrics.Usage.Cpu()
		memory := containerMetrics.Usage.Memory()
		fmt.Printf("Cpu: %v core, Memory: %v bytes.", cpu.AsDec(), memory.AsDec())
	}
}
```
## 利用する型・メソッドの説明
Metrics APIへのリクエストは`podMetrics, err := clientset.MetricsV1beta1().PodMetricses("default").Get(context.TODO(), podName, metav1.GetOptions{})`の箇所で行なっています。  
リクエスト時のクライアントは`k8s.io/metrics/pkg/client/clientset/versioned`パッケージの`Clientset`型を持っており、型は次のように定義されています。
```metrics/pkg/client/clientset/versioned/clientset.go
type Clientset struct {
	*discovery.DiscoveryClient
	metricsV1alpha1 *metricsv1alpha1.MetricsV1alpha1Client
	metricsV1beta1  *metricsv1beta1.MetricsV1beta1Client
}
```
`client-go`の`kubernetes`パッケージで提供される`Clientset`と同様にこちらの`Clientset`でも複数の`Group-Version`に対応するクライアントを持っています。  
今回取得したいPodのメトリクスはmetrics Groupのv1beta1 Versionになるため、クライアントの`MetricsV1beta1()`メソッドを呼んでいます。

対象のNamespace名、Pod名を指定して結果を取得する流れも、`client-go`の`kubernetes`パッケージでPodを取得する場合と同様です。

ここで、`Get()`メソッドは次の`PodMetrics`型の値を返します。
```metrics/pkg/apis/metrics/v1beta1/types.go
// PodMetrics sets resource usage metrics of a pod.
type PodMetrics struct {
	metav1.TypeMeta `json:",inline"`
	// Standard object's metadata.
	// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	// The following fields define time interval from which metrics were
	// collected from the interval [Timestamp-Window, Timestamp].
	Timestamp metav1.Time     `json:"timestamp" protobuf:"bytes,2,opt,name=timestamp"`
	Window    metav1.Duration `json:"window" protobuf:"bytes,3,opt,name=window"`

	// Metrics for all containers are collected within the same time window.
	Containers []ContainerMetrics `json:"containers" protobuf:"bytes,4,rep,name=containers"`
}
```
`PodMetrics`型の`Containers`フィールドでは`[]ContainerMetrics`型を持っているため、対象のPodで稼働するコンテナのメトリクスを取得するためにはこちらのフィールド値を参照します。

要素となる、`ContainerMetrics`型は、以下のように定義されています。
```metrics/pkg/apis/metrics/v1beta1/types.go
// ContainerMetrics sets resource usage metrics of a container.
type ContainerMetrics struct {
	// Container name corresponding to the one from pod.spec.containers.
	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
	// The memory usage is the memory working set.
	Usage v1.ResourceList `json:"usage" protobuf:"bytes,2,rep,name=usage,casttype=k8s.io/api/core/v1.ResourceList,castkey=k8s.io/api/core/v1.ResourceName,castvalue=k8s.io/apimachinery/pkg/api/resource.Quantity"`
}
```
コンテナのリソース使用量については、`ContainerMetrics`型の`Usage`フィールドの値を参照することで取得できることがわかります。

`Usage`フィールドの値が持つ、`v1.ResourceList`型については、`k8s.io/api`モジュールで次のように定義されています。
```api/core/v1/types.go
// ResourceName is the name identifying various resources in a ResourceList.
type ResourceName string

// Resource names must be not more than 63 characters, consisting of upper- or lower-case alphanumeric characters,
// with the -, _, and . characters allowed anywhere, except the first or last character.
// The default convention, matching that for annotations, is to use lower-case names, with dashes, rather than
// camel case, separating compound words.
// Fully-qualified resource typenames are constructed from a DNS-style subdomain, followed by a slash `/` and a name.
const (
	// CPU, in cores. (500m = .5 cores)
	ResourceCPU ResourceName = "cpu"
	// Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
	ResourceMemory ResourceName = "memory"
	// Volume size, in bytes (e,g. 5Gi = 5GiB = 5 * 1024 * 1024 * 1024)
	ResourceStorage ResourceName = "storage"
	// Local ephemeral storage, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
	// The resource name for ResourceEphemeralStorage is alpha and it can change across releases.
	ResourceEphemeralStorage ResourceName = "ephemeral-storage"
)

...省略

// ResourceList is a set of (resource name, quantity) pairs.
type ResourceList map[ResourceName]resource.Quantity
```
こちらの実装を見ると、`ResourceList`型の値はcpu・memory・storage・epemeral-storageをキーにして、それぞれに対応する値を`Quantity`型で返すことがわかります。

`fetch_metrics.go`では`Cpu()`、`Memory()`メソッドにより、これらcpu・memoryに対応する値を取得しています。
ここで取得できる`Quantity`型の値は整数と固定小数点を持っています。  
Quantity型についてここでは詳しく触れませんが、`k8s.io/apimachinery`モジュールの[`api/resource`パッケージ](https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go)で定義されているため、気になる方はご参照ください。

最後に、Quantity型の戻り値から`AsDec()`メソッドを呼ぶことで[`go-inf/inf`モジュール](https://github.com/go-inf/inf)の`inf`パッケージが提供する`Dec`型の値としてCPU、メモリの値を取得し、`fmt.Printf`で出力しています。

補足として、今回は取得対象のPodで稼働するコンテナが1つであったため特に絞り込みをしませんでしたが、複数コンテナが稼働するPodを対象とする場合は、ただ`Containers`フィールドでループするだけだと全てのコンテナのCPU、メモリ使用量が出力されてしまいます。  
この場合は`ContainerMetrics`型の`Name`フィールドの値を参照して出力対象の絞り込みを行うのが良さそうです。

## 実行結果
プログラムの実行結果は次になります。
```bash
 go run fetch_container_metrics.go --kubeconfig $HOME/.kube/config --name run-stress                                                                                            
 
Cpu: 0.999806090 core, Memory: 1154260992 bytes.% 
```
プログラムの実行結果から、`k8s.io/metrics`モジュールのクライアントを使って稼働中のコンテナのCPU、メモリ使用量が取得できていることがわかります。  
デフォルトのまま単位の変換をしていないため多少見づらいですが、それぞれの使用量はCPUで約1コア、メモリで約1.15Gとなっており、先ほど`kubectl top`コマンドで取得したものとほぼ同じ値が取得できていることが確認できました。

# まとめ
本記事では、`k8s.io/metrics`モジュールを利用して、Kubernetes Podで稼働するコンテナのメトリクスを取得するプログラムの実装例を紹介しました。  
こちらのモジュールを利用することにより、Metrics APIを介してコンテナメトリクスが取得できることを確認できました。  


注意点として、上記の実装例で取得したメトリクスはある時点でのスナップショット的な値になります。  
実際のプログラムでは、これらのメトリクスは時間により変化するため、一定間隔で取得した平均を取るなどして集計すると良さそうです。