ALB ControllerでALBを管理させつつ削除させない方法

ALB Controllerを使うとK8sのIngressリソースと連動してALBをデプロイできます。しかし、Ingressリソースを消してしまうとALBも一緒に消えてしまいます。ALBのホスト名を外部DNSに登録している場合など、ALBを極力消したくないこともあります。

ALB CotrollerでALBを管理さｓつつ削除させない方法をいくつか考えました。

# まとめ

以下3つの方法を考えました。

1. **削除保護の設定**: annotationsで`alb.ingress.kubernetes.io/load-balancer-attributes: deletion_protection.enabled=true`を設定する方法です。K8sリソースの削除を実行してもIngressおよびALBは削除されません。一見これで十分なような気がしますが削除を実行したコマンドが返ってこなくなる問題があります。
2. **グループの利用**: annotationsの`alb.ingress.kubernetes.io/group.name`を活用する方法です。デプロイ用とルール設定用でIngressリソースを分けつつ同じIngressグループを指定します。ルール設定用のIngressを削除してもALBは削除されずルールだけ消えます。
3. **外部管理LB**: あらかじめALBを作成しておきCRDのTargetGroupBindingでターゲットを管理する方法です。ALBはK8sの管理外になるためK8sの操作ではALBは消えなくなります。

個人的には1と2を組み合わせて使うのが安全で使いやすいやり方だと思います。具体的にはデプロイ用のIngressに削除保護を設定しルール設定用のIngressでルールを設定します。こうすることでALBを使いまわしつつ気軽にIngressを消したりできます。

3の方法はALBやルールの管理をK8s外で管理する必要があったり、Podとの通信許可（セキュリティグループ）にも気を配る必要があります。しかし、異なるEKS間でALBを共有したり、既存のALBを使用したいユースケースに使えます。

# 1. 削除保護の設定

AWSのリソースには削除を防止する**削除保護**の設定があります。Ingressのannotationsで`alb.ingress.kubernetes.io/load-balancer-attributes: deletion_protection.enabled=true`を設定するとALB ControllerでデプロイするALBに削除保護が設定されます。ALB Controllerのドキュメントにある説明は[こちら](https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/guide/ingress/annotations/#custom-attributes)です。

## 設定例

``` yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test
  annotations:
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/load-balancer-attributes: deletion_protection.enabled=true
spec:
  ingressClassName: alb
  rules:
  - http:
      paths:
      - backend:
          service:
            name: test
            port:
              number: 80
        path: /
        pathType: Exact
```

## 動作確認

上記applyすると以下のようにデプロイされます。

``` sh
$ kubectl get ingress
NAME   CLASS   HOSTS   ADDRESS                                                                            PORTS   AGE
test   alb     *       internal-k8s-default-test-0beab48a0d-1864872286.ap-northeast-1.elb.amazonaws.com   80      13s
```

Ingressを削除します。すると以下のようになるものの、プロンプトは返って来なくなります。（10分ほど待っても返って来ません。）

``` sh
$ kubectl delete ingress test
ingress.networking.k8s.io "test" deleted

```

ALB Controllerのログを確認すると削除保護により削除できなかった旨を確認できます。

```
$ kubectl logs -n kube-system aws-load-balancer-controller-7db9f97875-5z766
...
{"level":"error","ts":1739089983.5857038,"logger":"controller-runtime.manager.controller.ingress","msg":"Reconciler error","name":"test","namespace":"default","error":"deletion_protection is enabled, cannot delete the ingress: test"}
```

Ingressを確認するとまだ残っています。

```
$ kubectl get ingress
NAME   CLASS   HOSTS   ADDRESS                                                                            PORTS   AGE
test   alb     *       internal-k8s-default-test-0beab48a0d-1864872286.ap-northeast-1.elb.amazonaws.com   80      20m
```

Ingressを消したい場合、annotationsの`alb.ingress.kubernetes.io/load-balancer-attributes`を削除します。

``` sh
$ kubectl annotate ingress test alb.ingress.kubernetes.io/load-balancer-attributes-
```

するとしばらくするとIngressが消えます。

```
$ kubectl get ingress
No resources found in default namespace.
```

このように`deletion_protection.enabled=true`を設定することでIngressおよびALBの削除を防ぐことができます。しかし、プロンプトが返ってこないのは少々面倒です。

**2025/05/20 追記**  
`deletion_protection.enabled=true`を付けたIngressリソースも`metadata.finalizers`を`null`にすれば削除できました。しかし、ALBは削除保護が有効になっているため消えず、同じIngressリソースを再作成するとALBも同じものがアタッチされました。

また、annotationsを使わずマネコン等で削除保護を設定しても、Ingressリソースを消す時に**ALB Controllerが削除保護を無効化してALBを削除**してしまうので注意が必要です

# 2. グループの利用

ALB ControllerのIngressにはannotationsで`alb.ingress.kubernetes.io/group.name`を設定できます。同じグループ名を指定したIngressはALBを共有します。これを使いALBのデプロイ用とルール設定用の2つのIngressを用意します。ALB Controllerのドキュメントにある説明は[こちら](https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/guide/ingress/annotations/#ingressgroup)です。

## 設定例

以下2つのIngressを準備します。

**デプロイ用**  
pathは何かしらの設定が必要だったのでダミーのパスを設定しています。`common`というグループに属します。

``` yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: deploy
  annotations:
    alb.ingress.kubernetes.io/group.name: common
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/actions.response-503: >
      {"type":"fixed-response","fixedResponseConfig":{"contentType":"text/plain","statusCode":"503","messageBody":"503 error text"}}
spec:
  ingressClassName: alb
  rules:
  - http:
      paths:
      - path: /dummy
        pathType: Exact
        backend:
          service:
            name: dummy
            port:
              name: response-503
```

**ルール用**  
バックエンドへのルート設定をします。こちらも`common`というグループに属します。

``` yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test
  annotations:
    alb.ingress.kubernetes.io/group.name: common
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
  - http:
      paths:
      - backend:
          service:
            name: test
            port:
              number: 80
        path: /
        pathType: Exact
```

## 動作確認

上記applyすると以下のようにデプロイされます。Ingressは2つありますがADDRESSで同じALBを使用していることがわかります。

``` sh
$ kubectl get ingress
NAME     CLASS   HOSTS   ADDRESS                                                                      PORTS   AGE
deploy   alb     *       internal-k8s-common-5ba7cb9a7a-1608177685.ap-northeast-1.elb.amazonaws.com   80      26s
test     alb     *       internal-k8s-common-5ba7cb9a7a-1608177685.ap-northeast-1.elb.amazonaws.com   80      27s
```

ルール設定用の`test`を削除します。プロンプトはすぐに返ってきます。

```
$ kubectl delete ingress test
ingress.networking.k8s.io "test" deleted
```

確認すると`deploy`は残っています。

```
$ kubectl get ingress
NAME     CLASS   HOSTS   ADDRESS                                                                      PORTS   AGE
deploy   alb     *       internal-k8s-common-5ba7cb9a7a-1608177685.ap-northeast-1.elb.amazonaws.com   80      6m2s
```

このように`common`グループに属するIngressがすべて消えるまでALBは残り続けます。`deploy`のIngressにはさらに[1. 削除保護の設定](#1-削除保護の設定)の設定をしておけばより安全になります。

# 3. 外部管理LB

ALB ControllerのIngressをデプロイすると内部的にはCRDのTargetGroupBindingリソースも作られます。TargetGroupBindingはALBにアタッチされているターゲットグループを管理するもので、Ingressのターゲットに指定したServiceの背後にいるPodのIPアドレスを自動で登録します。これによりPodが入れ替わっても引き続きALB経由でアクセスできます。


TargetGroupBindingリソースを手動で作成すると既存ALBのターゲットグループをALB Controllerで管理できます。ALB Controllerのドキュメントにある説明は[こちら](https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/guide/use_cases/self_managed_lb/)です。TargetGroupBindingリソース自体はALBをデプロイしていないためTargetGroupBindingリソースを消してもALBは削除されません。


しかし、Ingressを使わないためIngressで使用できるALB Controllerのannotationsは使えません。また、IngressだとPodのセキュリティグループも自動で更新されますがそれもされなくなります。ALB関連リソース（ALB、ターゲットグループ、リスナー、セキュリティグループ）はK8s外部で管理します。

## 設定例

以下のように事前にALBやリスナーを用意します。作成したTargetGroupのARNを確認します。

``` terraform
resource "aws_lb" "this" {
  name               = "alb-test"
  internal           = true
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = data.terraform_remote_state.eks.outputs.vpc.private_subnets
}

resource "aws_security_group" "alb" {
  name        = "alb-test"
  description = "alb-test"
  vpc_id      = data.terraform_remote_state.eks.outputs.vpc.vpc_id

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_lb_target_group" "this" {
  name        = "alb-test"
  port        = 80
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = data.terraform_remote_state.eks.outputs.vpc.vpc_id
}

resource "aws_lb_listener" "this" {
  load_balancer_arn = aws_lb.this.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.this.arn
  }
}
```

K8sマニフェストは以下の通りです。`targetGroupARN`に作成したターゲットグループのARNを指定します。

``` yaml
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
  name: tgb-test
spec:
  serviceRef:
    name: tgb-test
    port: 80
  targetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/alb-test/c43cacc256114e34
```

## 動作確認

リソースデプロイ後、TargetGroupBinding、Ingress、Podを確認します。TargetGroupBindingはありますがIngressはありません。

``` sh
$ kubectl get TargetGroupBinding,ingress,pod -o wide
NAME                                        SERVICE-NAME   SERVICE-PORT   TARGET-TYPE   ARN                                                                                              AGE
targetgroupbinding.elbv2.k8s.aws/tgb-test   tgb-test       80             ip            arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/alb-test/c43cacc256114e34   17m

NAME                            READY   STATUS    RESTARTS   AGE   IP           NODE                                           NOMINATED NODE   READINESS GATES
pod/tgb-test-59df87c7df-659lp   1/1     Running   0          17m   10.0.1.253   ip-10-0-1-86.ap-northeast-1.compute.internal   <none>           <none>
```

ALBとターゲットグループを確認すると以下の通りです。ターゲットはPodのIPアドレスになっています。ヘルスが`unhealthy`になっていますがPod（Node）にアタッチされたセキュリティグループにALBのセキュリティグループからのインバウンドを許可していないからです。別途セキュリティグループの許可を追加すると`healthy`になります。

```
$ aws elbv2 describe-load-balancers --region=ap-northeast-1 | jq '.LoadBalancers[].LoadBalancerName'
"alb-test"

$ aws elbv2 describe-target-health --target-group-arn="arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/alb-test/c43cacc256114e34" --region=ap-northeast-1
{
    "TargetHealthDescriptions": [
        {
            "Target": {
                "Id": "10.0.1.253",
                "Port": 80,
                "AvailabilityZone": "ap-northeast-1a"
            },
            "HealthCheckPort": "80",
            "TargetHealth": {
                "State": "unhealthy",
                "Reason": "Target.Timeout",
                "Description": "Request timed out"
            }
        }
    ]
}
```

Podを削除してIPアドレスを変えてみます。

```
$ kubectl delete pod tgb-test-59df87c7df-659lp
pod "tgb-test-59df87c7df-659lp" deleted

$ kubectl get pod -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP           NODE                                           NOMINATED NODE   READINESS GATES
tgb-test-59df87c7df-4z8sc   1/1     Running   0          27s   10.0.1.138   ip-10-0-1-86.ap-northeast-1.compute.internal   <none>           <none>
```

ターゲットグループを確認するとターゲットのIPアドレスが更新されています。（ターゲットグループからPodのアドレスが消えるまで少し時間がかかります。）

```
$ aws elbv2 describe-target-health --target-group-arn="arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/alb-test/c43cacc256114e34" --region=ap-northeast-1
{
    "TargetHealthDescriptions": [
        {
            "Target": {
                "Id": "10.0.1.138",
                "Port": 80,
                "AvailabilityZone": "ap-northeast-1a"
            },
            "HealthCheckPort": "80",
            "TargetHealth": {
                "State": "healthy"
            }
        }
    ]
}
```

TargetGroupBindingを消します。

``` sh
$ kubectl delete TargetGroupBinding tgb-test
targetgroupbinding.elbv2.k8s.aws "tgb-test" deleted
```

ALBとターゲットグループは引き続きのこります。（ターゲットグループからPodのアドレスが消えるまで少し時間がかかります。）

```
$ aws elbv2 describe-load-balancers --region=ap-northeast-1 | jq '.LoadBalancers[].LoadBalancerName'
"alb-test"

$ aws elbv2 describe-target-health --target-group-arn="arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/alb-test/c43cacc256114e34" --region=ap-northeast-1
{
    "TargetHealthDescriptions": []
}
```

このようにTargetGroupBindingを使うと既存のALBやターゲットグループを ALB Controllerで管理でき、TargetGroupBindingを消しても既存のAWSリソースは消えません。