prometheus-adapterの設定を完全理解したい

kubernetesのオートスケールはデフォルトでhorizontal pod autoscalerが用意されています
https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/

cpuやメモリの使用率でスケールさせたい場合は大体これで事足りる（もちろん足りない場合もあるが）のですが、リクエスト数やキュー数などでスケールさせたい、という場面が出てくると思います
そういう場合は、外部メトリクスをHPAの指標にすることができます
そのためのアダプターはいくつかありますが、私はこちらを使って、prometheusのメトリクスをHPAの指標に変換しています

https://github.com/kubernetes-sigs/prometheus-adapter

が、設定が少し難しくてちゃんと理解せず雰囲気で使っていたので、今回理解を目指します
なかなかうまく言葉にできませんでした

# 資料

prometehus-adapterのドキュメント
https://github.com/kubernetes-sigs/prometheus-adapter/tree/master/docs
ここに全部書いてあるので、それを解読するだけの記事になります

HPAについて
https://kubernetes.io/ja/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/
`type`の種類、Pods Object External が今回関連してくるものなので押さえておきます

# 種類
大きな分類で3種類あります

- rules
    - apiServiceで定義した、`custom.metrics.k8s.io`で使用する
    - hpaでは `type: Pods` `type: Object` で使用する
- resourceRules
    - hpaデフォルトで使用できる`recouces`を置き換えるものなのではないか、と思っている
    - ドキュメントに情報がないので今回これは放置で
- externalRules
    - apiServiceで定義した、`external.metrics.k8s.io`で使用する
    - hpaでは `type: External` で使用する

# rules

設定例

```yaml
rules:
- seriesQuery: '{__name__=~"^container_.*",container!="POD",namespace!="",pod!=""}'
  seriesFilters:
    - isNot: "^container_fs_.+"
  resources:
    overrides:
      namespace:
        resource: namespace
      pod:
        resource: pod
  name:
    matches: ^container_(.*)_seconds_total$
    as: ""
  metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,container!="POD"}[1m])) by (<<.GroupBy>>)
```

設定項目を上から順に見ていきます

## seriesQuery

prometheusのどのメトリクスを変換するかを決めます

- 変換したいメトリクスがひとつだけの場合は、単純にメトリクス名を直接書くだけでよい
- 複数のメトリクスを変換したい場合は、クエリ形式でまとめて取得することもできる
    - その場合は例のように`__name__`を使用してごそっと取得
    - その他のlabelのフィルタも必要に応じて設定する
- ここではあくまで、どのメトリクス（series）を変換対象にするか、を決める

## seriesFilters

seriesQueryで取得したメトリクスを更に正規表現でフィルタリングします
例えば、`__name__`で一回で絞り込みきれなかった場合などに使用します

- `is` で一致したものを残す
- `isNot` で一致したものを除外

## resources

メトリクスとkubernetesのresourceを繋ぐ設定です
ここが設定のキモなんですが、文章で説明するのが難しい部分です

例えばpodの場合だと、

```yaml
resources:
  overrides:
    <<メトリクスのnamespaceのlabel名>>:
      resource: namespace
    <<メトリクスのpodのlable名>>:
      resource: pod
```

という形になります
もう少し具体的な例を挙げると

`some_metrics{kubernetes_namespace="samplenamespace",kubernetes_pod_name="samplepodname"}`
というメトリクスを変換したい場合、seriesQueryで`some_metrics`を指定した上で

```yaml
resources:
  overrides:
    kubernetes_namespace:
      resource: namespace
    kubernetes_pod_name:
      resource: pod
```

という形になります
podだけでなく、全てのresourceの変換が可能です
実はapi-groupを指定する必要があるのですが、podはcore apiなので指定不要になっています
例えばingressの場合だと

```yaml
resources:
  overrides:
    <<メトリクスのnamespaceのlabel名>>:
      resource: namespace
    <<メトリクスのingressのlable名>>:
      group: networking.k8s.io
      resource: ingress
```
となります、意外と定型文な部分が多いです

以下のように対応するlabel名が同じになっていて、一見設定不要な形になっている場合も定義を書く必要があります
`some_metrics{namespace="mynamespace",pod="mypodname"}`

```yaml
resources:
  overrides:
    namespace:
      resource: namespace
    pod:
      resource: pod
```

また、置き換えパターンが決まっている場合、テンプレートで一括変換可能です
例えば

