Closed7

AWS Load Balancer Controller & Cilium で Gateway APIをNLBで使ってみたい

dekimasoondekimasoon

リンクメモ

やりたいこと

  • 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が必要になってくる、という感じだと思っている
dekimasoondekimasoon

まずはやることを整理する。
ざっくりだが以下になるだろう。

  1. AWS Load Balancer ControllerをHelmで入れる
  2. CiliumをGatewayAPIに対応させる。Gateway APIのページを見ると具体的には以下が必要そう。
    • 2a. Gateway APIのCRDsを入れる
    • 2b. kubeProxyReplacement=true, gatewayAPI.enabled=trueを設定してCiliumを入れる(kubeProxyReplacement=trueは以前対応済みなので今回は対象外)
  3. AWS Load Balancer ControllerのNLB用のannotationのベストプラクティスを理解する
  4. 3でまとめたannotationをinfrastructureフィールドに設定してGatewayリソースを作る
  5. NLBが作られることを確認して、適当に疎通確認してみる

注意点としては、infrastructureフィールドが追加されたのは結構最近らしいので、対応しているバージョンなのかどうかをチェックしつつ進めること。自戒。

dekimasoondekimasoon

1. AWS Load Balancer ControllerをHelmで入れる

公式のガイドはこちら。

https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.6/deploy/installation/

宗教上の理由で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を理解するところで解決しよう。

dekimasoondekimasoon

2. CiliumをGatewayAPIに対応させる

とりあえず公式ドキュメント
https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/#gs-gateway-api

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に存在するか確認する。

https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.0.0

リリースノートだけではわからない…。
でも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
dekimasoondekimasoon

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という仕組みが用意されているらしい。

流れとしては、以下だと思われる。

  1. 空っぽのTargetGroupを用意する
  2. ServiceをNodePortで立てる(今回の場合はGatewayリソースから作られるService)
  3. TargetGroupをServiceとの紐づけ情報をTargetGroupBindingリソースとして作成
  4. 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>
dekimasoondekimasoon

4. 3でまとめたannotationをinfrastructureフィールドに設定してGatewayリソースを作る

もともとinfrastructureフィールドの利用を想定していたが、LBはPulumi側で作成してTargetGroupBindingでBindingするだけになったのでシンプルにGatewayリソースを作ってみる。

と思ったらGateway Controllerが作るServiceリソースのtypeを指定する方法が無い?
嘘でしょう…。でもリファレンスを見ても無さそうな感じである。

https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.Gateway

もしかするとCilium側にあるかなと思ったけど無い。
代わりにServiceをNodePortで作れるようにして!というIssueを発見。
さらにIstioではnetworking.istio.io/service-typeという独自アノテーションが用意されているらしいことも発見。つまりCiliumでは未対応であり、どうしようもない…。


と思えたが、以下で試してみる。

  1. AWS Load Balancer ControllerのfeatureGates.EnableServiceControllerをfalseにすることで、LoadBalancerタイプのServiceをつくってもLBが作成されないように設定可能らしいので試してみる
  2. LoadBalancerタイプでもNodePortは空いているのでinstanceタイプでTargetGroupBindingする
  3. 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>
dekimasoondekimasoon

まとめ

  • 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。
このスクラップは4ヶ月前にクローズされました