自作メトリクスサーバでHorizontalPodAutoscalerが使いたい！

# はじめに

CPUやメモリなどのResource Metrics API以外でHorizontal Pod Autoscaler(HPA)を利用したい時、例えばPrometheusのメトリクスを利用したい場合[Prometheus Adapter](https://github.com/directxman12/k8s-prometheus-adapter)や[Kube Metrics Adapter](https://github.com/zalando-incubator/kube-metrics-adapter)を利用することになるかと思います。

しかし利用したいデータソースに対するカスタム/外部メトリクスサーバがない場合、例えば社内の独自監視基盤のメトリクスをHPAで利用したくなった場合はどうすべきでしょうか？
そのような状況になっても「大丈夫です、作れますよ」と言えるようになるためにメトリクスサーバを作ってみました。

# カスタム/外部メトリクスサーバとは？

@Ladicle さんの[「独自メトリクスによるPodの水平スケール」](https://qiita.com/Ladicle/items/f97ab3653e8efa0e9d58)という記事で詳しく説明されていますので、そちらを参照することをおすすめします。ここでも簡単に説明しておくと

- カスタムメトリクスサーバ: Kubernetes APIリソースに紐づくリソースのメトリクスを提供する
- 外部メトリクスサーバ: Kubernetes APIリソースに紐づかないメトリクスを提供する

これら両方を実装しているものもあれば、片方だけのものもあります。
カスタムメトリクスサーバだとメトリクスとKubernetesのオブジェクトの紐付けがなんとなくのイメージで面倒臭そうだったので、今回は外部メトリクスサーバを実装しました。

# 実装した外部メトリクスサーバについて

成果物は[こちらのリポジトリ](https://github.com/uesyn/sample-metrics-server)にあります。
今回はPrometheus AdapterやKube Metrics Adapterもどきを作りました。単純に設定した外部メトリクスがKubernetesから呼び出されると、それに紐づくPromQLの結果を返すような外部メトリクスサーバです。試しに作っただけなので、かなり雑な作りとなっていますがとりあえず動きます。

![スクリーンショット 2020-12-22 9.44.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224576/532dd7e5-b6e3-8f4e-d895-c43b272d501a.png)


以下のようなコンフィグを設定ファイルとして利用します。

```yaml
# PrometheusのURL
url: http://prometheus.kube-system:9090
queries: 
  # External Metrics名: PromQL
  prom_up: up{component="prometheus"}
  http_requests_total: http_requests_total
  go_gc_duration_seconds_count: go_gc_duration_seconds_count
```

Prometheusに対するクエリ結果に含まれるラベルを外部メトリクスのラベルとして利用できるようにしました。
そのためPromQLの段階でラベルを用いて絞り込めますが、HPAのselectorで絞り込むこともできます。

```yaml
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
...
spec:
  metrics:
  - type: External
    external:
      metric:
        name: http_requests_total
        # Prometheusのメトリクスに含まれるラベルで絞り込み可能
        selector:
          matchLabels:
            component: web-app
      target:
        type: AverageValue
        averageValue: 30
```

# 実装

Custom Metrics Serverには[Boilerplate](https://github.com/kubernetes-sigs/custom-metrics-apiserver)が公式から用意されており、それを利用していけば比較的簡単に作れます。やらなければならないのはインターフェースを一つ実装するだけです。このリポジトリにはメトリクスサーバを作る上で便利なヘルパー関数も含まれています。
サンプル実装が[こちら](https://github.com/kubernetes-sigs/custom-metrics-apiserver/tree/master/test-adapter)にあり、カスタムメトリクスサーバの作り方に関しては[ドキュメント](https://github.com/kubernetes-sigs/custom-metrics-apiserver/blob/master/docs/getting-started.md)も用意されています。

実装するインターフェースは[`pkg/provider`](https://github.com/kubernetes-sigs/custom-metrics-apiserver/tree/master/pkg/provider)の中にあり、Custom Metrics Serverを実装する場合は[CustomMetricsProvider](https://github.com/kubernetes-sigs/custom-metrics-apiserver/blob/master/pkg/provider/interfaces.go#L70-L98)を実装します。

```go
type CustomMetricsProvider interface {
	// 条件にマッチするカスタムメトリクスのリストを返す
	GetMetricByName(name types.NamespacedName, info CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValue, error)
	GetMetricBySelector(namespace string, selector labels.Selector, info CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error)

	// その時点におけるメトリクスサーバで利用可能なメトリクス情報のリストを返す
	ListAllMetrics() []CustomMetricInfo
}
```

External Metrics Serverを実装する場合は[ExternalMetricsProvider](https://github.com/kubernetes-sigs/custom-metrics-apiserver/blob/master/pkg/provider/interfaces.go#L104-L108)を実装します。今回は外部メトリクスサーバなので、こちらを実装しています。

```go
type ExternalMetricsProvider interface {
	// 条件にマッチする外部メトリクスのリストを返す
	GetExternalMetric(namespace string, metricSelector labels.Selector, info ExternalMetricInfo) (*external_metrics.ExternalMetricValueList, error)
	// その時点におけるメトリクスサーバで利用可能なメトリクス情報のリストを返す
	ListAllExternalMetrics() []ExternalMetricInfo
}
```

## GetExternalMetric

`GetExternalMetric`は呼び出される時、`namespace(string)`と`labels.Selector`と`ExternalMetricInfo`が引数として渡されます。
第一引数のnamespaceはHPAのnamespaceが渡され、アクセス制御やメトリクスの識別のために利用しても利用しなくても良いみたいです。
第二引数にはHPAの`spec.metrics.external.metric.selector`が`labels.Selector`として渡されます。labels.Selectorを利用するために、外部から取得するメトリクス(今回の場合はPrometheus)のラベルやタグをKubernetesのlabels.Setへ変換することで、labels.Selectorを利用できるようになるため、今回はそのような方法をとりました。

```go
ls := labels.Set{}
// PrometheusのラベルをKubernetesのラベルへ変換
for k, v := range samples[i].Metric {
        ls[string(k)] = string(v)
}
// labels.Selectorを満たすかどうか確認
if !metricSelector.Matches(ls) {
        continue
}
```

戻り値には`ExternalMetricValueList`を返します。名前の通りメトリクスのリストのオブジェクトとなります。それぞれのメトリクスにはタイムスタンプ、メトリクス名、ラベル、メトリクスの値などを含むオブジェクトとなっています。このリストに含まれるメトリクスの合計値がHPAの結果の値として計算されることとなり、以下はKubernetes側のその部分のコードです。

```go
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/controller/podautoscaler/replica_calculator.go#L330-L337
// or
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/controller/podautoscaler/replica_calculator.go#L352-L359

metrics, timestamp, err := c.metricsClient.GetExternalMetric(metricName, namespace, metricLabelSelector)
if err != nil {
	return 0, 0, time.Time{}, fmt.Errorf("unable to get external metric %s/%s/%+v: %s", namespace, metricName, metricSelector, err)
}
utilization = 0
for _, val := range metrics {
	utilization = utilization + val
}
```

第三引数の`ExternalMetricInfo`はメトリクス名だけを含んでいる情報です。今回作ったメトリクスサーバでは、この情報を用いて紐づくPromQLをひっぱってきました。

```go
type ExternalMetricInfo struct {
	Metric string
}
```

## ListAllExternalMetrics

`ListAllExternalMetrics`では外部メトリクスとして利用可能なメトリクス名の一覧を返す必要があります。コンフィグに記載されているメトリクス名を単純に`ExternalMetricInfo`のリストとして返すだけの実装としてあります。

## Providerの登録

あとは上記の実装したProviderを[AdapterBase](https://github.com/kubernetes-sigs/custom-metrics-apiserver/blob/master/pkg/cmd/builder.go#L52)の関数`WithExternalMetrics`に渡して、`Run`するだけで、いい感じにAPI Aggregation LayerのAPIServiceに登録可能なカスタムAPIサーバとして起動してくれます。
(AdapterBaseという名前の通り、[このような書き方](https://github.com/kubernetes-sigs/custom-metrics-apiserver/blob/master/test-adapter/main.go#L39-L44)をする想定のものだと思うのですが今回は簡単のために以下のように利用しました。)
下記のコードは一部説明を簡単にするために省略しています。

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

        provider, err := NewPrometheusProvider(config)
        if err != nil {
                klog.Fatal(err)
        }
        cmd.WithExternalMetrics(provider)

        klog.Infof("starting sample metrics server")
        if err := cmd.Run(wait.NeverStop); err != nil {
                klog.Fatalf("unable to run custom metrics adapter: %v", err)
        }
}
```

あとはこれを適切なRole/ClusterRoleを与えて動かし、APIServiceとして登録すれば動きます。
メトリクスサーバとしてどの程度権限が必要になるのかはきちんと調べていなかったため、とりあえず[サンプル実装のもの](https://github.com/kubernetes-sigs/custom-metrics-apiserver/blob/master/test-adapter-deploy/testing-adapter.yaml)に対して[外部メトリクス用に少し修正したもの](https://github.com/uesyn/sample-metrics-server/blob/main/deploy/deploy.yaml)でデプロイしました。
正常にデプロイが完了し、設定が適切なものであれば、通常の外部メトリクスとしてHPAから利用できるようになっているはずです。

# 最後に

思っていたよりずっと簡単にメトリクスサーバを自作することができました。ドキュメントを見る限り、カスタムメトリクスサーバで必要なKubernetes APIリソースとの紐付けも、それほど面倒ではなさそうなので、そちらで実装しても良かったかもと思いました。これでどうやら社内監視基盤用にメトリクスサーバを作れ！と言われても「大丈夫です、作れます」と言えそうです。年末年始はテレビを見ながらこれをベースに、自分だけのメトリクスサーバの実装をして家で大人しく過ごそうと思います。