- Published on
EKS ALB Ingress 500 Target reset 원인·해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스나 단일 VM에서는 잘 안 보이던 500과 함께 ALB 액세스 로그에 target_reset이 찍히면, 대부분 “애플리케이션이 500을 냈다”가 아니라 로드밸런서(ALB)에서 타겟(대개 Pod)으로 연결한 TCP 세션이 타겟 측에서 리셋(RST)되거나 중간에 끊긴 것에 가깝습니다. 즉, 원인은 애플리케이션 코드일 수도 있지만, 더 자주 헬스체크/프로브/드레이닝/타임아웃/네트워크 경로처럼 인프라-런타임 영역에 숨어 있습니다.
이 글은 EKS + AWS Load Balancer Controller(= ALB Ingress) 환경에서 target_reset이 발생하는 대표 시나리오를 분류하고, “어디서 끊겼는지”를 빠르게 좁히는 진단 루틴과 재발 방지 설정을 제시합니다.
1) target_reset의 의미부터 정리
ALB 액세스 로그(또는 CloudWatch Logs로 내보낸 로깅)에서 다음과 같은 형태를 볼 수 있습니다.
elb_status_code=500target_status_code=-또는 비어 있음error_reason=Target.ResponseError혹은target_reset
여기서 핵심은 ALB가 타겟으로부터 “정상적인 HTTP 응답”을 끝까지 받지 못했다는 점입니다. 대표적으로:
- 타겟이 연결을 강제로 끊음(RST)
- 타겟이 응답을 보내기 전에 프로세스/컨테이너가 종료
- keep-alive 재사용 중 타겟이 이미 커넥션을 닫아 발생한 reset
- 중간 네트워크(노드/보안그룹/NACL)에서 세션이 끊김
즉, “500”은 ALB가 클라이언트에게 반환한 코드이고, 애플리케이션의 500과는 구분해야 합니다.
2) 먼저 확인할 로그/지표: 끊기는 지점 찾기
2.1 ALB 액세스 로그에서 패턴 추출
아래처럼 error_reason과 target_processing_time을 중심으로 봅니다.
target_processing_time이 매우 짧은데target_reset이면: 연결 직후 리셋(프로세스 크래시, 포트 미리스닝, readiness 불일치)target_processing_time이 길다가target_reset이면: 처리 중 종료/타임아웃/드레이닝 문제 가능
Athena로 S3 액세스 로그를 조회한다면(예시):
SELECT
elb_status_code,
target_status_code,
error_reason,
count(*) AS cnt,
approx_percentile(target_processing_time, 0.95) AS p95_target_time
FROM alb_logs
WHERE from_iso8601_timestamp(time) > now() - interval '1' hour
AND elb_status_code = 500
GROUP BY 1,2,3
ORDER BY cnt DESC;
2.2 Target Group 지표
CloudWatch에서 Target Group 단위로:
TargetConnectionErrorCount증가 여부HTTPCode_Target_5XX_Count가 아니라 ALB 5XX만 증가하는지
ALB 5XX만 증가하고 Target 5XX는 증가하지 않으면, 애플리케이션이 500을 낸 게 아니라 연결/전송 레벨 문제일 확률이 큽니다.
2.3 Pod/노드 이벤트와 함께 보기
- Pod 재시작(OOMKilled, CrashLoop, SIGTERM)
- 노드 교체/드레인/스팟 인터럽트
- readiness/liveness probe 실패
Probe/리소스 문제는 증상이 다양하므로, 필요하면 Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅에서 “프로브/리소스가 트래픽 중단으로 이어지는 패턴”을 먼저 점검하는 것이 빠릅니다.
3) 대표 원인 8가지와 해결책
3.1 Pod 종료(롤링 업데이트/스케일인) 중 커넥션이 끊김
가장 흔한 케이스입니다. Deployment 롤링 업데이트나 HPA 스케일인, 노드 드레인 중에 Pod가 SIGTERM을 받고 종료하면서 진행 중이던 요청 커넥션이 리셋됩니다.
해결 체크리스트
- terminationGracePeriodSeconds를 충분히 줍니다.
- 앱이 SIGTERM을 받으면 즉시 accept를 멈추고(또는 readiness를 false로), 진행 중 요청을 마무리한 뒤 종료하는 graceful shutdown을 구현합니다.
- Ingress/Service 라우팅에서 제외되는 타이밍과 실제 종료 타이밍을 맞춥니다.
예시(Deployment):
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: api
image: your-api:1.0.0
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 1
preStop sleep은 “정답”이라기보다 드레이닝 시간을 확보하기 위한 안전장치입니다. 핵심은 앱의 graceful shutdown입니다.
3.2 readinessProbe가 늦게 false가 되거나, readiness 없이 트래픽을 받음
Pod가 아직 준비되지 않았는데(서버 기동 중) ALB가 트래픽을 보내면, 앱이 커넥션을 즉시 끊거나(리스닝 전) 응답을 못해 reset/timeout이 발생할 수 있습니다.
해결
- readinessProbe 필수
- 앱이 실제로 요청 처리 가능해진 뒤 readiness가 true가 되도록 구성
- 초기화가 긴 경우
startupProbe도 고려
3.3 ALB idle timeout vs 앱/프록시 keep-alive 불일치
ALB의 기본 idle timeout(일반적으로 60초)을 넘겨 서버가 늦게 응답하거나, keep-alive 재사용 중 서버가 먼저 커넥션을 닫는 경우 클라이언트 관점에서 reset처럼 보이는 실패가 섞일 수 있습니다.
해결
- ALB idle timeout을 워크로드에 맞춰 조정
- 앱 서버의 keep-alive/headers timeout을 ALB와 정합성 있게 설정
ALB idle timeout(Ingress annotation 예시):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
annotations:
alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=120
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
3.4 Target Group deregistration delay(드레이닝) 부족
Pod가 종료되기 전에 Target Group에서 충분히 드레인되지 않으면, ALB가 “곧 죽을 타겟”에 요청을 보내다가 reset을 맞습니다.
해결
- Target Group의 deregistration delay를 늘리거나(워크로드 특성에 맞게)
- 위에서 말한 graceful shutdown + readiness 전환을 확실히
(설정은 컨트롤러/어노테이션/콘솔 방식이 혼재할 수 있으니, 현재 TG 속성이 어떻게 적용되는지부터 확인하세요.)
3.5 노드/파드 레벨 리소스 압박(OOM, CPU throttling)으로 처리 중 종료
- OOMKilled로 프로세스가 죽으면 기존 커넥션은 즉시 끊깁니다.
- CPU throttling이 심하면 응답 지연 → 타임아웃/드레이닝 실패로 연쇄됩니다.
해결
- requests/limits 재조정
- JVM/Node/Python 등 런타임별 메모리 상한 설정 점검
- 실제 트래픽에서 p95/p99 latency를 보고 타임아웃을 맞춤
3.6 HTTP/2(gRPC 포함)에서 서버/프록시 설정 불일치
gRPC나 HTTP/2를 ALB로 받는 구성에서, 서버의 keepalive 정책/최대 스트림/핑 정책이 맞지 않으면 연결이 끊겨 reset처럼 보일 수 있습니다.
- gRPC는 특히 클라이언트 측에서는
UNAVAILABLE,RST_STREAM등으로 관측 - 서버가 keepalive를 공격적으로 끊거나, 중간 프록시가 idle로 끊는 경우가 많습니다.
관련해서 gRPC 타임아웃/지연이 섞여 보이면 EKS에서 gRPC DEADLINE_EXCEEDED 폭증 해결도 함께 보는 것을 권합니다.
3.7 보안그룹/NACL/네트워크 경로에서 세션이 끊김
EKS에서 ALB → Node/Pod 경로는 보안그룹, NACL, 라우팅, CNI 상태에 영향을 받습니다.
- 노드가 NotReady/네트워크 불안정이면 연결이 리셋/드롭될 수 있음
- CNI 이슈로 Pod IP 할당/라우팅이 흔들리면 간헐적 연결 오류가 발생
노드 네트워크 계층 이슈가 의심되면 EKS Node NotReady - CNI ENI 할당 실패 해결 가이드에서 ENI/보조 IP/노드 상태를 함께 점검하세요.
3.8 헬스체크 경로/포트 불일치로 “정상 타겟처럼 보이다가” 트래픽에서만 실패
헬스체크는 통과하는데 실제 트래픽에서만 reset이 난다면:
- 헬스체크는
/healthz로 200을 주지만, 실제 경로는 앱 라우팅/미들웨어에서 강제 종료 - 헬스체크 포트와 서비스 포트가 다름
- readiness는 true인데 실제 의존성(DB/캐시)이 불안정
이 경우는 “ALB가 잘못 붙었다”기보다 앱이 준비 완료 신호를 과대평가하는 설계 문제일 때가 많습니다.
4) 실전 진단 루틴(10~20분 내 원인 좁히기)
4.1 1단계: 500이 ALB 500인지, 앱 500인지 분리
- ALB 로그에서
target_status_code가 비어 있고target_reset이면 앱 500이 아닐 가능성 ↑ - 앱 로그에 해당 요청 ID/trace가 남는지 확인
4.2 2단계: Pod 재시작/종료 이벤트와 시간대 매칭
kubectl get pods -n your-ns -o wide
kubectl describe pod -n your-ns <pod>
kubectl get events -n your-ns --sort-by=.lastTimestamp | tail -n 50
- OOMKilled, SIGTERM, Evicted, 노드 드레인 흔적이 있으면 3.1/3.5 쪽
4.3 3단계: readiness/프로브가 라우팅 제외를 제대로 트리거하는지
- readinessProbe 실패 시 Endpoints에서 빠지는지 확인
kubectl get endpoints -n your-ns <service-name> -w
롤링 업데이트 중 endpoints가 빠지는 타이밍과 실제 프로세스 종료 타이밍이 엇갈리면 reset이 나기 쉽습니다.
4.4 4단계: 타임아웃 정합성 점검(ALB ↔ Ingress ↔ 앱)
- ALB idle timeout
- 앱 서버 keep-alive/headers timeout
- 프레임워크/프록시(Nginx, Envoy 등) 업스트림 타임아웃
이 중 하나가 너무 짧으면 “중간에서 끊김”이 발생합니다.
5) 재발 방지용 권장 설정 템플릿
5.1 Ingress(Idle timeout, 헬스체크 튜닝)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
annotations:
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/healthcheck-path: /healthz
alb.ingress.kubernetes.io/healthcheck-interval-seconds: "10"
alb.ingress.kubernetes.io/healthcheck-timeout-seconds: "5"
alb.ingress.kubernetes.io/healthy-threshold-count: "2"
alb.ingress.kubernetes.io/unhealthy-threshold-count: "2"
alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=120
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- 헬스체크는 “빠르게 죽은 타겟을 빼는 것”과 “일시적 흔들림에 과민반응하지 않는 것”의 균형이 필요합니다.
5.2 Deployment(그레이스풀 종료 + 충분한 종료 유예)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: api
image: your-api:1.0.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 1
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
maxUnavailable: 0는 가용성을 우선할 때 유용하지만, 리소스 여유가 있어야 합니다.
6) 자주 하는 오해와 반례
오해: “ALB가 500이니 백엔드가 500을 낸 것”
- 반례:
target_reset은 HTTP 응답을 받기 전에 연결이 깨진 경우가 많습니다.
- 반례:
오해: “헬스체크가 정상인데 왜 reset이 나지?”
- 반례: 헬스체크는 특정 경로/짧은 요청만 보며, 실제 트래픽의 장시간 요청/스트리밍/DB 의존성은 반영하지 못합니다.
오해: “terminationGracePeriodSeconds만 늘리면 해결”
- 반례: 앱이 SIGTERM을 무시하거나 즉시 종료하면 유예 시간을 늘려도 소용이 없습니다. 그레이스풀 종료 구현이 핵심입니다.
7) 마무리: 결론은 ‘리셋은 대부분 종료/타임아웃/정합성’
EKS의 ALB Ingress에서 500 + target_reset이 보이면, 우선순위는 다음 순서로 두는 것이 효율적입니다.
- Pod 종료/재시작/드레인 이벤트와 시간대 매칭
- readiness/프로브가 라우팅 제외를 제대로 만드는지
- ALB idle timeout ↔ 앱 keep-alive/timeout 정합성
- 네트워크(CNI/노드 상태/보안그룹) 이상 여부
같은 5XX라도 503/404처럼 규칙/타겟그룹 문제로 고정되는 유형과는 접근이 다르니, 증상이 섞여 있다면 EKS Ingress 503인데 Pod 정상일 때 점검 가이드도 함께 참고하면 분리 진단에 도움이 됩니다.
원인을 특정하기 위해 필요한 최소 정보는 보통 3가지입니다: ALB 액세스 로그 한 줄(마스킹), 해당 시각의 Pod 이벤트, 그리고 타임아웃/드레이닝 관련 설정 값. 이 3가지만 모아도 target_reset의 80%는 재현 없이도 방향이 잡힙니다.