brunch

You can make anything
by writing

C.S.Lewis

by 이권수 Aug 30. 2023

AWS Load Balancer Controller

장애로 배운 AWS Load Balancer Controller 원리


최근에 팀에서 운영하던 서비스에서 사소한 장애가 발생했다. 빌드 배포 파이프라인을 수정하는 과정에서 일부 쿠버네티스 리소스를 삭제했는데, 서비스가 정상적으로 응답하지 못했다. 서비스와 전혀 관련 없는 Ingress를 삭제했기 때문에, 우리는 의아했다. 문제의 원인은 EC2 보안 그룹에 8001번 포트가 빠져있었던 것이었다. 보안 그룹에 8001번을 추가해서 간단하게 장애를 해결했지만, 그 원인이 확실하지 않았다. 왜냐하면, 다른 서비스에서 사용하는 클러스터에서도 동일한 작업을 했는데 같은 현상이 발생하지 않았기 때문이었다. 


일단 로드밸런서와 EC2간 통신문제임을 확인할 수 있었기 때문에, AWS Load Balancer Controller에 어떤 로그가 남았을 거라고 생각했다. AWS Load Balancer Controller는 쿠버네티스 Ingress 리소스에 대한 이벤트를 받아서 AWS 환경에 로드밸런서를 구축해 주는 일을 했다. 이때 작업했던 내용 중에 Ingress 리소스를 지우는 부분도 있었기 때문에, 나는 AWS Load Balancer Controller가 의심스러웠다.


그래서 원인을 파악해 보고자 AWS Load Balancer Controller의 로그를 확인했다. AWS Load Balancer Controller 로그에는 보안 그룹에 관한 로그가 남겨져 있었다. 원본 로그가 길어서 필요한 부분만 표기하면 아래와 같다.


"msg": "revoking securityGroup ingress", "FromPort":3100, "ToPort":8001

"msg": "authorizing securityGroup ingress", "FromPort":3100, "ToPort":3100


Revoking이란 보안그룹 규칙을 제거시켰다는 의미이고, authorizing이란 보안그룹 규칙을 추가했다는 의미이다. 즉, AWS Load Balancer Contoller는 작업 시점에 보안 그룹 규칙 중 3100-8001 범위로 허용된 보안 규칙을 제거하고, 3100-3100으로 허용된 보안규칙을 새롭게 추가했다. 문제는 8001번 포트가 서비스에서 사용하고 있는 파드였다는 점이다. 위 로그 순서에 따라 작업이 되면서 EC2는 더 이상 로드밸런서로부터 오는 8001번 트래픽을 받을 수 없게 되었다. 이로 인해 서비스 파드(Pod)는 정상인데도 요청이 실패하기 시작했다.


이 문제에 대한 원인을 파악하기 위해 AWS Load Balancer Controller가 작동하는 원리에 대해서 알아보았다.



AWS Load Balancer Controller 작동 원리

AWS Load Balancer Controller가 작동하는 원리는 다음과 같다.

출처: https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.6/how-it-works/


1. AWS Load Balancer Controller는 쿠버네티스 API 서버에서 특정 이벤트를 Watch 한다. AWS Load Balancer의 경우, 대표적으로 Ingress와 Service 리소스에 대한 이벤트를 Watch 한다.

2. AWS Load Balancer Controller는 AWS ALB(ELBv2)를 생성한다. ALB는 어노테이션(annotation)에 따라 외부 혹은 내부 통신용으로 구성할 수 있다. 또한 관리자가 임의로 ALB가 사용할 서브넷을 지정할 수도 있다.

3. AWS Load Balancer Controller는 Ingress 리소스에 등록된 Service를 보고 타깃그룹(Target Group)을 생성한다. 

4. AWS Load Balancer Controller는 어노테이션을 보고 리스너(Listener)를 생성한다. 이때 어노테이션이 없으면 80, 443이 기본으로 생성된다. 이때, alb.ingress.kubernetes.io/certificate-arn 어노테이션을 통해 인증서를 추가할 수 있다.

5. AWS Load Balancer Controller는 Ingress 리소스에 적힌 Path별로 리스너 규칙을 생성한다. 리스너 규칙을 통해 AWS ALB는 Path별로 적합한 서비스로 트래픽을 전송할 수 있다.


