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 等を呼び出すリクエストハンドラが適用されます。
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 | 条件に合致したメトリクスの内容を返す |
ここでは、固定の内容を返すように実装しました。
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.1
と v0.29.0
が混在してビルドエラーになりました。
そこで、とりあえず go.mod を以下のように編集してから go mod tidy
を実施する事で回避しました。
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 リクエスト送信許可 |
これらを付与したサービスアカウントの設定はこのようになります。
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 にしています。
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 になります。
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 を使用しました。
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 を実行します。
$ 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
$ 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 の結果を得られます。
$ 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 の結果に無いメトリクス名を指定しても問題ありません。
$ 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"}]}