k3s の Traefik Ingress で外部認証を実施 - CoreDNS メトリクスを確認

k3s 環境の Ingress で外部認証（カスタム認証）を行なってみます。

k3s ではデフォルトで [Traefik](https://github.com/traefik/traefik) が用いられており、Traefik の ```forwardAuth``` を設定する事で外部認証を行う事ができます。

ここでは、「[Lima で k3s 環境を構築し BuildKit でアプリをビルドして実行](https://qiita.com/fits/items/2596fd944025984bf881)」で構築した k3s 環境（Lima で実行）をそのまま利用する事にします。

# 1. 認証処理

単純な認証処理を Deno で実装してみました。

```Authorization``` ヘッダーの値が ```abc123``` だった場合にのみ、認証成功（HTTP ステータスコード 200）となるようにしています。

```typescript:auth/auth.ts
import { serve } from 'https://deno.land/std@0.182.0/http/server.ts'

const port = parseInt(Deno.env.get('PORT') ?? '8100')

const handler = (req: Request) => {
    req.headers.forEach((v, k) => console.log(`header ${k}:${v}`))
    console.log('-----')

    const v = req.headers.get('authorization') ?? ''

    if (v === 'abc123') {
        // 認証成功
        return new Response(undefined, { status: 200 })
    }

    return new Response(undefined, { status: 403 })
}

serve(handler, { port })
```

この認証処理をホスト OS（macOS）側で実行しておきます。

```sh:実行例
% deno run --allow-all auth/auth.ts
Listening on http://localhost:8100/
```

# 2. Ingress 設定

ホスト OS で実行している認証処理を Kubernetes 環境から呼び出せるように ```ExternalName``` の Service を作成します。

```yaml:auth-svc.yaml
kind: Service
apiVersion: v1
metadata:
  name: auth-svc
spec:
  type: ExternalName
  externalName: host.lima.internal
```

```sh:auth-svc.yaml 適用例
$ sudo kubectl apply -f auth-svc.yaml
```

Traefik v2 では ```forwardAuth``` 等の機能は Middleware となっており、```kind: Middleware``` リソースで設定できます。

下記では forwardAuth の他に ```stripPrefix```（URL のパスから prefix に合致する先頭を取り除く）も定義して、Ingress へ適用しています。

Middleware は ```traefik.ingress.kubernetes.io/router.middlewares``` の値として文字列で指定します。
個々の Middleware は ```<namespace>-<name>@kubernetescrd``` で指定でき、"```,```" で区切る事で複数指定できるようです。

```yaml:sample-ingress.yaml
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: sampleauth
spec:
  forwardAuth:
    address: http://auth-svc.default.svc.cluster.local:8100

---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: sample-prefix
spec:
  stripPrefix:
    prefixes:
      - /sample/

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: sample-ingress
  annotations:
    traefik.ingress.kubernetes.io/router.middlewares:
      default-sampleauth@kubernetescrd, default-sample-prefix@kubernetescrd
spec:
  rules:
  - http:
      paths:
      - path: /sample/
        pathType: Prefix
        backend:
          service:
            name: sampleapp
            port:
              number: 8080
```

これを適用して Ingress を作成します。

```sh:sample-ingress.yaml 適用例
$ sudo kubectl apply -f sample-ingress.yaml
```

ここで、forwardAuth の address の値を ```http://auth-svc:8100``` のようにすると、次のように名前解決に失敗するので注意が必要でした。（k3s の Traefik は kube-system 名前空間で実行しているためだと思われる）

```sh:エラー例
level=debug msg="Error calling http://auth-svc:8100. Cause: Get \"http://auth-svc:8100\": dial tcp: lookup auth-svc on 10.43.0.10:53: no such host" middlewareType=ForwardedAuthType middlewareName=default-sampleauth@kubernetescrd
```

このエラーを確認するには、Traefik のログレベルを変更する必要があり、次のような ```HelmChartConfig``` を適用します。

```yaml:traefik-config.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    additionalArguments:
      - "--log.level=DEBUG"
```

また、Traefik の HelmChartConfig で設定できる項目等は下記で確認できます。

* https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml

# 3. 動作確認

ホスト OS からアクセスして動作確認してみます。

まずは、認証に失敗するケースです。HTTP ステータスコードが 403 となりました。

```sh:動作確認1
% curl http://localhost/sample/test1 -v                 
*   Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /sample/test1 HTTP/1.1
> Host: localhost
> User-Agent: curl/7.87.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< Content-Length: 0
・・・
```

次に成功するケースです。
stripPrefix も機能しており、特に問題無さそうです。

```sh:動作確認2
% curl http://localhost/sample/test2 -H "Authorization: abc123"
ok:/test2
```

認証処理側の出力結果は次のようになりました。

```sh:認証処理の出力例
% deno run --allow-all auth/auth.ts
Listening on http://localhost:8100/
header accept:*/*
header accept-encoding:gzip
header host:auth-svc.default.svc.cluster.local:8100
header user-agent:curl/7.87.0
header x-forwarded-for:10.42.1.39
header x-forwarded-host:localhost
header x-forwarded-method:GET
header x-forwarded-port:80
header x-forwarded-proto:http
header x-forwarded-server:traefik-6b5c9ddb87-x528k
header x-forwarded-uri:/sample/test1
header x-real-ip:10.42.1.39
-----
header accept:*/*
header accept-encoding:gzip
header authorization:abc123
header host:auth-svc.default.svc.cluster.local:8100
header user-agent:curl/7.87.0
header x-forwarded-for:10.42.1.39
header x-forwarded-host:localhost
header x-forwarded-method:GET
header x-forwarded-port:80
header x-forwarded-proto:http
header x-forwarded-server:traefik-6b5c9ddb87-x528k
header x-forwarded-uri:/sample/test2
header x-real-ip:10.42.1.39
-----
```

x-forwarded-for 等に設定されている ```10.42.1.39``` の Pod の内容は以下の通りです。

```sh:Pod 内容
$ sudo kubectl -n kube-system describe pod/svclb-traefik-3c0b1c72-7pngl

Name:             svclb-traefik-3c0b1c72-7pngl
Namespace:        kube-system
Priority:         0
Service Account:  svclb
Node:             lima-limak3s/192.168.5.15
Start Time:       Sat, 29 Apr 2023 21:53:17 +0000
Labels:           app=svclb-traefik-3c0b1c72
                  controller-revision-hash=7884fc7946
                  pod-template-generation=1
                  svccontroller.k3s.cattle.io/svcname=traefik
                  svccontroller.k3s.cattle.io/svcnamespace=kube-system
Annotations:      <none>
Status:           Running
IP:               10.42.1.39
IPs:
  IP:           10.42.1.39
Controlled By:  DaemonSet/svclb-traefik-3c0b1c72
...省略
```

## レスポンスが遅くなる原因 （CoreDNS メトリクスを確認）

ここで、初回アクセスと時間を置いてからアクセスした場合のレスポンスが異様に遅いのが気になりました。

0.01秒ほどの処理が、この場合は以下のように 1秒程度かかります。

```sh:例
% time curl http://localhost/sample/test6 -H "Authorization: abc123"
...省略 0.00s user 0.01s system 0% cpu 1.022 total
```

調べてみたところ CoreDNS のメトリクスで気になる箇所を見つけました。

```prometheus:CoreDNS メトリクス例1
...省略
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="0.00025"} 0
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="0.0005"} 2
...省略
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="0.256"} 6
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="0.512"} 6
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="1.024"} 11
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="2.048"} 11
...省略
coredns_forward_request_duration_seconds_sum{rcode="NOERROR",to="192.168.5.3:53"} 5.018901210999999
coredns_forward_request_duration_seconds_count{rcode="NOERROR",to="192.168.5.3:53"} 11
...省略
```

上記の状態から ```http://localhost/sample/xxx``` へ 1回アクセスし、レスポンスに 1秒かかった後のメトリクスが以下です。

```prometheus:CoreDNS メトリクス例2
...省略
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="0.00025"} 0
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="0.0005"} 3
...省略
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="0.256"} 7
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="0.512"} 7
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="1.024"} 13
coredns_forward_request_duration_seconds_bucket{rcode="NOERROR",to="192.168.5.3:53",le="2.048"} 13
...省略
coredns_forward_request_duration_seconds_sum{rcode="NOERROR",to="192.168.5.3:53"} 6.020857919999999
coredns_forward_request_duration_seconds_count{rcode="NOERROR",to="192.168.5.3:53"} 13
...省略
```

全体的に 2カウント増えており、```le="0.0005"``` と ```le="1.024"``` でそれぞれ 1カウントずつ増えています。（累計になっており分かり難いかもしれませんが）

```le="1.024"``` のカウントが増えている事から、CoreDNS からの forward つまりは ```host.lima.internal``` の名前解決に 1秒程度かかってしまっているのが遅い原因だと考えられます。

更に、レスポンスが速い時（0.01秒程度）は上記 forward のカウントが増えなかったので、これが原因とみて間違いなさそうでした。

ちなみに、CoreDNS のメトリクスは port-forward してホスト OS 側から取得しました。

```sh:CoreDNS メトリクス取得用の port-forward 例
$ sudo kubectl port-forward -n kube-system service/kube-dns 9153:9153
Forwarding from 127.0.0.1:9153 -> 9153
Forwarding from [::1]:9153 -> 9153
```
```sh:メトリクス取得例
% curl http://127.0.0.1:9153/metrics
```