```yaml
resources:
  overrides:
    kubernetes_pod_name:
      resource: "pod"
    kubernetes_namespace_name:
      resource: "namespace"
```

この設定は、以下のようにも設定可能です

```yaml
resources:
  template: "kubernetes_<<.Resource>>_name"
```

変数になっている部分は、`<<.Group>>`と`<<.Resource>>`が使用可能みたいですが、Groupは推測されるので必須ではないとかなんとか（ここちょっと追いきれてないです、使わなさそうなので…）


## name

HPAを設定する時に使用する名前を決められます

`matches`で正規表現を使って元のメトリクス名からキャプチャして
`as`でそのキャプチャ結果を使用して名前を決められます

```yaml
name:
  matches: "^(.*)_total$"
  as: "${1}_per_second"
```

この設定の場合、`container_requests_total`という名前のメトリクスは`container_requests_per_second`という名前でHPAで使用できます

matchesの指定がない場合、デフォルトは `.*` になり、matchesにキャプチャ（括弧）が使用されていない場合、asのデフォルトは`$0`となります
何も指定しないとメトリクス名がそのまま使用されるということですかね

ｍatchesでキャプチャが使われていると、asのデフォルトは`$1`になります

（このあたり軽く謎仕様の雰囲気を感じます）

## metricsQuery

対象になるseriesに対して、メトリクスとなる値を算出するためのクエリを定義します
以下の変数（テンプレート？）を使用しています

- <<.Series>>
    - メトリクス名
    - seriesQueryで`__name__`で正規表現を使わずに単一のメトリクスを設定する場合は、これを使わず直接メトリクス名を書くのでも問題ない
- <<.LabelMatchers>>
    - resourcesでの設定を使用して作られたlabel matcher
    - 上のresourceでの例だとこんな値になる
        - `kubenetes_pod_name=~"samplepodname",kubernetes_namespace=~"samplenamespace"`
    - 複数のpodのメトリクスを取得する場合は、正規表現で並ぶ `pod=~"mypod1|mypod2"`
    - 他にlabelを指定したい場合は、前後にコンマで繋げてlabelを追加できる
        - `{<<.LabelMatchers>>,container!="POD"}` `{container!="POD",<<.LabelMatchers>>}`
- <<.GroupBy>>
    - `.LabelMatchers`で使用されているのと同じlabelのkeyがコンマ区切りで入る

基本は以上の3つで事足りますが、以下も用意されています

- <<.LabelValuesByName>>
    - `.GroupBy`とは逆で、こっちは値のリスト
    - 複数labelのを全部混ぜるんだろうか？namespaceとpodとか…
    - `|`で区切り
- <<.GroupBySlice>>
    - GroupByのスライス…よくわからなかった…

# externalRules

podなどのリソースと紐付けにくいメトリクスを指標に変換したい場合はこちらのexternalRulesを使用します
worker処理待ちのキュー数などがこれになります

使用例

```yaml
externalRules:
- seriesQuery: '{__name__=~"^.*_queue_(length|size)$",namespace!=""}'
  resources:
    overrides:
      namespace:
        resource: namespace
  name:
    matches: ^.*_queue_(length|size)$
    as: "$0"
  metricsQuery: max(<<.Series>>{<<.LabelMatchers>>})
```

基本的には`rules`と同じ書式なのですが、`resources`でkubernetesのリソースと紐付ける部分が減っています
v0.8以前ではnamespaceが必要になっているので、hpaがあるnamespaceと同じnamespaceのlabelをメトリクスに付与しておく必要があります（key名は変換できるので同じ値のlabelがあればなんとかなる）
v0.9以降ではnamespaceを指定しない方法もできていますが、v0.8以降はkubernetes-external-secretと衝突する問題があるので私は使えていません…

# 実演

全体的にうまいこと説明できなかったので軽く実演して誤魔化すことにします
以下の設定でprometheus-adapterを起動します
ちなみにバージョンはv0.7

```yaml
rules:
- seriesQuery: 'container_threads'
  seriesFilters: []
  resources:
    overrides:
      namespace:
        resource: namespace
      pod:
        resource: pod
  name:
    matches: ""
    as: pod_test
  metricsQuery: 'sum(<<.Series>>{<<.LabelMatchers>>,container!="POD",container!=""}) by (<<.GroupBy>>)'
- seriesQuery: 'kube_service_created'
  seriesFilters: []
  resources:
    overrides:
      namespace:
        resource: namespace
      service:
        resource: service
  name:
    matches: ""
    as: service_test
  metricsQuery: 'time() - sum(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)'
externalRules:
- seriesQuery: container_fs_inodes_total
  seriesFilters: []
  resources:
    overrides:
      namespace:
        resource: namespace
  name:
    matches: ""
    as: "external_test"
  metricsQuery: 'sum(<<.Series>>{<<.LabelMatchers>>})'
```

