AWS Load Balancer ControllerのTargetGroupBindingを試す

# はじめに
※HISYSのアドベントカレンダー12日目の記事です。
最近私はAWS EKSをよく触っているのですが、先日EKS周りのアップデートとして気になるトピックが挙がっていました。

- [AWS Load Balancer Controller の紹介](https://aws.amazon.com/jp/about-aws/whats-new/2020/10/introducing-aws-load-balancer-controller/)
- [Introducing the AWS Load Balancer Controller](https://aws.amazon.com/jp/blogs/containers/introducing-aws-load-balancer-controller/)

今まで「ALB Ingress Controller」と呼ばれていた、AWSのALBをKubernetesリソースとして操作するためのモジュールの後継(v2)という立ち位置で、「AWS Load Balancer Controller」というコントローラがリリースされたというアップデートです。
v2になって新しく追加された機能としては、下記の通りです。

- NLBをサポート
- 複数のIngressリソースでALBの共有が可能に
- TargetGroupBindingというカスタムリソースが新しく作成されるようになった

この中で、今回は3番目のTargetGroupBindingについて書こうと思います。
まだネット上でこの機能について言及されているそんなに記事が多くないように感じますが、私的にはまさに抱えていた課題の解決にぴったりの機能だったので、少しでも同じ悩みを抱えている方の助けになれば幸いです。

# 従来のEKSとALB周りのパターンと課題
そもそも、冒頭で述べたALB Ingress Controllerとは、Kubernetes上でALB IngressというIngressリソースが作成されたときにAWS上のリソースであるALBをデプロイ、管理してくれるモジュールです。
Kubernetesでクラスター外からクラスター内のPodへのルーティングを提供する場合LoadBalancerのServiceを使うという方法もありますが、EKSでLoadBalancer Serviceを作成するとCLBがデプロイされます。
L7での制御が必要になるユースケースでは、できればCLBではなくALBを利用したいのですが、その際の選択肢として今回は以下の3つを考えてみます。

①ALB Ingress Controllerを利用してALBもKubernetes側からデプロイ、管理する
②AWS側でALBを作成し、ServiceをNodePortとして公開する
③AWS側でALBを作成し、NGINX Ingress ControllerをNodePortとして公開する

### ①ALB Ingress Controllerを利用してALBもKubernetes側からデプロイ、管理する
![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/c186c228-c164-9bda-43a1-3d85f6a659a5.png)

【メリット】
ALBの作成、Service（ClusterIPもしくはNodePort)へのルーティングをマニフェストで宣言的に管理することができる。
【デメリット】
ALBのライフサイクルがKubernetes側のライフサイクルと切り離せないため、その他AWSリソースとALBが連携されている場合などでは管理が複雑になる

### ②AWS側でALBを作成し、ServiceをNodePortとして公開する
![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/c706beeb-f436-0a0b-c2bc-5bf5e25653b3.png)

【メリット】
ALBとKubernetesの関係が、①よりは疎結合になっている
【デメリット】
TargetGroupとNodePortとして公開したポートの紐づけを行わなくてはいけないため、Service側で変更（例えばサービスの追加など）をしたときにALB側も修正しなくてはいけなくなる

### ③AWS側でALBを作成し、NGINX Ingress ControllerをNodePortとして公開する
![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/e8422c06-3db3-989e-1645-d2f4055375fb.png)

【メリット】
ALBとKubernetesの関係が完全に疎結合になっている
【デメリット】
NGINXをNodePortとして公開する必要があるが、その場合アクセス元IPの取得制限等がある
https://kubernetes.github.io/ingress-nginx/deploy/baremetal/#over-a-nodeport-service

以上の3ケースからの考察をまとめると、
・ALBのライフサイクルとKubernetesのライフサイクルを切り離し疎結合にしながらも、
・Serviceへのルーティング部分の管理性はKubernetes側に持たせて、
・NGINX Ingress ControllerをNodePortとして運用しなくていい
ソリューションが理想だと思われます。
そんなソリューションあるのだろうか…（棒読み

# それ、AWS Load Balancer TargetGroupBindingsでできます
予定調和感が否めませんが、AWS Load BalancerのTargetGroupBindingsを利用すれば、上記の課題が解決できると考えています。
![005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/2f526285-f24f-237f-009f-28267e842c63.png)
ALBおよびTargetGroupはCloudFormationやTerraform等AWS側のツールでデプロイを行い、Kubernetes側ではCustomResourceDefinitionであるTargetGroupBindingsというリソースを作成することで、AWS Load Balancer ControllerはTargetGroupからKubernetes Serviceへのルーティングのみを制御することが可能です。
これにより、KubernetesからALB自体のリソースのライフサイクルを切り離しつつ、いい感じにServiceへのルーティング等の管理をKubernetes側に移譲することができます。
今のところ、この分け方が一番管理的にはきれいになるのではないかと思っています。

# いざ実装

※下記環境で確認しています。
　OS : Amazon Linux2
　aws cli : aws-cli/1.18.147
　eksctl : 0.33.0
まずはサクッとEKSクラスターを作ってしまいましょう。

```
eksctl create cluster --name=sample-cluster --nodes=2 --node-type=t2.medium
```

30分弱ほどコーヒーでも飲みながら待ちましょう。
プロンプトが返ってきたら、kubectlで確認。

```
kubectl get nodes
NAME                                          STATUS   ROLES    AGE    VERSION
ip-192-168-34-63.us-west-2.compute.internal   Ready    <none>   3m1s   v1.18.9-eks-d1db3c
ip-192-168-6-110.us-west-2.compute.internal   Ready    <none>   3m2s   v1.18.9-eks-d1db3c
```
おお、できてるできてる。eksctlで作成されたVPC-IDをメモっておきましょう。あとで使います。
![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/19ef54bf-e97c-302a-1719-f9589211ff3f.png)
いい感じですね。
ではALBを作っていきます。
まずはTargetGroupから
![021.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/1564c278-9314-67e4-fb51-ee0fc6d30ca3.png)
今回はClusterIPにルーティングするので、target typeをIP addressにします。Target group nameを入力して、VPCはeksctlで作成されたvpcを選択します。
※注意！NodePortでサービスを公開する場合は、instanceを選択します。ここが間違っているとTargetGroupから作り直しになります。
他はすべてデフォルトで作成します。出来上がったTargetGroupのArnもメモっておきましょう。これも後で使います。
![022.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/c2ebfb40-54f8-72a1-cd1a-e8058e567f9a.png)
次はALBです。
![010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/c31f89af-6a84-e7da-b77e-4bc17c41e940.png)
ALBを選びましょう。
![011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/aca055ce-07b7-b923-bf5b-8404f2f9a533.png)
名前を入力します。
![012.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/7c137067-e45e-4e8b-f132-2725e24c5418.png)
VPCはeksctlで作成されたVPCを、サブネットはeksctlで作成されたPublicSubnetを選択。
![013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/95b438d5-d96b-f10c-2144-522f32dd1553.png)
次へ次へと押していき、手順3では一旦検証のために自分のマシンに対して80番を開けたSGを作成します。
![014.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/bbd0be34-376b-f4fc-8f67-9a1964359479.png)

手順4ルーティングの設定では先ほど作ったTargetGroupを指定しましょう。
![015.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/f8f80f8e-33cd-4ff7-6c2e-de32310872a5.png)
あとは次へを連打します。
![016.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/9c332010-5f55-367c-04dd-d00836b6d03a.png)

CLIに戻って、AWS Load Balancer Controllerを導入します。
今回はeksctlで作成したので、service account等の設定もeksctlだけで完結することもできるのですが、CloudFormationで作成した場合はそうもいきません。そんな人でも大丈夫なようにシェルを用意しました。
[eksctlでやる場合はこちら](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/alb-ingress.html)

```shell:AWSLoadBalancer.sh
CLUSTER_NAME=<your_cluster_name>
AWS_DEFAULT_REGION=<your_region_name>
VPC_ID=<cluster_vpc_id>

# oidc providerの作成及びeksclusterとの紐づけ
eksctl utils associate-iam-oidc-provider \
    --cluster $CLUSTER_NAME \
    --approve

# ALB Controller用のPolicy作成
curl -o iam-policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json
aws iam create-policy \
    --policy-name $CLUSTER_NAME-ALB-Policy \
    --policy-document file://iam-policy.json

IAM_ARN=$(aws iam list-policies --query 'Policies[?PolicyName==`'$CLUSTER_NAME'-ALB-Policy`].Arn' --output text)

# ALB Controller用のRole作成
ISSUER_URL=$(aws eks describe-cluster \
        --name $CLUSTER_NAME \
        --query cluster.identity.oidc.issuer \
        --output text)
ISSUER_HOSTPATH=$(echo $ISSUER_URL | cut -f 3- -d'/')
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
PROVIDER_ARN="arn:aws:iam::$ACCOUNT_ID:oidc-provider/$ISSUER_HOSTPATH"
cat > irp-trust-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "$PROVIDER_ARN"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${ISSUER_HOSTPATH}:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller"
        }
      }
    }
  ]
}
EOF
ROLE_NAME=$CLUSTER_NAME-ALB-IAM-Role
aws iam create-role \
        --role-name $ROLE_NAME \
        --assume-role-policy-document file://irp-trust-policy.json

aws iam update-assume-role-policy \
        --role-name $ROLE_NAME \
        --policy-document file://irp-trust-policy.json
aws iam attach-role-policy \
        --role-name $ROLE_NAME \
        --policy-arn $IAM_ARN
ALB_ROLE_ARN=$(aws iam get-role \
        --role-name $ROLE_NAME \
        --query Role.Arn --output text)

# ALB Controller用のservice account作成
kubectl create sa aws-load-balancer-controller -n kube-system
kubectl annotate sa aws-load-balancer-controller eks.amazonaws.com/role-arn=$ALB_ROLE_ARN -n kube-system

# ALB Controller用にcert-managerを設定
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.0.2/cert-manager.yaml
echo '[MESSAGE] wait for cert-manager...'
sleep 30

# ALB Controller用のManifestを持ってきて修正してapply
curl -o v2_0_0_full.yaml https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/v2_0_0_full.yaml
sed s/your-cluster-name/$CLUSTER_NAME/ v2_0_0_full.yaml > ALB_Controller_manifest.yaml
sed -i -e "/ingress-class=alb$/a \            - --aws-region=$AWS_DEFAULT_REGION" ./ALB_Controller_manifest.yaml
sed -i -e "/ingress-class=alb$/a \            - --aws-vpc-id=$VPC_ID" ./ALB_Controller_manifest.yaml
kubectl apply -f ALB_Controller_manifest.yaml
```
ここまででAWS Load Balancer Controllerが作成されているはずです。
確認用のnginxサンプルをデプロイします。

```yaml:nginx.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: sample-ns
---
apiVersion: v1
kind: Service
metadata:
  name: service-sample-nginx
  namespace: sample-ns
  labels:
    app: sample-nginx
spec:
  selector:
    app: sample-nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: sample-ns
  labels:
    app: sample-nginx
spec:
  selector:
    matchLabels:
      app: sample-nginx
  replicas: 3
  template:
    metadata:
      labels:
        app: sample-nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.19.2
        ports:
        - containerPort: 80
```

```shell
kubectl apply -f nginx.yaml
```
いよいよTargetGroupBindingを作成してみましょう。
<your-targetgroup-arn>にはTargetGroupを作成した際にメモしたARNを、<your-alb-securitygroup>にはalbに紐づいているSecuirtyGroupIDを入れましょう。

```yaml:target-group-binding.yaml
apiVersion: elbv2.k8s.aws/v1alpha1
kind: TargetGroupBinding
metadata:
  name: sample-tgb
  namespace: sample-ns
spec:
  serviceRef:
    name: service-sample-nginx
    port: 80
  targetGroupARN: <your-targetgroup-arn>
  targetType: ip
  networking:
    ingress:
    - from:
      - securityGroup:
          groupID: <your-alb-securitygroup>
      ports:
      - protocol: TCP
```
デプロイ。

```shell
kubectl apply -f target-group-binding.yaml
```
早速ALBのDNS名にアクセスしてみると…
![018.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/33c23b1a-f57d-fbac-3f40-6a7abd566c54.png)

うまくいっていそうですね。
TargetGroupにもちゃんとPodのIPアドレスが登録されています。
![019.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/d52bbc7f-6934-4f9f-33e9-11a38ad7822c.png)
画像はdeploymentを更新したときのもので、Podが入れ替わってもちゃんとAWS Load Balancer Controllerが追いかけてくれているのがわかります。

以上AWS Load Balancer ControllerのTargetGroupBindingを試してみましたが、どうだったでしょうか。実際にこの手順をなぞると1時間程度でできるのでぜひぜひやってみてください。
EKSに対するハードルが少しでも下がれば幸いです。

記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。