Published on

EKS ALB Ingress 404 고정 - 10분 규칙·TG 진단

Authors

서버가 살아있는데도 ALB Ingress가 항상 404만 내보내면, 대개 애플리케이션 문제가 아니라 라우팅(규칙) 또는 타겟그룹(Target Group, TG) 연결에서 어긋난 경우가 많습니다. 특히 AWS Load Balancer Controller(구 ALB Ingress Controller)가 만든 Listener Rule이 기대와 다르게 생성되거나, TG가 비어있거나 Unhealthy면 ALB는 기본 동작으로 404(또는 고정 응답/기본 액션)를 반환합니다.

이 글은 “10분 안에” 문제를 좁히는 것을 목표로, 다음 2가지를 빠르게 분리합니다.

  • 규칙 문제: Host/Path가 매칭되지 않아 기본 규칙으로 떨어짐
  • 타겟그룹 문제: 규칙은 맞는데 TG에 정상 타겟이 없거나(0개), Unhealthy라 전달 실패

아래 절차대로 하면 kubectl과 AWS CLI만으로도 원인을 상당히 빠르게 특정할 수 있습니다.

0) 먼저 확인: 이 404가 "ALB 404"인가 "앱 404"인가

가장 먼저 해야 할 일은 “어디서 404가 나오는지”입니다.

  • ALB 404: 응답 바디가 짧고(고정), 서버 헤더가 awselb/2.0인 경우가 많습니다.
  • 앱 404: 프레임워크/앱의 404 페이지(HTML/JSON) 형태가 그대로 나옵니다.

다음처럼 헤더를 확인합니다.

curl -sS -D- -o /dev/null http://<alb-dns-name>/some/path

여기서 server: awselb/2.0에 가깝다면, 이제부터는 Ingress 규칙/ALB Rule/TG를 봐야 합니다.

1) 2분 컷: Ingress 스펙(Host/Path/Service/Port)부터 정합성 확인

Ingress가 기대대로 작성되어 있어도, 사소한 오타로 규칙이 다르게 생성될 수 있습니다.

kubectl -n <ns> get ingress <ing-name> -o yaml
kubectl -n <ns> describe ingress <ing-name>

1-1) Host가 실제 요청 Host와 일치하는가

ALB Ingress는 보통 host 기반 규칙을 만듭니다.

  • 브라우저로 http://alb-dns/로 치면 Host는 alb-dns-name입니다.
  • Ingress가 host: api.example.com이면, alb-dns-name으로 접근 시 규칙 미매칭 → 기본 규칙 → 404가 납니다.

검증:

curl -H 'Host: api.example.com' http://<alb-dns-name>/

이렇게 했을 때 정상이라면, 문제는 애초에 DNS(CNAME) 또는 Host 헤더입니다.

1-2) PathType/경로가 ALB 규칙으로 의도대로 번역되는가

특히 pathType: ImplementationSpecific은 컨트롤러/버전에 따라 기대와 달라질 수 있습니다. 가능하면 명시적으로:

  • Prefix: /api/api, /api/xxx 모두 매칭
  • Exact: 정확히 /api만 매칭

예시:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo
  namespace: demo
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-svc
            port:
              number: 80

1-3) Service 포트/타겟 포트가 맞는가

Ingress는 Service의 port.number로 연결합니다. Service가 80을 열어도 실제 Pod가 8080을 듣는다면 targetPort가 맞아야 합니다.

kubectl -n <ns> get svc api-svc -o yaml
kubectl -n <ns> get endpoints api-svc -o wide
# 또는 EndpointSlice
kubectl -n <ns> get endpointslice -l kubernetes.io/service-name=api-svc -o wide

Endpoints/EndpointSlice가 비어 있으면(주소 0개) TG도 비어 있게 되고, 이때도 404/502 계열로 이어질 수 있습니다.

2) 3분 컷: AWS Load Balancer Controller 이벤트로 "규칙 생성" 실패 여부 확인

컨트롤러가 Ingress를 ALB로 변환하는 과정에서 실패하면, Ingress는 있어도 ALB 측 리소스가 덜 만들어지거나 엉뚱하게 만들어질 수 있습니다.

