AWS Load Balancer Controller & Cilium で Gateway APIをNLBで使ってみたい
リンクメモ
やりたいこと
- Ciliumのv1.15でGateway APIのinfrastructureフィールドに対応したらしい。PRはこちら。
- Gateway APIのinfrastructureフィールドとは、Gatewayリソースのinfrastructureフィールドにlableやannotationを設定でき、それがGatewayコントローラーによって作成されるServiceリソースに伝搬されるというもの
- つまり、このinfrastructureフィールドにAWS Load Balancer Controller用のannotationを設定することで、作成されるLBをコントロールできるようになるはず、という感じである
- 今回はこれを使って、Gatewayリソース作成時に良い感じのNLBが作成されることを確認したい
補足
- NLBを作るだけであればAWS Load Balancer Controllerは不要で、EKSに標準で入っているcloud-provider-awsでも
service.beta.kubernetes.io/aws-load-balancer-type: nlb
というannotationを付与すればいけるらしい - ただしcloud-providerに実装されている機能は最低限で、NLBにSecurityGroupを設定したりなどしたい場合はAWS Load Balancer Controllerが必要になってくる、という感じだと思っている
まずはやることを整理する。
ざっくりだが以下になるだろう。
- AWS Load Balancer ControllerをHelmで入れる
- CiliumをGatewayAPIに対応させる。Gateway APIのページを見ると具体的には以下が必要そう。
- 2a. Gateway APIのCRDsを入れる
- 2b.
kubeProxyReplacement=true
,gatewayAPI.enabled=true
を設定してCiliumを入れる(kubeProxyReplacement=trueは以前対応済みなので今回は対象外)
- AWS Load Balancer ControllerのNLB用のannotationのベストプラクティスを理解する
- 3でまとめたannotationをinfrastructureフィールドに設定してGatewayリソースを作る
- NLBが作られることを確認して、適当に疎通確認してみる
注意点としては、infrastructureフィールドが追加されたのは結構最近らしいので、対応しているバージョンなのかどうかをチェックしつつ進めること。自戒。
1. AWS Load Balancer ControllerをHelmで入れる
公式のガイドはこちら。
宗教上の理由でPulumi(Terraform的なやつ)を使っているので以下のような実装になった。
抜粋なので、参照している部分は察してほしい。
ChartのバージョンとAPIのバージョンがズレているので、バージョン指定したい方は注意。
this.release = new k8s.helm.v3.Release(
"release",
{
chart: "aws-load-balancer-controller",
namespace,
version: "1.6.2",
repositoryOpts: {
repo: "https://aws.github.io/eks-charts",
},
values: {
clusterName: args.cluster.cluster.name,
region: aws.config.region,
vpcId: args.vpcId,
serviceAccount: {
create: false,
name: sa.metadata.name,
},
},
},
this.k8sOpts,
)
pulumi up
で適応して問題なし。確認してみる。
✗ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
cilium-298vr 1/1 Running 0 11h
cilium-5r6r8 1/1 Running 0 11h
cilium-operator-844b7cf95b-b6qgx 1/1 Running 0 11h
cilium-operator-844b7cf95b-fmzhq 1/1 Running 0 11h
coredns-5488df4cc7-7bdqq 1/1 Running 0 11h
coredns-5488df4cc7-d2ww8 1/1 Running 0 11h
eks-pod-identity-agent-nfj2x 1/1 Running 0 11h
eks-pod-identity-agent-ns2kv 1/1 Running 0 11h
release-a0be8526-aws-load-balancer-controller-59ffd7f5db-2dslh 1/1 Running 0 49m
release-a0be8526-aws-load-balancer-controller-59ffd7f5db-mrq2r 1/1 Running 0 49m
名前が気になるがpodはいそう。
✗ kubectl get deploy -n kube-system
NAME READY UP-TO-DATE AVAILABLE AGE
cilium-operator 2/2 2 2 11h
coredns 2/2 2 2 11h
release-a0be8526-aws-load-balancer-controller 2/2 2 2 56m
deploymentもいる。でも名前が大変気になる。
いったん無視して動作を確認してみる。
✗ kubectl create ns nginx
namespace/nginx created
✗ kubectl apply -f nginx.yaml
deployment.apps/nginx created
✗ kubectl get po -n nginx
NAME READY STATUS RESTARTS AGE
nginx-cc558d6d5-cssfg 1/1 Running 0 21s
nginx-cc558d6d5-xq57m 1/1 Running 0 21s
✗ kubectl expose deployment -n nginx nginx --type=LoadBalancer --port=80
service/nginx exposed
✗ kubectl get svc -n nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 172.20.153.46 k8s-nginx-nginx-e0b6f331ae-acd04099ee3981a2.elb.ap-northeast-1.amazonaws.com 80:31601/TCP 23s
AWS上にNLBが作成されていることを確認。
AWS Load Balancer Controllerを入れる前はELB Classicが作成されてたので、うまく動いているのだろう。
ところがcurlで叩いてみたら返事がない。
✗ curl k8s-nginx-nginx-e0b6f331ae-acd04099ee3981a2.elb.ap-northeast-1.amazonaws.com
よくみたらinternalなNLBになっている。
デフォルトだとinternalなNLBになるのかな?
この辺は後でannotationを理解するところで解決しよう。
2. CiliumをGatewayAPIに対応させる
とりあえず公式ドキュメント
a. Gateway APIのCRDsを入れる
もともとEKSに入ってないかなと思って確認してみる。
✗ kubectl get crd
NAME CREATED AT
amazoncloudwatchagents.cloudwatch.aws.amazon.com 2024-01-04T15:01:22Z
ciliumcidrgroups.cilium.io 2024-01-04T15:01:25Z
ciliumclusterwidenetworkpolicies.cilium.io 2024-01-04T15:01:25Z
ciliumendpoints.cilium.io 2024-01-04T15:01:25Z
ciliumexternalworkloads.cilium.io 2024-01-04T15:01:25Z
ciliumidentities.cilium.io 2024-01-04T15:01:25Z
ciliuml2announcementpolicies.cilium.io 2024-01-04T15:01:25Z
ciliumloadbalancerippools.cilium.io 2024-01-04T15:01:25Z
ciliumnetworkpolicies.cilium.io 2024-01-04T15:01:26Z
ciliumnodeconfigs.cilium.io 2024-01-04T15:01:25Z
ciliumnodes.cilium.io 2024-01-04T15:01:25Z
ciliumpodippools.cilium.io 2024-01-04T15:01:25Z
cninodes.vpcresources.k8s.aws 2024-01-04T14:54:59Z
eniconfigs.crd.k8s.amazonaws.com 2024-01-04T14:54:57Z
ingressclassparams.elbv2.k8s.aws 2024-01-05T01:37:37Z
instrumentations.cloudwatch.aws.amazon.com 2024-01-04T15:01:22Z
policyendpoints.networking.k8s.aws 2024-01-04T14:54:57Z
securitygrouppolicies.vpcresources.k8s.aws 2024-01-04T14:54:59Z
targetgroupbindings.elbv2.k8s.aws 2024-01-05T01:37:37Z
無いと思われる。では入れてみる。
Standard ChannelとExperimental Channelがあるらしい。
infrastructureフィールドがStandard Channelに存在するか確認する。
リリースノートだけではわからない…。
でもExperimental FeatrureってなっているからたぶんExperimental Channelだな。
直接yamlをDLしてinfrastructure
で検索したらExperimental Channelの方にしか定義がなかった。次は最初からyamlに当たろう…。
Pulumiで実装する。
new k8s.yaml.ConfigFile(
"config-file",
{
file: "https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml",
},
this.opts,
)
pulumi upしたら差分がかなり出てきた。
✗ pulumi up
Previewing update (example):
Type Name Plan
pulumi:pulumi:Stack stack8-example
└─ stack8 stack8
└─ stack8:kubernetes kubernetes
+ └─ stack8:kubernetes:GatewayAPI gateway-api create
+ └─ kubernetes:yaml:ConfigFile config-file create
+ ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition referencegrants.gateway.networking.k8s.io create
+ ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition backendtlspolicies.gateway.networking.k8s.io create
+ ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition tlsroutes.gateway.networking.k8s.io create
+ ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition gatewayclasses.gateway.networking.k8s.io create
+ ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition udproutes.gateway.networking.k8s.io create
+ ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition tcproutes.gateway.networking.k8s.io create
+ ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition grpcroutes.gateway.networking.k8s.io create
+ ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition gateways.gateway.networking.k8s.io create
+ └─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition httproutes.gateway.networking.k8s.io create
Ciliumが必要/対応としているCRD以外のやつ(grpcroutesとか、udproutesとか)も入っちゃうけど、特に問題はないはずなので、このまま適応してみる。
✗ kubectl get crd | grep gateway
backendtlspolicies.gateway.networking.k8s.io 2024-01-05T04:44:43Z
gatewayclasses.gateway.networking.k8s.io 2024-01-05T04:44:44Z
gateways.gateway.networking.k8s.io 2024-01-05T04:44:44Z
grpcroutes.gateway.networking.k8s.io 2024-01-05T04:44:44Z
httproutes.gateway.networking.k8s.io 2024-01-05T04:44:44Z
referencegrants.gateway.networking.k8s.io 2024-01-05T04:44:43Z
tcproutes.gateway.networking.k8s.io 2024-01-05T04:44:44Z
tlsroutes.gateway.networking.k8s.io 2024-01-05T04:44:44Z
udproutes.gateway.networking.k8s.io 2024-01-05T04:44:43Z
OKだろう。
b. gatewayAPI.enabled=trueを設定してCiliumを入れる
既にCiliumは入れているのでhelmでのインストール時の引数を修正するだけ。
入れる前のstatusを取得しておく。
✗ kubectl -n kube-system exec ds/cilium -- cilium status
Defaulted container "cilium-agent" out of: cilium-agent, config (init), mount-cgroup (init), apply-sysctl-overwrites (init), mount-bpf-fs (init), clean-cilium-state (init), install-cni-binaries (init)
KVStore: Ok Disabled
Kubernetes: Ok 1.28+ (v1.28.4-eks-8cb36c9) [linux/amd64]
Kubernetes APIs: ["EndpointSliceOrEndpoint", "cilium/v2::CiliumClusterwideNetworkPolicy", "cilium/v2::CiliumEndpoint", "cilium/v2::CiliumNetworkPolicy", "cilium/v2::CiliumNode", "cilium/v2alpha1::CiliumCIDRGroup", "core/v1::Namespace", "core/v1::Pods", "core/v1::Service", "networking.k8s.io/v1::NetworkPolicy"]
...
gatewayAPI.enabled=true
を設定してhelmをupgrade。
その後、ドキュメントにあるようにcilium用のpodを再起動して、statusを取得してみる。
✗ kubectl -n kube-system rollout restart deployment/cilium-operator
deployment.apps/cilium-operator restarted
✗ kubectl -n kube-system rollout restart ds/cilium
daemonset.apps/cilium restarted
✗ kubectl -n kube-system exec ds/cilium -- cilium status
Defaulted container "cilium-agent" out of: cilium-agent, config (init), mount-cgroup (init), apply-sysctl-overwrites (init), mount-bpf-fs (init), clean-cilium-state (init), install-cni-binaries (init)
KVStore: Ok Disabled
Kubernetes: Ok 1.28+ (v1.28.4-eks-8cb36c9) [linux/amd64]
Kubernetes APIs: ["EndpointSliceOrEndpoint", "cilium/v2::CiliumClusterwideEnvoyConfig", "cilium/v2::CiliumClusterwideNetworkPolicy", "cilium/v2::CiliumEndpoint", "cilium/v2::CiliumEnvoyConfig", "cilium/v2::CiliumNetworkPolicy", "cilium/v2::CiliumNode", "cilium/v2alpha1::CiliumCIDRGroup", "core/v1::Namespace", "core/v1::Pods", "core/v1::Secrets", "core/v1::Service", "networking.k8s.io/v1::NetworkPolicy"]
...
直接的にGatewayAPIの記述はないけどKubernetes APIsの箇所にcilium/v2::CiliumClusterwideEnvoyConfig
, cilium/v2::CiliumEnvoyConfig
, "core/v1::Secrets"が増えている。たぶん大丈夫なんだろう。
GatewayClassを見てみたらciliumのGatewayClassができていた。
✗ kubectl get gatewayclass -o wide
NAME CONTROLLER ACCEPTED AGE DESCRIPTION
cilium io.cilium/gateway-controller True 3d23h
3. AWS Load Balancer ControllerのNLB用のannotationのベストプラクティスを理解する
そもそもALBではなくてNLBにしたい前提としては、
- EKSの前に立てるLBは特に仕事しない(LB的なことはCiliumが行う)ので、NLBにできるなら嬉しい
- 最近NLBにもSecurityGroupが設定できるようになったらしい
- NLBにはWAFが設定できないが、CloudFront + WAF -> NLBにして、NLBのSecurityGroupでWAF以外のIPをブロックすればALBと比較してもセキュリティ面でデメリットが少ないのでは
という感じである。
NLBに求められている要件としては、
- SecurityGroupが設定できる
- CloudFront -> NLB間を暗号化できるようにTLS設定しておきたい
- 適当なドメインを振る必要がある
くらいのはず。
サポートするドメインについての考慮
- cert-managerとexternal-dnsでk8sが管理できる範囲でDNS, SSL証明書については自動的に管理できそう
- しかしCloudFrontやNLBのTLSに関する設定更新については、少なくともAWS Load Balancer Controllerだけでは足りない
- なので、サポートするのは
example.com & *.example.com
の範囲に限定して、複数のexample1.com, example2.com
のようなサポートは一旦諦めてみる。 - そうすれば、Pulumiで事前にCloudFront & NLB用の証明書はACMで取得しておける
- nlbには
nlb.example.com
のような適当なドメインを振っておく
そうすると、やっぱりCloudFrontとNLBはPulumi側で管理したくなる。
AWS Load Balancer Controllerには、外部管理されているLB(に紐づくTargetGroup)と、ServiceとをBindしてくれるTargetGroupBinding CRDという仕組みが用意されているらしい。
流れとしては、以下だと思われる。
- 空っぽのTargetGroupを用意する
- ServiceをNodePortで立てる(今回の場合はGatewayリソースから作られるService)
- TargetGroupをServiceとの紐づけ情報をTargetGroupBindingリソースとして作成
- AWS Load Balancer ControllerがTargetGroupBindingをもとにTargetGroupにServiceのEndpointを設定してくれる
3.5 PulumiでNLB関連のリソースを実装
以下の感じでNLBとTargetGroup, ListenerをPulumiで追加。
Listnerはいったんhttpのみ検証してみる。
const nlb = new aws.lb.LoadBalancer(
"nlb",
{
name: tags.Name,
internal: false,
loadBalancerType: "network",
securityGroups: [nlbSG.id],
subnets: subnets.map(x => x.id),
tags,
},
opts,
)
const tg = new aws.lb.TargetGroup(
"target-group",
{
name: tags.Name,
vpcId: vpc.id,
targetType: "ip",
protocol: "TCP",
port: 80,
},
opts,
)
const listener = new aws.lb.Listener(
"https-listner",
{
loadBalancerArn: nlb.arn,
protocol: "TCP",
port: 80,
defaultActions: [
{
type: "forward",
targetGroupArn: tg.arn,
},
],
},
opts,
)
LBの作成はOKそう。
次にTargetGroupBindingを作ってみる。適当に立てたnginxに対してBindingしてもらう。
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
namespace: nginx
name: tgb
spec:
serviceRef:
name: nginx
port: 80
targetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:017057219515:targetgroup/s8-stack8-example/94ce755912160bf9
targetType: ip
networking:
ingress:
- from:
- securityGroup:
groupID: sg-0d409006514866ceb
ports:
- protocol: TCP
applyするとすぐにTargetGroupにServiceのEndpointが登録された。
しかし、なぜかnetworking.ingress.from.securityGroup.groupID
などまでちゃんと指定しないとunhealtyになってしまった。関係ないような気もするのだけど…なにかしら関係があるのかもしれない。
NLBに振られたDNSにhttpでアクセスすると返ってくる!やった。
✗ curl s8-stack8-example-xxxxxxxxxxx.elb.ap-northeast-1.amazonaws.com | grep title
<title>Welcome to nginx!</title>
4. 3でまとめたannotationをinfrastructureフィールドに設定してGatewayリソースを作る
もともとinfrastructureフィールドの利用を想定していたが、LBはPulumi側で作成してTargetGroupBindingでBindingするだけになったのでシンプルにGatewayリソースを作ってみる。
と思ったらGateway Controllerが作るServiceリソースのtypeを指定する方法が無い?
嘘でしょう…。でもリファレンスを見ても無さそうな感じである。
もしかするとCilium側にあるかなと思ったけど無い。
代わりにServiceをNodePortで作れるようにして!というIssueを発見。
さらにIstioではnetworking.istio.io/service-type
という独自アノテーションが用意されているらしいことも発見。つまりCiliumでは未対応であり、どうしようもない…。
と思えたが、以下で試してみる。
- AWS Load Balancer ControllerのfeatureGates.EnableServiceControllerをfalseにすることで、LoadBalancerタイプのServiceをつくってもLBが作成されないように設定可能らしいので試してみる
- LoadBalancerタイプでもNodePortは空いているのでinstanceタイプでTargetGroupBindingする
- Gatewayに対してHttpRouteをつくってみる
1. AWS Load Balancer ControllerのfeatureGates.EnableServiceController=falseを試す
すんなりできた。
helmからも制御できるようになっていた。
以下のようにEXTERNAL-IP
のとこがPendingになったまま。NLBも作られていない。
✗ kubectl get svc cilium-gateway-gateway -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
cilium-gateway-gateway LoadBalancer 172.20.18.49 <pending> 80:31489/TCP 10h
2. instanceタイプでTargetGroupBindingする
先程作ったやつのtargetTypeをinstanceにして作成。
NLB側のTargetGroupもtargetTypeをinstanceにする。
コードは割愛。
先に作っておいたNLB側のTargetGroupにもノードインスタンスが登録されていることを確認。
ヘルスチェックも通っているが、念のためcatnetで疎通確認してみる。
Connection to s8-stack8-example-xxxxxxxxxxxxxxxx.elb.ap-northeast-1.amazonaws.com (57.181.51.50) 80 port [tcp/http] succeeded!
大丈夫そう。netcatコマンドは知らなかった。便利である。
3. Gatewayに対してHttpRouteをつくってみる
以下のような感じで作成した。
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
namespace: my-nginx
name: nginx
spec:
parentRefs:
- name: gateway
namespace: kube-system
rules:
- backendRefs:
- name: nginx
port: 80
呼び出せることも確認!やったね。
✗ curl s8-stack8-example-xxxxxxxxxxxxxxxx.elb.ap-northeast-1.amazonaws.com | grep title
<title>Welcome to nginx!</title>
まとめ
- AWS Load Balancer Controller & Cilium & NLB で Gateway API は利用可能
- CiliumではGatewayに紐づくServiceのtypeを指定できず、必ずtype=LoadBalancerになってしまうのが注意点
- AWS Load Balancer ControllerにLBの管理を任せても良いなら、
infrastructure
フィールドに対応したCiliumのv1.5以降とGatewayAPIのv1.0以降のExperimental ChannelのCRDsを入れて、infrastructure
フィールドにAWS Load Balancer Controller用のannotationを記載すればOK。 - TerraformやPulumiでLBを管理したいなら、AWS Load Balancer Controllerの
controllerConfig.featureGates.EnableServiceController=false
を設定して、AWS Load Balancer Controller がLBを作成しないようにした上で、TargetGroupBindingを使って自前で立てたLBとCiliumが作ったServiceを紐づければOK。
- AWS Load Balancer ControllerにLBの管理を任せても良いなら、