使ってるメトリクスはcAdvisorとkube-state-metricsのメトリクスを取得していればあると思います、たぶん

- 1番目
    - メトリクス`container_threads`を変換して`pod_test`として使用
        - podで使えるメトリクスを適当に選んだ
    - resourceはpodで、hpaでは`Pods`で使用する
    - `metricsQuery`でlabel `container` がPODのものと空のものを除外する
- 2番目
    - メトリクス`kube_service_created`を変換して`service_test`として使用
    - resourceはserviceで、hpaでは`Object`で使用する
    - `metricsQuery`では、serviceが作られてからの経過時間を計算している
- 3番目
    - externalRules
    - メトリクス`container_fs_inodes_total`を変換して`external_test`として使用
        - 普通ならpodsで使うようなメトリクスだけど、デフォルトで出るメトリクスのうちexternalで使える良さそうなのが見当たらなかったので、externalとして使用する
    - `metricsQuery`でnamespace内にある全てのコンテナの`container_fs_inodes_total`を合計する
        - 値を出すためなので特に意味はない

この設定に対して、テスト用のdeploymentとserviceをapplyして

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  selector:
    app: nginx
  ports:
    - port: 80
      targetPort: 80
```

そこにhpaをapply

```yaml
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: nginx
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx
  minReplicas: 2
  maxReplicas: 5
  metrics:
    - type: Pods
      pods:
        metric:
          name: pod_test
        target:
          type: AverageValue
          averageValue: "10000"
    - type: Object
      object:
        describedObject:
          kind: service
          name: nginx
        metric:
          name: service_test
        target:
          type: AverageValue
          averageValue: "10000"
    - type: External
      external:
        metric:
          name: external_test
        target:
          type: Value
          value: "10000"
```

prometheus-adapterに設定した3種類をそれぞれ使用するようにHPAを設定します

- 共通
    - `type: Value`は取得した値をそのまま使う
        - 値がすでにpodの数で割られてるならこっちを使用
    - `type: AverageValue`は取得した値を現在のpodの数で割って使う
- 1番目
    - `type: Pods`
    - prometheus-adapterで設定したresourceがpodの場合は`type: Pods`を使用する
    - `Value`と`AverageValue`どっちを使っていいのか迷ったけど、Valueで設定しようとするとエラーになるのでAverageValueを使っておけばよさそう
- 2番目
    - `type: Object`
    - resourceがpod以外のrulesは`type: Object`を使用して、`describedObject`で対象リソース詳細を設定する
- 3番目
    - `type: External`
    - 意外と語ることがない

HPAをdescrieしてみます

```
$ kubectl describe hpa nginx
Name:                                                      nginx
Namespace:                                                 default
Labels:                                                    <none>
Annotations:                                               <none>
CreationTimestamp:                                         Sun, 12 Dec 2021 23:28:54 +0900
Reference:                                                 Deployment/nginx
Metrics:                                                   ( current / target )
  "pod_test" on pods:                                      3 / 10k
  "service_test" on service/nginx (target average value):  85760m / 10k
  "external_test" (target value):                          74 / 10k
Min replicas:                                              2
Max replicas:                                              5
Deployment pods:                                           2 current / 2 desired
```

HPAでちゃんと使えてますね

# デバッグ

ちなみにapiを直接叩いて設定できてるかの確認も可能です

rules
`kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta2`
externalRules
`kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1`

メトリクス詳細
rules
`kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta2/namespaces/<<namespace名>>/<<リソース種（podsなど）>>/<<pod名（全podの場合は*）>>/<<メトリクス名>>"`
externalRules
`kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/mynamespace/<<namespace名>>/<<メトリクス名>>"`

今回の場合だと

```
$ kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/pod_test
$ kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1/namespaces/default/service/*/service_test
$ kubectl get --raw /apis/external.metrics.k8s.io/v1beta1/namespaces/default/external_test
```

こんな感じでデバッグできます

# 以上
ちょっとまとまりがなさすぎる感じになってしまいました
いやはや難しい