이처럼 AWS Load Balancer Controller는 AWS Load Balancer와 관련된 리소스를 생성하고 관리한다. 만약 새롭게 서비스가 추가되면 3번 과정을 통해 새롭게 타깃그룹을 생성한다. 어노테이션이 변경되면 그에 맞는 AWS 리소스를 설정한다.


여기서 한 가지 흥미로운 건 AWS ALB와 EC2 노드 간의 보안그룹 설정이다. AWS ALB는 뒷단에 Pod로 트래픽을 보낸다. 트래픽을 보내기 위해서는 EC2의 보안그룹에서 해당 트래픽을 허용해주어야 한다. 하지만 쿠버네티스 사용자는 서비스를 추가할 때 EC2 보안그룹을 수정하지 않는다. 그럼에도 불구하고 Ingress로 생성된 ALB를 통해 트래픽이 정상적으로 전달된다. 


그러면 AWS Load Balancer Controller는 어떻게 보안그룹 규칙을 관리할까?


이 과정을 이해하기 위해서는 우선 TargetGroupBinding에 대해 알아야 한다. 

코드를 통해서 TargetGroupBinding이 어떻게 동작하는지 상세하게 분석해보자.


TargetGroupBinding 이해하기

AWS Load Balancer Controller는 타깃그룹이 생성되면 내부적으로 TargetGroupBinding라는 커스텀 리소스(Custom Resource)를 생성한다. TargetGroupBinding은 파드/노드를 ALB나 NLB의 타깃그룹으로 연결해 주는 역할을 한다.


우선 TargetGroupBinding이 호출되는 구조를 살펴보자.


아래 그림을 보면 StackDeployer의 Deploy()라는 함수에 synthesizer 리스트가 정의되어 있다. 이 Synthesizer들은 각각 자신의 Synthesize(ctx) 함수를 호출한다. 리스트를 자세히 보면, Ingress 하나로 어떤 리소스들을 생성하는지 확인할 수 있다.


이 중에서 TargetGroupBinding Synthesizer는 TargetGroupBinding이라는 커스텀 리소스를 만든다. 아래 그룹은 targetGroupBindingSynthesizer의 Synthesize() 함수이다. 함수 중간에 기존 리소스와 매칭되지 않는 리소스는 새롭게 생성하는 것을 볼 수 있다. (Create함수)


이렇게 TargetGroupBinding이 생성되면 TargetGroupBindingReconciler에게 이벤트가 전달된다. Reconciler는 특정 커스텀 리소스가 생성/삭제/변경될 때 어떤 작업을 해야 하는지 정의하는 함수이다. TargetGroupBinding 또한 targetGroupBindingReconciler라는 구조체를 통해 Reconcile을 진행한다.


Reconcile의 과정은 다음과 같다.


1. 현재 이미 생성된 TargetGroupBinding이 있는지 확인한다. 

2. 만약 DeletionTimestamp가 존재하면 TargetGroupBinding을 삭제한다.

3. 아니면 현재 정의된 TargetGroupBinding 정보에 맞기 리소스를 수정한다.

4. 리소스를 생성/수정할 때는 Finalizer를 추가한다. 

5. rgbResourceManager.Reconcile() 함수를 통해 타깃 유형에 따라 타깃을 등록/제거한다.


이제 rgbResourceManager.Reconcile() 함수를 확인해 보자. TargetGroupBinding에 정의된 타깃유형에 따라 서로 다른 함수를 호출한다. 각 함수에서는 실제 타깃을 연결하는 작업을 호출한다. 혹은 삭제된 타깃이 있다면 실제 타깃그룹에서 제거(Deregister)한다. 


각 함수별로 설명하기에는 함수가 길어서 실제 타깃을 AWS 타깃그룹에 등록하는 API 부분만 소개하겠다.



그러면 만약 TargetGroupBinding이 삭제되면 어떻게 될까?


쿠버네티스 API 서버는 Finalizer가 설정되어 있는 리소스가 삭제되는 경우, deletionTimestamp를 메타데이터에 추가한다. DeletionTimestamp는 실제 종료가 시작한 시간을 의미한다. 아까 Reconcile과정에서 설명했듯이, deletionTimestamp가 존재하는 경우에는 TargetGroupBinding 삭제를 진행한다.


