HTTP 301이 뜨는 이유 — ALB ssl-redirect
curl로 API를 호출했을 때 301이 뜬 이유를 파고들다 ALB의 ssl-redirect annotation까지 닿았다.
왜 궁금해졌는가
curl work-api.test-nudge-eap.io/readyz 를 날렸더니 정상 응답 대신 이게 돌아왔다.
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
</body>
</html>
반면 https://를 명시해서 치면:
{"status":"ready"}
같은 엔드포인트인데 왜 한쪽은 301이고 한쪽은 200인가.
탐구
301이 정확히 무엇인가
301 Moved Permanently는 리소스가 영구적으로 다른 URL로 이동했다는 뜻이다.
- 클라이언트에게 앞으로는 새 URL을 쓰라고 알림
- 브라우저는 이 결과를 캐싱함
- 응답에
Location헤더가 포함됨
curl -v 로 확인하면:
< HTTP/1.1 301 Moved Permanently
< Location: https://work-api.test-nudge-eap.io/readyz
→ http://로 보낸 요청을 https://로 리다이렉트하고 있다.
누가 301을 보내는가
EKS + ALB + Ingress 구조라면 트래픽 경로는 이렇다.
Route53
↓
ALB (HTTP 80 → HTTPS 443 redirect)
↓
Ingress
↓
Service
↓
Pod
301을 보내는 주체 후보는 세 가지다.
- ALB Listener Rule — 80 → 443 redirect 설정
- nginx ingress annotation —
nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - Application 레벨 — 앱 코드에서 직접 redirect
이 경우는 ALB 레벨에서 발생한다.
Ingress annotation이 원인이었다
실제 Ingress 설정을 보면:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress
annotations:
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-2:...
alb.ingress.kubernetes.io/healthcheck-path: /readyz
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
alb.ingress.kubernetes.io/ssl-redirect: '443' # ← 이것
alb.ingress.kubernetes.io/target-type: ipalb.ingress.kubernetes.io/ssl-redirect: '443' 이 annotation 때문이다.
ssl-redirect annotation의 동작 원리
AWS Load Balancer Controller는 이 annotation을 보고 ALB를 다음과 같이 구성한다.
80 리스너:
- 기본 action: Redirect
- 대상 포트: 443
- 상태 코드:
HTTP_301
443 리스너:
- TargetGroup으로 forward
실제 트래픽 흐름:
HTTP 80 요청
↓
ALB Listener (301 redirect)
↓
HTTPS 443
↓
TargetGroup → Pod
→ 301은 Ingress나 Pod가 아니라 ALB 레벨에서 발생한다.
그런데 브라우저에서는 왜 안 보이는가
브라우저로 http://work-api.test-nudge-eap.io/readyz를 열면 301이 뜨지 않고 바로 응답이 온다. 301이 사라진 게 아니라, 브라우저가 301을 받고 자동으로 따라가기 때문이다.
HTTP 클라이언트마다 리다이렉트를 처리하는 방식이 다르다.
| 클라이언트 | 301 수신 시 동작 | 사용자에게 보이는 것 |
|---|---|---|
| 브라우저 | Location 헤더의 URL로 자동 재요청 | 최종 응답(200)만 보임 |
| curl (기본) | 301 응답에서 멈춤 | 301 HTML 그대로 출력 |
| curl -L | 브라우저처럼 자동으로 따라감 | 최종 응답(200)이 출력됨 |
브라우저가 자동으로 따라가는 이유는 HTTP 스펙상 301은 클라이언트가 새 URL로 재요청해야 한다고 정의되어 있고, 브라우저는 이를 UX 차원에서 자동 처리하기 때문이다. 추가로 브라우저는 301 결과를 캐싱하기 때문에, 같은 URL을 다시 열면 서버에 요청조차 보내지 않고 바로 https://로 간다.
curl은 기본적으로 이 자동 처리를 하지 않는다. API 클라이언트나 스크립트는 리다이렉트를 명시적으로 인지해야 하는 경우가 많기 때문에 기본 동작이 멈추는 것이다.
# 301에서 멈춤 (기본)
curl http://work-api.test-nudge-eap.io/readyz
# 브라우저처럼 자동으로 따라감
curl -L http://work-api.test-nudge-eap.io/readyz이해한 것
301은 오류가 아니라 의도된 보안 설정이다.
| 요청 | 결과 | 이유 |
|---|---|---|
http:// | 301 | HTTPS 강제 리다이렉트 |
https:// | 200 | 정상 |
HTTP는 평문이기 때문에 인증 토큰·쿠키가 노출될 위험이 있다. 그래서 HTTP 요청을 HTTPS로 강제 전환하는 게 보안 표준이다.
ssl-redirect: '443' annotation을 제거하면 301이 사라지고 HTTP 직접 접근이 가능해지지만, 보안상 권장되지 않는다.
curl에서 리다이렉트를 따라가려면 -L 옵션을 쓴다. 이 옵션이 없으면 301에서 멈추고, 있으면 브라우저처럼 자동으로 최종 응답까지 따라간다.