AWS Load Balancer Controller の Ingress Group を権限管理する

AWS Load Balancer Controller の Ingress Group 機能をマルチテナントクラスタでも安全に使うための権限管理を行う方法を紹介します。

これは [Kubernetes 2 Advent Calender 2020 14日目](https://qiita.com/advent-calendar/2020/kubernetes2) の記事です。

## AWS Load Balancer Controllerとは

[AWS Load Balancer Controller](https://aws.amazon.com/jp/about-aws/whats-new/2020/10/introducing-aws-load-balancer-controller/) は AWS の ALB や NLB をKubernetes の Ingress や Service 経由で管理するためのカスタムコントローラです。
もともと ALB Ingress Controller という名前で公開されていた Ingress Controller が v2 リリースのタイミングでリネームされました。

## Ingress Group

AWS Load Balancer Controller で追加された機能の中に [Ingress Group](https://kubernetes-sigs.github.io/aws-load-balancer-controller/guide/ingress/annotations/#ingressgroup) というものがあります。
これは Ingress の集合を表す AWS Load Balancer 上の概念で、これを使うと1つのロードバランサーインスタンスに複数の Ingress を同居させることができます。
ホビーユースから業務におけるまで、AWSのコストを最適化する上で非常に便利な機能と言えます。

ただしこの機能には使用上の注意点があります。それは Ingress Group を設定するためのインターフェースが単なる Ingress の annotation であるため、 この設定のスコープが namespace 分離されていないということです。
同じk8sクラスタに同居する複数のチームが同じ名前の Ingress Group を指定すると、それらが1つのロードバランサーの設定に統合されてしまい、意図しない設定を招くリスクがあります。多くの場合、ロードバランサーの設定ミスの影響は広範囲に及びます。

AWS Load Balancer Controller ではこの ingress group を安全に管理するための機能は提供されていません。なのでいまいまはユーザーがよしなに安全性を担保することになります。

## Validating Admission Webhook

そこで今回は [Validating Admission Webhook](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook) を使って Ingress Group を namespace で権限分離するポリシーを作成することを考えます。
具体的には、 [`alb.ingress.kubernetes.io/group.name`](http://alb.ingress.kubernetes.io/group.name) annotation に `$(namespace).` というプレフィックスを持たない文字列が入れられようとした時、そのリクエストをリジェクトするバリデーション機構を作ります。

## 実装方法

Admission Webhookを実装するメジャーな手段は大きく２つあります。

- controller-runtime で実装する
- Gatekeeper を使って Constraint を作成する

まあそれぞれ pros cons あるとは思うのですが、せっかくなので両方で実装してみました。それぞれ紹介した後最後に比較を書きます。

## kubebuilder / controller-runtime

今回は kubebuilder v2.3.1 を使います。

kubebuilder は `kubebuilder create webhook` というサブコマンドを持っており、一見これで admission webhook の scaffold ができそうなのですが、実はこれは CRD 用のコードを生成する用途でないと機能しません。今回はビルトインAPIである [networking.k8s.io](http://networking.k8s.io) を対象とするのでこのサブコマンドは使えません。

よって controller-runtime を直接使用することになります。

流れとしては、[公式ドキュメント](https://book.kubebuilder.io/reference/webhook-for-core-types.html)に案内されているように以下の手順で実装します。

1. `kubebuilder init` でアプリケーションの雛形を作る
2. `[admission.Handler`](https://pkg.go.dev/github.com/kubernetes-sigs/controller-runtime/pkg/webhook/admission#Handler) を実装した struct  `IngressValidator` を作る [https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/api/ingress_webhook.go](https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/api/ingress_webhook.go)
3. Controller Manager に `IngressValidator` を Regster する [https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/main.go#L71-L78](https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/main.go#L71-L78)

実装はこちらにあります。[https://github.com/dulltz/ingress-group-validator](https://github.com/dulltz/ingress-group-validator)

### テスト

controller-runtime で実装すると、当然 Go でテストを書けます。controller-runtime のテストには envtest を使うのがメジャーです。今回は Ingress の metadata を見るだけの話なので、何の問題もなくすんなりテストが書けます。

- [https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/api/suite_test.go](https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/api/suite_test.go)
- [https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/api/ingress_webhook_test.go](https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/api/ingress_webhook_test.go)

## Gatekeeper

Gatekeeper v3.2.2 を使います。

まず Rego でポリシーを記述します。Gatekeeper では `input.review.object` というフィールドにリクエストされた API オブジェクトが入ってきます。

Gatekeeper は `violation` ルールと `warn` ルールを admission の成否に使用します。conftest ではdeprecated なもののまだ使える `deny` は使えないので注意です。

また conftest との差異でいうと、戻り値に `violation{msg}` という形式を指定しても動作しないのでそれも注意です。

```
package ingressgroup

violation[{"msg": msg, "details": {"alb.ingress.kubernetes.io/group.name": group_name}}] {
  group_name := input.review.object.metadata.annotations["alb.ingress.kubernetes.io/group.name"]
  namespace := input.review.object.metadata.namespace
  expected := sprintf("%s%s", [namespace, "."])

  startswith(group_name, expected) == false
  msg := sprintf("you must format alb.ingress.kubernetes.io/group.name annotation as <namespace>.<group>. (e.g. %s.team-a)", [namespace])
```

次にこれをもとに ConstraintTemplate カスタムリソースを作成します。

```yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: ingressgroupformatpolicy
spec:
  crd:
    spec:
      names:
        kind: IngressGroupFormatPolicy
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package ingressgroup
        violation[{"msg": msg, "details": {"alb.ingress.kubernetes.io/group.name": group_name}}] {
          group_name := input.review.object.metadata.annotations["alb.ingress.kubernetes.io/group.name"]
          namespace := input.review.object.metadata.namespace
          expected := sprintf("%s%s", [namespace, "."])
          startswith(group_name, expected) == false
          msg := sprintf("you must format alb.ingress.kubernetes.io/group.name annotation as <namespace>.<group>. (e.g. %s.team-a)", [namespace])
        }
```

そしてここからかなり変わった挙動なのですが、この ConstraintTemplate リソースをクラスタに適用すると、Gatekeeper は IngressGroupFormatPolicy CRD を作成します。

そのあとユーザーが IngressGroupFormatPolicy カスタムリソースを作成することで、所望の Validating Webhook を実現できます。このリソースには、どの kind や label をもつ API オブジェクトに対して validating を実施するか、という設定を行えます。

```yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: IngressGroupFormatPolicy
metadata:
  name: ingress-group-format
spec:
  match:
    kinds:
      - apiGroups: ["extensions", "networking.k8s.io"]
        kinds: ["Ingress"]
```

```yaml
❯ cat e2etest/invalid-ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: invalid
  namespace: default
  annotations:
    alb.ingress.kubernetes.io/group.name: kube-system.test
spec:
  rules:
    - http:
        paths:
          - path: /testpath
            pathType: Prefix
            backend:
              serviceName: test
              servicePort: 8080
❯ kubectl apply -f e2etest/invalid-ingress.yaml
Error from server ([denied by ingress-group-format] you must format alb.ingress.kubernetes.io/group.name annotation as <namespace>.<group>. (e.g. default.team-a)): error when creating "STDIN": admission webhook "validation.gatekeeper.sh" denied the request: [denied by ingress-group-format] you must format alb.ingress.kubernetes.io/group.name annotation as <namespace>.<group>. (e.g. default.team-a)
```

## 比較

まず見て明らかなように、コード量は Gatekeeper のほうが圧倒的に少なくなります。実質十数行で validating webhook を実装できます。

ただし実装体験は Go のほうがずっと良いです。なぜかというと Gatekeeper の動作確認結果が分かりづらいからです。デバッグ時には一旦全てのリクエストを warn する Constraint を作ってその戻り値の msg に変数の値を格納したりします。
また`violation` は `violation[{"msg": msg}]` は受理されるけど `violation[msg]`は受理されない、といったような conftest との微妙な差異でハマったりしました。Gatekeeper を検討するユーザーの多くは conftest で Rego に慣れた方が多いと思うので要注意かなと思います。

ちなみに Conftest と Gatekeeper で同じ Rego を使い回す方法については[日経新聞社のアドベントカレンダー](https://qiita.com/advent-calendar/2020/nikkei)の24日で共有するので、もし興味ある方は御覧ください。

## おわりに

この記事では Admission Webhook で AWS Load Balancer Controller の Ingress Group をnamespace 分離するための方法を紹介しました。

なにかフィードバックあればQiitaコメント欄か[Twitter](https://twitter.com/dulltz)に書いて頂けると幸いです。