AWS Load Balancer Controller の Ingress Group 機能をマルチテナントクラスタでも安全に使うための権限管理を行う方法を紹介します。
これは Kubernetes 2 Advent Calender 2020 14日目 の記事です。
AWS Load Balancer Controllerとは
AWS Load Balancer Controller は AWS の ALB や NLB をKubernetes の Ingress や Service 経由で管理するためのカスタムコントローラです。
もともと ALB Ingress Controller という名前で公開されていた Ingress Controller が v2 リリースのタイミングでリネームされました。
Ingress Group
AWS Load Balancer Controller で追加された機能の中に Ingress Group というものがあります。
これは 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 を使って Ingress Group を namespace で権限分離するポリシーを作成することを考えます。
具体的には、 alb.ingress.kubernetes.io/group.name
annotation に $(namespace).
というプレフィックスを持たない文字列が入れられようとした時、そのリクエストをリジェクトするバリデーション機構を作ります。
実装方法
Admission Webhookを実装するメジャーな手段は大きく2つあります。
- 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 を対象とするのでこのサブコマンドは使えません。
よって controller-runtime を直接使用することになります。
流れとしては、公式ドキュメントに案内されているように以下の手順で実装します。
-
kubebuilder init
でアプリケーションの雛形を作る -
[admission.Handler
](https://pkg.go.dev/github.com/kubernetes-sigs/controller-runtime/pkg/webhook/admission#Handler) を実装した structIngressValidator
を作る https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/api/ingress_webhook.go - Controller Manager に
IngressValidator
を Regster する https://github.com/dulltz/ingress-group-validator/blob/f53cd69a95c53d6729effd9095821ab7371babb8/main.go#L71-L78
実装はこちらにあります。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/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 カスタムリソースを作成します。
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 を実施するか、という設定を行えます。
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: IngressGroupFormatPolicy
metadata:
name: ingress-group-format
spec:
match:
kinds:
- apiGroups: ["extensions", "networking.k8s.io"]
kinds: ["Ingress"]
❯ 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 を使い回す方法については日経新聞社のアドベントカレンダーの24日で共有するので、もし興味ある方は御覧ください。
おわりに
この記事では Admission Webhook で AWS Load Balancer Controller の Ingress Group をnamespace 分離するための方法を紹介しました。
なにかフィードバックあればQiitaコメント欄かTwitterに書いて頂けると幸いです。