kubectl -n kube-system get deploy | grep -i load-balancer-controller
kubectl -n kube-system logs deploy/aws-load-balancer-controller --tail=200

Ingress 자체 이벤트도 확인합니다.

kubectl -n <ns> describe ingress <ing-name>

자주 나오는 원인:

  • 서브넷 태그/선택 실패
  • 보안그룹/권한(특히 IRSA) 문제
  • annotation 오타로 인한 파싱 실패

IRSA/권한 쪽이 의심되면 아래 글의 점검 루틴도 같이 보면 도움이 됩니다.

3) 5분 컷: 실제 ALB Listener Rule이 "요청"과 매칭되는지 확인

Ingress가 맞아 보여도, 최종적으로는 ALB Listener Rule이 요청을 받아야 합니다.

3-1) ALB ARN과 Listener ARN 찾기

Ingress에 생성된 ALB DNS/이름을 먼저 얻습니다.

kubectl -n <ns> get ingress <ing-name> -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'; echo

AWS CLI로 ALB를 찾습니다.

aws elbv2 describe-load-balancers \
  --query "LoadBalancers[?DNSName=='<alb-dns-name>'].[LoadBalancerArn,LoadBalancerName]" \
  --output text

Listener(대개 80/443)를 확인:

aws elbv2 describe-listeners --load-balancer-arn <alb-arn>

3-2) Listener Rule 우선순위/조건 확인

ALB는 **우선순위(priority)**가 높은 규칙부터 평가합니다. 기대했던 host/path 조건이 없다면 기본 규칙으로 떨어집니다.

aws elbv2 describe-rules --listener-arn <listener-arn>

여기서 확인할 것:

  • Conditions에 host-header가 있는가
  • path-pattern이 기대대로 들어갔는가
  • Actions가 올바른 TargetGroupArn을 가리키는가
  • Default rule이 fixed-response 404인지(혹은 다른 TG로 가는지)