cleanupTargetGroupBinding() 함수를 보면 정리하는 과정은 다음과 같다.


1. Finalizer가 있는지 확인한다.

2. Finalizer가 있으면 먼저 TargetGroupBinding을 정리한다.

3. Finalizer를 제거한다. 참고로, finalizer가 제거된 후에야 etcd에서 실제로 리소스가 삭제된다.


이제는 Cleanup 과정만 보면 된다. Cleanup() 함수에서 눈여겨보아야 할 건 아래 2가지 함수이다.

cleanupTargets(): 타깃그룹에 속한 타깃을 전부 제거한다.

networkingManager.Cleanup(): 네트워크 변경사항에 따른 보안그룹 설정을 변경한다.


networkingManager가 실제 네트워크 설정에 관한 작업을 수행한다. 예컨대, 필요한 보안그룹 규칙을 추가, 삭제하는 작업이 있다. Cleanup() 함수는 특정 TargetGroupBinding이 제거되면서 필요 없는 보안그룹 규칙을 정리하는 과정을 수행한다. 실제 함수를 보면 바로 reconcileWithIngressPermissionsPerSG() 함수를 호출한다. 


그런데 이때 ingressPermissionPerSG 값을 nil로 전달한다. ingressPermissionPerSG는 IPPermissionInfo 구조체를 담고 있는데, IPPermissionInfo는 보안그룹 규칙에 필요한 정보를 의미한다. 예컨대, FromPort, ToPort, IPProtocol, IPRange 등의 정보가 이에 속한다. 즉, 이 값을 nil로 처리했다는 의미는 기존에 가지고 있던 보안그룹 규칙을 제거하겠다는 의미이다. 이 과정은 computeAggregatedIngressPermissionPerSG() 함수에 나온다.

computeAggregatedIngressPermissionPerSG() 함수는 클러스터 전체의 TargetGroupBinding을 확인해서 허용해야 할 보안그룹 규칙을 계산한다. 이때, Controller 옵션 중에 disableRestrictedSGRules를 설정하면, 사용하는 포트가 아니라 광범위하게 포트를 전부 허용한다. 하지만, 이 옵션은 기본적으로 비활성화되어 있다. 즉, 보통의 경우 computeRestrictedIngressPermissionsPerSG() 함수가 호출된다.


computeRestrictedIngressPermissionsPerSG() 함수를 보면, 전체 TargetGroupBinding을 확인해서 실제 사용하고 있는 포트 중에 가장 큰 값과 작은 값을 구한다. 예컨대, 3000, 4000, 5000번 포트를 사용한다고 가정하면, min = 3000, max = 5000으로 계산하고, 보안그룹 규칙을 설정할 때 3000번부터 5000번까지를 전부 허용한다. 


이렇게 계산을 마치고 나면, TargetGroupBinding이 삭제된 후에도 사용 중인 최소, 최대 포트가 남는다.

reconcileWithIngressPermissionsPerSG() 함수에서 해당 정보를 가지고 

sgReconciler.ReconcileIngress() 함수를 호출한다. sgReconciler는 보안그룹 설정을 생성/삭제/변경해 주는 구조체이다. sgReconciler는 ReconcileIngress() 함수를 통해 최신의 보안그룹 규칙을 유지한다.


ReconcileIngress() 함수는 내부적으로 reconcileIngressWithSGInfo() 함수를 호출한다. 바로 여기서 Revoke와 Authorize 과정이 진행된다.


여기까지가 TargetGroupBinding 정보에 따라 보안그룹 규칙이 어떻게 바뀌는지에 대해서 알아보았다. 그러면 이제 앞서 언급했던 장애가 왜 발생했는지 알아보자.


대체 장애는 왜 발생했을까?

바로 앞에서 설명했던 과정처럼, Ingress리소스와 더불어 Service 리소스를 삭제하면서 내부적으로 TargetGroupBinding이 삭제되었다. 삭제되기 전에 총 3개의 Ingress가 있었고, 그에 맞게 TargetGroupBinding도 3개 존재했다. 각 타깃그룹별로 사용 중인 포트는 다음과 같다. 


