EKS에서 Pod Readiness Gate의 동작 원리를 이해할 수 있습니다.
Pod Readiness Gate를 활용하여 안정적인 애플리케이션 배포 전략을 세울 수 있습니다.
Kubernetes와 AWS ALB Controller 간의 상호작용 주기를 분석합니다.
EKS 환경에서 애플리케이션을 배포·운영하다 보면 가끔 502/504
과 같은 에러를 경험했거나 접해보신 적이 있을 것입니다.
이는 단순히 새로운 버전을 배포하는 과정에서만 발생하는 문제가 아니라, 파드를 스케일 아웃하면서 새로 생성할 때도 Kubernetes 내부 처리와 AWS 리소스 간의 타이밍이 맞지 않아 생길 수 있습니다.
Q1.
예를 들어, 새 파드가 생성되었을 때는 Kubernetes의 Readiness Probe가 이미 정상 동작해 Service가 트래픽을 보낼 준비가 되었지만, 정작 AWS ALB 측에서 아직 해당 파드를 헬시(Healthy)로 인식하지 못한다면 어떤 일이 발생할까요?Answer.
이 경우, 서비스 트래픽은 준비 단계에 있는 파드로 전달될 수 있습니다.ALB에서는 해당 파드가 정상 작동한다고 판단하지 않기 때문에, 헬스 체크 실패와 연결하여 결과적으로 요청이 제대로 처리되지 않아 502/504 등의 에러 응답이 발생할 수 있습니다.
즉, Kubernetes에서 Pod가 Ready 상태라고 하더라도 ALB의 헬스 체크가 완료되지 않은 상태에서 트래픽이 전달되면, 클라이언트 입장에서는 서비스 불안정 또는 접속 실패의 문제가 발생합니다.
Q2.
반대로, 파드는 이미 Terminated 상태임에도 불구하고 ALB가 여전히 종료된 파드로 트래픽을 전달한다면 또 어떤 문제가 생길까요?Answer.
이미 종료된 파드에 트래픽이 전달된다면, 해당 파드에서는 요청을 처리할 수 없으므로 결과적으로 서비스 응답이 지연되거나 에러 응답이 발생할 수 있습니다. 이로 인해 클라이언트는 응답 지연(Timeout) 또는 네트워크 오류 등의 문제를 경험하게 되며, 전반적인 서비스 가용성과 안정성에 악영향을 미치게 됩니다.
이러한 상황을 보다 안정적으로 제어하기 위해 Kubernetes에서 제공하는 핵심 기능이 바로 Pod Readiness Gate입니다.
이번 글에서는 Pod Readiness Gate가 무엇이며, 이를 통해 어떻게 안정적인 애플리케이션 배포·운영을 보장할 수 있는지 알아보겠습니다.
Pod Readiness Gate는 Pod의 준비 상태(Ready 상태)를 결정하는 조건을 확장할 수 있는 쿠버네티스의 기능입니다.
기존에는 kubelet의 readinessProbe
결과를 바탕으로 Ready 조건을 설정했지만, Pod Readiness Gate를 사용하면 외부 컨디션을 포함하여 Ready 조건을 정의할 수 있습니다.
아래는 쿠버네티스 문서에서 정의된 Pod Readiness Gate의 주요 내용입니다:
"Pod readiness gates extend the condition check of Pods to determine if they are ready to serve traffic."
커스텀 컨디션 추가:
서비스 트래픽 안정성:
유연한 통합:
Failover 지원:
Pod Readiness Gate의 동작 과정을 살펴보겠습니다.
주요 흐름은 다음과 같습니다:
- Pod 생성:
- Pod 템플릿에
readinessGate
가 정의되면,kube-apiserver
는 이를 기반으로 Pod를 생성합니다.
- 컨디션 초기화:
- Pod가 생성되면
readinessGate
에 정의된 모든 conditions은False
상태로 초기화됩니다.
- 컨디션 업데이트:
- 외부 컨트롤러(예: AWS Load Balancer Controller)가
readinessGate.status
을 업데이트합니다.
- Ready 상태 결정:
- readinessGate.status가 모두
True
가 될 때까지 Pod는 Ready 상태로 전환되지 않습니다.
- 서비스 엔드포인트 추가:
- readinessGate.status가 모두
True
로 설정되면, 쿠버네티스의 Endpoint Controller가 Pod의 Ready 상태를 확인합니다.- Ready 상태로 전환된 Pod의 IP가 서비스 엔드포인트에 추가되어 트래픽을 받을 수 있게 됩니다.
Target Group(이하 TG)에서 헬스 체크가 성공하면 다음과 같은 과정을 거칩니다.
1. ALB가 Target Group 헬스 체크 수행
AWS ALB(어플리케이션 로드 밸런서)는 TG에 등록된 대상(Pod IP:Port)에 주기적으로 HTTP(S) 헬스 체크 요청을 보냅니다.
특정 횟수(예: 2회 연속 성공) 이상 응답이 성공(2xx/3xx
)이면, ALB는 해당 Pod를 Healthy 상태로 간주합니다.
2. ALB Controller가 헬스 체크 결과 수집
AWS Load Balancer Controller(ALB Controller)는 주기적으로(또는 이벤트 기반으로) AWS API(DescribeTargetHealth
)를 호출해, 각 Target(즉, Pod IP:Port)의 헬스 상태를 조회합니다.
헬스 체크 결과가 Healthy로 확인되면, ALB Controller는 Kubernetes API를 통해 Pod의 target-health.elbv2.k8s.aws/<TargetGroupName>
ReadinessGate Condition을 True
로 업데이트합니다.
3. Pod ReadinessGate 충족 → Ready 상태 전환
Pod에 여러 개의 ReadinessGate가 있을 수 있는데, 모든 ReadinessGate Condition이 True가 되어야 최종적으로 K8s에서 해당 Pod가 Ready
로 표시됩니다.
ALB Controller가 위 2단계에서 Condition을 True
로 패치하면, kubelet 및 Endpoints Controller가 Pod 상태를 다시 확인해 “(1) ReadinessProbe도 OK, (2) ALB 관련 ReadinessGate도 True” 등의 조건이 모두 충족되었다고 판단합니다.
결과적으로 Pod의 status.conditions
중 Ready
상태가 True
로 바뀝니다.
4. Kubernetes Endpoint Controller가 Service에 추가
Kubernetes Endpoint Controller(혹은 EndpointSlice Controller)는 Pod 상태의 변화를 Watch하며,
Pod가 Ready
로 전환되면 해당 Pod IP를 Service의 Endpoints(EndpointSlice)에 포함시킵니다.
이 시점부터 Service를 통해 들어오는 신규 트래픽은 Ready 상태의 Pod로 라우팅될 수 있게 됩니다.
5. 정상 트래픽 처리
이제 ALB와 Kubernetes Service 양쪽 모두에서 “해당 Pod가 정상적으로 응답 가능한 대상”이라고 인식하므로, 클라이언트로부터 들어오는 요청이 차질없이 처리됩니다.
만약 ALB가 HTTP Keep-Alive를 사용하거나 노드 간 내부 라우팅을 거치더라도, Pod가 최종적으로 헬시(Healthy) 판정을 받은 뒤 트래픽이 전달되기 때문에, 502/504 오류
가 발생할 확률이 크게 감소합니다.
Namespace
에 elbv2.k8s.aws/pod-readiness-gate-inject: enabled
를 지정하면, AWS Load Balancer Controller는 Pod에 자동으로 readinessGate.conditionType
을 추가합니다. 이를 통해 ALB(Application Load Balancer)와 통합된 트래픽 관리가 가능합니다.
Namespace에 주석 추가:
Namespace 리소스에 아래와 같은 주석을 추가합니다:
apiVersion: v1
kind: Namespace
metadata:
name: example-namespace
annotations:
elbv2.k8s.aws/pod-readiness-gate-inject: enabled
Pod Readiness Gate 추가:
pkg/inject/pod_readiness_gate.go
파일에 정의된 Mutate
메서드에 따라, readinessGate에 대한 상태가 Pod 생성 시 자동으로 추가됩니다.func (m *PodReadinessGate) Mutate(ctx context.Context, pod *corev1.Pod) error {
if !m.config.EnablePodReadinessGateInject {
return nil
}
req := webhook.ContextGetAdmissionRequest(ctx)
targetHealthCondTypes, err := m.computeTargetHealthReadinessGateConditionTypes(ctx, req.Namespace, pod)
if err != nil {
return err
}
for _, condType := range targetHealthCondTypes {
if !k8s.IsPodHasReadinessGate(pod, condType) {
pod.Spec.ReadinessGates = append(pod.Spec.ReadinessGates, corev1.PodReadinessGate{
ConditionType: condType,
})
}
}
return nil
}
이는 ALB가 Pod의 상태를 감지하고 트래픽을 라우팅하기 전에 추가적인 확인을 수행하도록 합니다.
ALB 상태 연동:
target-health.elbv2.k8s.aws/<TargetGroupName>
조건은 ALB의 Target Group 상태를 기반으로 업데이트됩니다.Pod Ready 상태 업데이트:
target-health.elbv2.k8s.aws/<TargetGroupName>
조건이 True
가 될 때까지 Pod는 Ready 상태로 전환되지 않습니다.Namespace 주석 누락 시 동작:
elbv2.k8s.aws/pod-readiness-gate-inject
주석이 없으면 readinessGate가 추가되지 않습니다.AWS Load Balancer Controller(이하 ALB Controller)가 Pod Readiness Gate를 자동으로 붙이는 핵심 로직은 pkg/inject/pod_readiness_gate.go
파일의 Mutate() 함수에 구현되어 있습니다.
(1) Namespace 애노테이션 확인
func (m *PodReadinessGate) Mutate(ctx context.Context, pod *corev1.Pod) error { if !m.config.EnablePodReadinessGateInject { return nil } req := webhook.ContextGetAdmissionRequest(ctx) targetHealthCondTypes, err := m.computeTargetHealthReadinessGateConditionTypes(ctx, req.Namespace, pod) if err != nil { return err } for _, condType := range targetHealthCondTypes { if !k8s.IsPodHasReadinessGate(pod, condType) { pod.Spec.ReadinessGates = append(pod.Spec.ReadinessGates, corev1.PodReadinessGate{ ConditionType: condType, }) } } return nil }
EnablePodReadinessGateInject:
- ALB Controller 설정에서
--enable-pod-readiness-gate-inject
플래그가true
이거나, Namespace 어노테이션이elbv2.k8s.aws/pod-readiness-gate-inject: enabled
로 설정되어 있어야 이 로직이 동작합니다.
computeTargetHealthReadinessGateConditionTypes():
- Pod가 속한 Namespace의 모든 TargetGroupBinding(TGB) 객체를 확인하고,
- 그 TGB가 참조하는 Service의 Selector와 Pod Label이 일치할 경우,
target-health.elbv2.k8s.aws/<TGBName>
형태의 ConditionType 목록([]corev1.PodConditionType
)을 리턴합니다.
for _, condType := range targetHealthCondTypes {...}:
- 이미 Pod에 동일한 ConditionType이 없다면(
!k8s.IsPodHasReadinessGate(pod, condType)
),pod.Spec.ReadinessGates
배열에 새로 추가합니다.이 로직을 통해 최종적으로
Pod.spec.readinessGates
필드에target-health.elbv2.k8s.aws/<TargetGroupName>
같은 문자열이 자동으로 삽입됩니다.
(2) computeTargetHealthReadinessGateConditionTypes()
func (m *PodReadinessGate) computeTargetHealthReadinessGateConditionTypes( ctx context.Context, namespace string, pod *corev1.Pod) ([]corev1.PodConditionType, error) { tgbList := &elbv2api.TargetGroupBindingList{} if err := m.k8sClient.List(ctx, tgbList, client.InNamespace(namespace)); err != nil { return nil, errors.Wrap(err, "unable to list TGB") } var targetHealthCondTypes []corev1.PodConditionType for _, tgb := range tgbList.Items { // TGB가 참조하는 Service 획득 svcKey := types.NamespacedName{Namespace: tgb.Namespace, Name: tgb.Spec.ServiceRef.Name} svc := &corev1.Service{} if err := m.k8sClient.Get(ctx, svcKey, svc); err != nil { if apierrors.IsNotFound(err) { continue } return nil, errors.Wrap(err, "unable to get Service") } // Service Selector와 Pod 라벨이 매칭되는지 확인 svcSelector := labels.SelectorFromSet(svc.Spec.Selector) if svcSelector.Matches(labels.Set(pod.Labels)) { // 매칭된다면 readinessGate ConditionType 생성 targetHealthCondType := targetgroupbinding.BuildTargetHealthPodConditionType(&tgb) targetHealthCondTypes = append(targetHealthCondTypes, targetHealthCondType) } } return targetHealthCondTypes, nil }
- ALB Controller는 Pod가 생성되는 순간(Admission Webhook 등) 이 로직을 수행하여,
“Pod 라벨”과 “TGB가 참조하는 Service의 Selector”가 매칭되는지 확인합니다.- 매칭될 경우,
target-health.elbv2.k8s.aws/<TGBName>
형태의 ConditionType 문자열을 만들어 리턴합니다.- 이 ConditionType은 나중에 TargetGroup 헬스 체크 결과를 Pod.status.conditions에 업데이트할 때 사용됩니다.
(1) Target Health 상태 수집
pkg/targetgroupbinding/resource_manager.go
안에는 Target Group Binding(TGB) 객체를 관리하면서,
ALB의 Target Health 상태를 조회(DescribeTargetHealth
)하여 Pod ReadinessGate로 반영하는 로직이 있습니다.ALB Controller는 다음 과정을 거칩니다.
- TGB와 매핑된 Service를 통해, 해당 Service Selector에 맞는 Pod 목록을 가져옴.
- 각 Pod
IP:Port
를 ALB Target Group에RegisterTargets / DeregisterTargets API
를 통해 등록/해제.DescribeTargetHealth API
를 주기적으로 호출하여, ALB가 인지한 Target 상태(Healthy/Unhealthy 등)를 가져옴.- 가져온 Target 상태와 Pod 매핑을 대조하여, Pod의
ReadinessGate Condition
을True
또는False
로 업데이트.
(2) Condition 업데이트 함수
func (m *defaultResourceManager) updateTargetHealthPodConditionForPod( ctx context.Context, pod k8s.PodInfo, targetHealth *elbv2types.TargetHealth, targetHealthCondType corev1.PodConditionType, tgb *elbv2api.TargetGroupBinding, ) (bool, error) { // 1) Pod에 해당 ReadinessGate(ConditionType)가 존재하지 않으면 그냥 return if !pod.HasAnyOfReadinessGates([]corev1.PodConditionType{targetHealthCondType}) { return false, nil } // 2) ALB Target Health 결과 기반으로 message/Reason 생성 var reason, message string if targetHealth != nil { reason = string(targetHealth.Reason) message = awssdk.ToString(targetHealth.Description) } // 3) 실제 Condition Status (True/False)를 결정 targetHealthCondStatus, needFurtherProbe := m.calculateReadinessGateTransition( pod, targetHealthCondType, targetHealth) // 4) 기존 Pod Condition과 동일하면 업데이트 스킵 existingTargetHealthCond, hasExistingCond := pod.GetPodCondition(targetHealthCondType) if hasExistingCond && existingTargetHealthCond.Status == targetHealthCondStatus && existingTargetHealthCond.Reason == reason && existingTargetHealthCond.Message == message { return needFurtherProbe, nil } // 5) 새 Condition 생성 newTargetHealthCond := corev1.PodCondition{ Type: targetHealthCondType, Status: targetHealthCondStatus, Reason: reason, Message: message, } if !hasExistingCond || existingTargetHealthCond.Status != targetHealthCondStatus { newTargetHealthCond.LastTransitionTime = metav1.Now() } else { newTargetHealthCond.LastTransitionTime = existingTargetHealthCond.LastTransitionTime } // 6) Pod.status.conditions 업데이트 (Patch) // -> 실제 K8s API 서버에 Patch 요청을 보내어 Pod의 상태를 갱신 podPatchSource := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: pod.Key.Namespace, Name: pod.Key.Name, }, Status: corev1.PodStatus{ Conditions: []corev1.PodCondition{} }, } if hasExistingCond { podPatchSource.Status.Conditions = []corev1.PodCondition{existingTargetHealthCond} } podPatchTarget := podPatchSource.DeepCopy() podPatchTarget.UID = pod.UID podPatchTarget.Status.Conditions = []corev1.PodCondition{newTargetHealthCond} if err := m.k8sClient.Status().Patch(ctx, podPatchTarget, client.StrategicMergeFrom(podPatchSource)); err != nil { if apierrors.IsNotFound(err) { return false, nil } return false, err } // 7) Condition이 True로 전환된 시간 지표 수집 if targetHealthCondStatus == corev1.ConditionTrue && hasExistingCond && !existingTargetHealthCond.LastTransitionTime.IsZero() && existingTargetHealthCond.Status != corev1.ConditionTrue { delta := newTargetHealthCond.LastTransitionTime.Sub(existingTargetHealthCond.LastTransitionTime.Time) m.metricsCollector.ObservePodReadinessGateReady(tgb.Namespace, tgb.Name, delta) } return needFurtherProbe, nil }
위 로직에서 가장 중요한 부분은 6)번입니다.
- ALB Controller가
k8sClient.Status().Patch(...)
를 호출함으로써,- Pod의
status.conditions
중Type: target-health.elbv2.k8s.aws/<TGBName>
값을 업데이트합니다.- 이 값이
True
로 설정되어야 최종적으로 Kubernetes에서는 해당 ReadinessGate가 충족되었다고 판단하고, Pod가Ready
상태가 됩니다.
결과적으로
AWS ALB Target Group 헬스 체크
→ ALB Controller의 상태 조회
→ Pod Condition Patch
→ Pod Ready 상태 결정
→ Service Endpoints 포함 여부라는 흐름이 이루어집니다.
Kubernetes 기본 컨트롤러 중 하나인 Endpoints Controller는 Pod가 “Ready” 상태가 되면 해당 Pod IP를 해당 Service의 Endpoints(혹은 EndpointSlice)에 추가합니다.
Ready == True
인지 판단할 때, readinessGate가 모두 만족되어야 Ready로 표시됩니다.코드 레벨에서는 Kubernetes 레포지토리의
pkg/controller/endpoint/endpoints_controller.go
등에 구현되어 있습니다.func (e *Controller) syncService(key string) error { // ... // 1) Service Selector로부터 매칭되는 Pod 리스트를 가져온다. // 2) 각 Pod에 대해 IsPodReadyConditionTrue() 등을 통해 Ready 상태인지 확인한다. // 3) Ready 상태라면 Endpoints(또는 EndpointSlice)에 추가한다. // ... }
여기서 IsPodReadyConditionTrue()
는 Pod의 status.conditions
내 Ready Condition이 True
인지 확인합니다.
Namespace
에 elbv2.k8s.aws/pod-readiness-gate-inject: enabled
주석을 지정하면, AWS Load Balancer Controller가 Pod에 readinessGate
를 자동으로 추가하고, ALB와의 연동을 통해 더욱 안정적인 트래픽 관리를 수행할 수 있습니다.
이로써 서비스 트래픽의 안정성과 가용성을 높이고, 타이밍 불일치로 인한 오류를 효과적으로 줄일 수 있습니다.
주요 이점은 다음과 같습니다:
- 트래픽 안정성 향상
- Pod가 ALB 측에서 ‘Healthy’로 인식될 때만 서비스 엔드포인트로 등록되므로, 준비가 안 된(혹은 이미 종료된) Pod로 트래픽이 전달되는 상황을 최소화합니다.
- Failover 자동화
- 헬스 체크 실패 시, ALB가 정상적으로 인식한 다른 Ready 파드로 트래픽을 전환하여 다운타임을 줄입니다.
- 유연한 운영
- readinessGate는 외부 시스템 연동에도 활용 가능하므로, CI/CD, 모니터링, 기타 커스텀 프로브와 결합하여 더욱 세밀한 배포 전략을 세울 수 있습니다.
- 소스 코드 가시성
pod_readiness_gate.go
와resource_manager.go
를 통해, Pod와 ALB Target Group 간의 매핑이 어떤 로직으로 업데이트되는지 이해할 수 있습니다- 문제 발생 시, 코드 레벨의 디버깅도 가능해 안정적인 운영 및 트러블슈팅에 도움이 됩니다.
결론적으로, Pod Readiness Gate를 제대로 활용하면 새로운 버전을 롤아웃하거나 스케일 아웃/인 과정에서 ALB와 Kubernetes 간의 타이밍 불일치를 줄이고, 서비스 가용성을 크게 높일 수 있습니다.
이번 글에서 다룬 원리와 적용 방법을 바탕으로, 실제 EKS 운영 환경에서도 좀 더 안심하고 애플리케이션을 배포·관리할 수 있을 것입니다.