패턴이 흔히 틀리는 케이스

  • /api/*를 기대했는데 실제는 /api*로 들어가거나, 반대로 /api만 매칭
  • host가 api.example.com인데 요청은 www.api.example.com

이 단계에서 “규칙이 없다/조건이 다르다”면, Ingress YAML(Host/PathType/annotation)로 돌아가 수정하는 게 가장 빠릅니다.

4) 10분 완성: Target Group에 타겟이 붙었는지/Healthy인지 확인

규칙이 맞는데도 404가 고정이면, 다음은 TG입니다. (ALB가 기본 규칙으로 떨어져 404인 경우도 있지만, 규칙이 맞다면 TG 상태를 보는 게 정석입니다.)

4-1) Rule이 가리키는 TargetGroupArn 확인

describe-rules 결과에서 Actions의 TargetGroupArn을 찾습니다.

4-2) TG 타겟 등록/헬스 상태 확인

aws elbv2 describe-target-health --target-group-arn <tg-arn>

대표적인 분기:

  • 타겟이 0개: Service endpoints가 없거나, 컨트롤러가 타겟 등록을 못함
  • unhealthy: 헬스체크 path/port/success code 불일치, SG/NACL 차단, Pod readiness 문제

NLB에서 타겟 Unhealthy를 파는 글이지만, “타겟/헬스체크/보안그룹” 관점은 ALB에도 그대로 적용됩니다.

4-3) 헬스체크 경로가 앱에서 200을 반환하는가

ALB TG 헬스체크는 기본이 /인 경우가 많고, 앱이 /에서 404를 주면 타겟이 Unhealthy가 됩니다.

Ingress annotation으로 명시할 수 있습니다.

metadata:
  annotations:
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
    alb.ingress.kubernetes.io/success-codes: "200"

그리고 Pod에서 직접 확인:

kubectl -n <ns> port-forward svc/api-svc 18080:80
curl -i http://127.0.0.1:18080/healthz

4-4) target-type(Instance vs IP) 불일치 점검

AWS Load Balancer Controller는 보통 target-type: ip를 많이 씁니다. 그런데 클러스터/서브넷/보안그룹 구성에 따라 instance로 붙는 구성도 있습니다.

kubectl -n <ns> get ingress <ing-name> -o jsonpath='{.metadata.annotations.alb\.ingress\.kubernetes\.io/target-type}'; echo
  • ip: Pod IP로 TG에 등록 (보안그룹/서브넷 라우팅이 Pod IP 대역과 맞아야 함)
  • instance: NodePort로 노드에 등록 (Service가 NodePort를 열어야 함)

이게 꼬이면 “규칙은 맞는데 TG가 기대 포트로 못 붙음”이 발생합니다.

5) 자주 나오는 "404 고정" 원인 Top 7 (체크리스트)

아래는 현장에서 가장 많이 만나는 순서대로 정리한 체크리스트입니다.

  1. Host 불일치: Ingress host와 실제 요청 Host가 다름 (ALB DNS로 직접 접속)
  2. Path 매칭 실패: Prefix/Exact/패턴 차이로 rule 미매칭
  3. IngressClass/annotation 오류: kubernetes.io/ingress.class: alb 누락/오타
  4. Service 포트 불일치: Ingress backend port ↔ Service port ↔ Pod containerPort 불일치
  5. Endpoints 0개: selector 라벨 불일치, Pod Ready 아님
  6. TG unhealthy: healthcheck-path가 404, success code 불일치, readiness probe와 불일치
  7. 보안그룹/네트워크: ALB → Pod/Node로 트래픽이 못 감 (특히 ip target-type)

네트워크 레이어가 의심되면 IPv6/보안그룹/DNS까지 같이 점검해보는 게 좋습니다.

6) 재현 가능한 "10분 진단" 커맨드 세트

아래만 순서대로 실행해도, 대부분의 404 고정은 10분 내에 갈라집니다.

# 1) Ingress 핵심 정보
kubectl -n <ns> get ing <ing-name> -o wide
kubectl -n <ns> describe ing <ing-name>

# 2) Service/Endpoints
kubectl -n <ns> get svc <svc-name> -o yaml | sed -n '1,120p'
kubectl -n <ns> get endpoints <svc-name> -o wide

# 3) ALB DNS 확보
ALB_DNS=$(kubectl -n <ns> get ing <ing-name> -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo $ALB_DNS

# 4) Host 매칭 테스트
curl -sS -D- -o /dev/null http://$ALB_DNS/
curl -sS -D- -o /dev/null -H 'Host: <expected-host>' http://$ALB_DNS/<path>

# 5) AWS: ALB/LISTENER/RULE/TG 확인
ALB_ARN=$(aws elbv2 describe-load-balancers --query "LoadBalancers[?DNSName=='$ALB_DNS'].LoadBalancerArn" --output text)
aws elbv2 describe-listeners --load-balancer-arn $ALB_ARN --output table

LISTENER_ARN=$(aws elbv2 describe-listeners --load-balancer-arn $ALB_ARN --query 'Listeners[0].ListenerArn' --output text)
aws elbv2 describe-rules --listener-arn $LISTENER_ARN --output json | jq '.Rules[] | {Priority,Conditions,Actions}'

# (Rule에서 TG ARN을 찾은 뒤)
aws elbv2 describe-target-health --target-group-arn <tg-arn> --output table

jq가 없으면 --output json 그대로 보고 TargetGroupArn만 눈으로 찾아도 됩니다.

7) 마무리: "규칙"과 "TG"를 분리하면 404는 빨리 끝난다

ALB Ingress 404 고정은 감으로 파기 시작하면 오래 걸리지만, 다음 두 질문으로 분리하면 빠릅니다.

  1. 요청이 Listener Rule에 매칭되는가? (Host/Path)
  2. 매칭된 Rule이 가리키는 TG에 Healthy 타겟이 있는가? (Endpoints/Healthcheck)

이 글의 순서대로 보면, 대부분은 Host 헤더 불일치 또는 healthcheck-path 404 같은 단순한 원인으로 귀결됩니다. 반대로 컨트롤러 권한/네트워크 이슈가 섞여 있으면 이벤트/로그에서 힌트가 나오니, “Ingress 스펙 → ALB Rule → TG health” 순서를 고정 루틴으로 만들어두는 것을 추천합니다.