삭제하기 전 보안그룹 설정

1개의 타깃그룹은 3100번 포트를, 나머지는 8001번 포트를 사용했다. 앞서 설명했던 과정대로 EC2 인스턴스 보안그룹 규칙에는 최솟값인 3100번부터 최댓값인 8001번까지의 포트를 허용하고 있었다. 이때 트래픽을 허용하기 위한 Source ID로 하나의 보안그룹을 사용했는데, AWS Load Balancer 내부적으로는 이를 Backend Security Group(보안그룹)이라고 부른다. 


Backend 보안그룹은 ALB와 EC2간의 통신에 필요한 보안그룹 규칙만을 위해 존재하는 보안그룹이다. 하나의 클러스터에서 사용되는 모든 Ingress는 같은 Backend 보안그룹을 가진다. 왜냐하면 ALB 입장에서는 어떤 노드그룹에 어떤 파드가 뜰지 모르기 때문에 모든 서비스에 대한 트래픽을 모든 노드에 허용해야 하기 때문이다. 즉, 특정 로드밸런서가 전달해야 할 대상이 어느 노드에 있는지 모르니까 모든 ALB와 모든 노드그룹이 전부 소통하도록 만든 것이다. 이때 최소한의 보안을 챙기기 위해서 TargetGroupBinding에 명시된 최소포트와 최대포트 구간까지만 허용하는 것이다.


위 예시에서 Backend 보안그룹이 바로 sg-12345이다. 


이 상태에서 관리자가 타깃그룹 3을 삭제했다. 이때 당연히 기존에 존재하는 타깃그룹 2번이 같은 8001번을 사용하기 때문에 보안그룹 규칙에는 변화가 없어야 했다. 그런데 실제로 작업된 내용은 다음과 같았다.


작업 후 결과


이상하게도 보안그룹 규칙에서 8001번 포트가 사라졌다. 그리고 불행하게도 타깃그룹 2번이 실제 서비스를 위한 ALB에 연결되어 있었다.


원인을 찾아보니, 바로 Ingress의 어노테이션에 있었다.


앞서 설명했던 Backend 보안그룹은 Ingress 어노테이션 중에 alb.ingress.kubernetes.io/security-groups 어노테이션이 있으면 무시된다. 즉, 사용자가 지정한 보안그룹이 있으면, AWS Load Balancer Controller는 이를 우선적으로 적용하기 위해 자동으로 관리하는 동작을 멈춘다. 


실제로 타깃그룹 2번에도 해당 어노테이션이 들어있었고, 그래서 타깃그룹 2번의 8001번 포트는 애초에 관리대상이 들어가지 않았다. 그래서 AWS Load Balancer Controller는 3100번 포트만 있다고 생각해서 보안그룹 규칙을 3100-3100으로 변경한 것이었다.


만약 alb.ingress.kubernetes.io/security-groups을 사용하면서 자동으로 관리도 되기를 원한다면, 다음의 어노테이션도 추가해야 한다. 


alb.ingress.kubernetes.io/manage-backend-security-group-rules: "true"


이 부분을 코드로 보면 다음과 같다.

sgNameOrIDsViaAnnotation이 바로 사용자가 어노테이션에 지정한 보안그룹 리스트이다. 만약 해당 값이 0보다 크면, manageBackendSGRules라는 옵션을 확인하는데, 이 값이 바로 manage-backend-security-group-rules 옵션이다. 참고로, Controller는 해당 옵션이 있으면 t.backendSGIDToken을 정상적으로 설정하지만, 그렇지 않으면 nil값을 사용한다. 다른 코드에서 t.backendSGIDToken이 nil이면,  보안그룹을 변경하지 않는다.



지금까지 AWS Load Balancer Controller와 관련된 장애에 대해서 알아보았다. AWS Load Balancer Controller를 사용하고 있다면 해당 Controller가 어떻게 리소스를 관리하는지 알아두면 도움이 되리라 믿는다. 


이번 글을 통해 부디 같은 장애를 맞지 않기를 바란다.



관련 문서

https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.5/deploy/security_groups/

https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.1/guide/targetgroupbinding/targetgroupbinding/

https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.6/guide/ingress/annotations/


매거진의 이전글 Karpenter 작동 원리
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari