0
0

Kubernetes の外部メトリクスを自作する

Posted at

Kubernetes の外部メトリクスを次のフレームワークで実装してみました。

はじめに

Kubernetes は Aggregation Layer という仕組みを使って Kubernetes の API を拡張できるようになっており、外部メトリクスもこれを使います。

これにより、Kubernetes の API 呼び出し時に Kubernetes API サーバー(kube-apiserver)がプロキシとなって、APIService で設定した拡張 API サーバー(extension-apiserver)を呼び出すようになります。

この extension-apiserver をカスタムメトリクスや外部メトリクス用に実装するためのフレームワークが custom-metrics-apiserver です。

実装

custom-metrics-apiserver で外部メトリクス用の extension-apiserver を実行するのに最低限必要な処理は次のようになります。

処理内容
(a) ExternalMetricsProvider インターフェースを実装した Provider を用意
(b) AdapterBase へ (a) を WithExternalMetrics して Run を実施

デフォルト設定で実行する場合のコードはこのようになります。
ポート番号 443 で extension-apiserver が立ち上がり、GetExternalMetric 等を呼び出すリクエストハンドラが適用されます。

main.go
package main

import (
	"context"

	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/klog/v2"
	"k8s.io/metrics/pkg/apis/external_metrics"
	basecmd "sigs.k8s.io/custom-metrics-apiserver/pkg/cmd"
	"sigs.k8s.io/custom-metrics-apiserver/pkg/provider"
)

type SampleProvider struct{}
// ExternalMetricsProvider インターフェースの実装
func (p *SampleProvider) ListAllExternalMetrics() []provider.ExternalMetricInfo {
    ...省略
}
// ExternalMetricsProvider インターフェースの実装
func (p *SampleProvider) GetExternalMetric(ctx context.Context, namespace string, metricSelector labels.Selector, info provider.ExternalMetricInfo) (*external_metrics.ExternalMetricValueList, error) {
    ...省略
}

func main() {
	cmd := basecmd.AdapterBase{}

	provider := SampleProvider{}

	cmd.WithExternalMetrics(&provider)

	if err := cmd.Run(wait.NeverStop); err != nil {
		klog.Fatal(err)
	}
}

ExternalMetricsProvider の実装

ExternalMetricsProvider は次のように実装します。

メソッド名 処理内容
ListAllExternalMetrics その時点で有効なメトリクス名の一覧を返す
GetExternalMetric 条件に合致したメトリクスの内容を返す

ここでは、固定の内容を返すように実装しました。

main.go ExternalMetricsProvider実装例
func (p *SampleProvider) ListAllExternalMetrics() []provider.ExternalMetricInfo {
	return []provider.ExternalMetricInfo{
		{
			Metric: "sample1",
		},
		{
			Metric: "sample2",
		},
	}
}

func (p *SampleProvider) GetExternalMetric(ctx context.Context, namespace string, metricSelector labels.Selector, info provider.ExternalMetricInfo) (*external_metrics.ExternalMetricValueList, error) {
	name := info.Metric

	items := []external_metrics.ExternalMetricValue{
		{
			MetricName: name,
			MetricLabels: map[string]string{
				"app":      "sample",
				"category": "1",
			},
			Timestamp: metav1.Now(),
			Value:     *resource.NewQuantity(123, resource.DecimalSI),
		},
		{
			MetricName: name,
			MetricLabels: map[string]string{
				"app":      "sample",
				"category": "2",
			},
			Timestamp: metav1.Now(),
			Value:     *resource.NewScaledQuantity(456, resource.Micro),
		},
	}

	return &external_metrics.ExternalMetricValueList{
		Items: items,
	}, nil
}

注意点

12/23 の時点で go mod tidy に任せると、Kubernetes 関連ライブラリでバージョン v0.28.1v0.29.0 が混在してビルドエラーになりました。

そこで、とりあえず go.mod を以下のように編集してから go mod tidy を実施する事で回避しました。

go.mod バージョン混在問題の回避措置
module sample

go 1.21.5

require (
	k8s.io/apimachinery v0.28.1
	k8s.io/metrics v0.28.1
)

設定

extension-apiserver を Pod で実行し、APIService 登録すると、/apis/{group}/{version}/... の API 呼び出し時に該当する extension-apiserver が呼び出されるようになります。

extension-apiserver は次のような権限が必要となるため、これらの権限を付与したサービスアカウントを使用します。

  • kube-system の extension-apiserver-authentication ConfigMap 読み込み
  • kube-apiserver へ SubjectAccessReview リクエスト送信

サービスアカウント設定

必要な権限を付与するための Role は次のようになります。

Role kind Role name 適用される権限
Role extension-apiserver-authentication-reader extension-apiserver-authentication ConfigMap 読み込み
ClusterRole system:auth-delegator kube-apiserver へ SubjectAccessReview リクエスト送信許可

これらを付与したサービスアカウントの設定はこのようになります。

sample-metrics.yaml(ServiceAccount設定部分)
kind: ServiceAccount
apiVersion: v1
metadata:
  name: sample-metrics-apiserver
  namespace: default

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: sample-metrics:system:auth-delegator
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
- kind: ServiceAccount
  name: sample-metrics-apiserver
  namespace: default

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sample-metrics-auth-reader
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: extension-apiserver-authentication-reader
subjects:
- kind: ServiceAccount
  name: sample-metrics-apiserver
  namespace: default

Deployment・Service 設定

Pod で実行するための設定を行います。
今回は手動でインポートしたコンテナイメージを使うため imagePullPolicy を Never にしています。

sample-metrics.yaml(Pod設定部分)
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sample-metrics-apiserver
  name: sample-metrics-apiserver
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-metrics-apiserver
  template:
    metadata:
      labels:
        app: sample-metrics-apiserver
      name: sample-metrics-apiserver
    spec:
      serviceAccountName: sample-metrics-apiserver
      containers:
      - name: sample-metrics-apiserver
        image: sample-metrics
        imagePullPolicy: Never
        ports:
        - containerPort: 443
          name: https

---
apiVersion: v1
kind: Service
metadata:
  name: sample-metrics-apiserver
spec:
  ports:
  - name: https
    port: 443
  selector:
    app: sample-metrics-apiserver

APIService 設定

外部メトリクスとして sample-metrics-apiserver を適用するための設定を行います。
接続先のポートはデフォルトで 443 になります。

sample-metrics.yaml(APIService設定部分)
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1beta1.external.metrics.k8s.io
spec:
  service:
    name: sample-metrics-apiserver
    namespace: default
  group: external.metrics.k8s.io
  version: v1beta1
  insecureSkipTLSVerify: true
  groupPriorityMinimum: 100
  versionPriority: 100

なお、groupPriorityMinimum と versionPriority は必須で 1以上の値を指定する必要がありました。

動作確認

今回は K3s 環境で実行しました。

ビルド

このような Dockerfile を使用しました。

Dockerfile
FROM golang:1.21-bullseye AS build

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY *.go ./

RUN go build -o mainapp

FROM debian:bullseye-slim

WORKDIR /app

RUN apt-get update && rm -rf /var/lib/apt/lists/*

COPY --from=build /app/mainapp .

CMD ["./mainapp"]

今回は BuildKit でビルドし、そのコンテナイメージを(K3s へ)インポートします。

ビルド例
$ sudo buildctl build --frontend dockerfile.v0 --local context=. --local dockerfile=. --output type=oci,name=sample-metrics > sample-metrics.tar
インポート例
$ sudo k3s ctr images import sample-metrics.tar

実行

kubectl apply して Pod を実行します。

apply 例
$ kubectl apply -f sample-metrics.yaml 
serviceaccount/sample-metrics-apiserver created
clusterrolebinding.rbac.authorization.k8s.io/sample-metrics:system:auth-delegator created
rolebinding.rbac.authorization.k8s.io/sample-metrics-auth-reader created
deployment.apps/sample-metrics-apiserver created
service/sample-metrics-apiserver created
apiservice.apiregistration.k8s.io/v1beta1.external.metrics.k8s.io created
Pod の一覧
$ kubectl get pod
NAME                                        READY   STATUS    RESTARTS   AGE
sample-metrics-apiserver-7bc64b769f-6pdpw   1/1     Running   0          5s

動作確認

実行した外部メトリクスを動作確認します。

/apis/external.metrics.k8s.io/v1beta1 で ListAllExternalMetrics の戻り値に基づいたレスポンスが得られます。

メトリクス名の取得例
$ kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1"
{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"external.metrics.k8s.io/v1beta1","resources":[{"name":"sample1","singularName":"","namespaced":true,"kind":"ExternalMetricValueList","verbs":["get"]},{"name":"sample2","singularName":"","namespaced":true,"kind":"ExternalMetricValueList","verbs":["get"]}]}

/apis/external.metrics.k8s.io/v1beta1/namespaces/{namespace}/{メトリクス名} で GetExternalMetric の結果を得られます。

sample1 メトリクス取得例
$ kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/default/sample1"
{"kind":"ExternalMetricValueList","apiVersion":"external.metrics.k8s.io/v1beta1","metadata":{},"items":[{"metricName":"sample1","metricLabels":{"app":"sample","category":"1"},"timestamp":"2023-12-23T04:16:47Z","value":"123"},{"metricName":"sample1","metricLabels":{"app":"sample","category":"2"},"timestamp":"2023-12-23T04:16:47Z","value":"456u"}]}

また、ListAllExternalMetrics の結果に無いメトリクス名を指定しても問題ありません。

aaa メトリクス取得例
$ kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/default/aaa"
{"kind":"ExternalMetricValueList","apiVersion":"external.metrics.k8s.io/v1beta1","metadata":{},"items":[{"metricName":"aaa","metricLabels":{"app":"sample","category":"1"},"timestamp":"2023-12-23T04:17:20Z","value":"123"},{"metricName":"aaa","metricLabels":{"app":"sample","category":"2"},"timestamp":"2023-12-23T04:17:20Z","value":"456u"}]}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0