Published on

EKS에서 Pod→S3가 301 리다이렉트로 실패할 때

Authors

서버에서 S3를 호출하다가 301 Moved Permanently를 만나면 대부분 “리다이렉트를 따라가면 되지 않나?”라고 생각합니다. 하지만 EKS Pod 환경에서는 리다이렉트가 곧 실패로 이어지는 경우가 흔합니다. 예를 들어 (1) 서명된 요청(SigV4)이 리다이렉트 후 무효화되거나, (2) SDK/클라이언트가 301을 자동으로 따라가지 않거나, (3) 프록시/네트워크 정책이 리다이렉트 대상 도메인을 막아버리거나, (4) S3가 “정확한 리전 엔드포인트로 다시 오라”고 301을 주는 상황이 겹칩니다.

이 글은 EKS Pod → S3 요청이 301로 실패하는 대표 케이스를 빠르게 분류하고, 로그/헤더 기반으로 원인을 좁힌 뒤, 애플리케이션/SDK/인프라 설정에서 어떻게 고치는지 정리합니다.

301이 뜨는 순간: S3가 실제로 말하는 것

S3의 301은 단순한 HTTP 리다이렉트가 아니라, 대개 아래 중 하나의 신호입니다.

  1. 잘못된 리전으로 요청했다 (특히 s3.amazonaws.com 또는 타 리전 엔드포인트로 요청)
  2. 버킷이 특정 리전에 고정인데 글로벌 엔드포인트로 접근했다
  3. Path-style vs Virtual-hosted-style 또는 커스텀 엔드포인트(S3 호환 스토리지 포함) 설정이 꼬였다
  4. 프록시/서비스 메시/인그레스가 S3 트래픽을 가로채며 리다이렉트를 유발했다

S3는 301 응답에 힌트를 넣습니다. 대표적으로:

  • x-amz-bucket-region: ap-northeast-2
  • Location: https://<bucket>.s3.<region>.amazonaws.com/...

따라서 첫 번째 목표는 “S3가 어느 리전으로 오라고 하는지”를 확인하는 것입니다.

Pod 안에서 즉시 재현/확인하는 방법 (curl)

우선 Pod 내부에서 실제로 어떤 URL로 나가고 어떤 헤더가 오는지 확인합니다.

# Pod 내부에서 실행
apk add --no-cache curl || true

BUCKET=my-bucket
KEY=healthcheck.txt

# 1) 글로벌 엔드포인트(또는 잘못된 엔드포인트)로 호출해 일부러 301을 유발해보기
curl -sv "https://s3.amazonaws.com/${BUCKET}/${KEY}" -o /dev/null

# 2) 301 응답의 Location/x-amz-bucket-region 확인
# 출력에서 다음을 찾습니다:
# < HTTP/2 301
# < x-amz-bucket-region: ap-northeast-2
# < location: https://my-bucket.s3.ap-northeast-2.amazonaws.com/healthcheck.txt

만약 여기서 x-amz-bucket-region이 보이면, 해결의 80%는 끝난 겁니다. 애플리케이션이 해당 리전 엔드포인트로 직접 요청하도록 바꾸면 됩니다.

가장 흔한 원인 1: 리전 미지정(또는 잘못 지정) + SigV4

EKS에서 IRSA를 쓰든, 노드 IAM Role을 쓰든, S3 요청은 결국 SigV4 서명으로 나갑니다. 그런데 다음과 같은 경우 301이 자주 발생합니다.

  • SDK가 기본 리전을 못 찾았고, 글로벌 엔드포인트로 호출
  • AWS_REGION/AWS_DEFAULT_REGION이 잘못 들어감
  • 멀티 리전 환경에서 ConfigMap/Secret이 섞여 잘못된 리전으로 배포

해결: SDK에서 리전을 “명시적으로” 설정

(예) AWS SDK for JavaScript v3

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

const region = process.env.AWS_REGION || "ap-northeast-2"; // 운영에서는 명시 권장

const s3 = new S3Client({ region });

export async function getObject(bucket: string, key: string) {
  const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
  return res;
}

(예) boto3 (Python)

import os
import boto3

region = os.getenv("AWS_REGION", "ap-northeast-2")

s3 = boto3.client("s3", region_name=region)

def head_object(bucket, key):
    return s3.head_object(Bucket=bucket, Key=key)

포인트: “리다이렉트 따라가면 되겠지”가 아니라, 처음부터 올바른 리전 엔드포인트로 서명해서 요청해야 301/서명오류의 연쇄를 끊을 수 있습니다.

가장 흔한 원인 2: S3 URL 구성 실수 (virtual-hosted-style vs path-style)

S3는 이제 virtual-hosted-style(https://bucket.s3.region.amazonaws.com/key)이 표준입니다. 반면 path-style(https://s3.region.amazonaws.com/bucket/key)은 제약이 많고(특히 일부 리전/버킷 정책/가속/듀얼스택 조합), 잘못 쓰면 301/307으로 튕길 수 있습니다.

진단 체크

  • 애플리케이션이 s3.amazonaws.com/<bucket>/... 형태를 만들고 있지 않은가?
  • 프리사인 URL을 생성할 때 호스트가 s3.amazonaws.com으로 고정돼 있지 않은가?

해결: 올바른 호스트를 사용

  • 다운로드/업로드 URL은 가능하면 https://{bucket}.s3.{region}.amazonaws.com/{key}
  • SDK를 쓰면 대개 자동으로 처리되지만, 커스텀 엔드포인트를 조합하면 수동 설정이 필요합니다.

원인 3: VPC Endpoint(S3 Gateway/Interface) + DNS/리전 불일치

프라이빗 서브넷에서 S3로 나갈 때는 보통 S3 Gateway Endpoint를 사용합니다. 이때도 301이 날 수 있습니다.

  • Pod가 s3.amazonaws.com으로 호출 → DNS/라우팅은 되지만 S3가 “버킷 리전이 다르다”고 301
  • 멀티 리전 버킷/교차 리전 액세스 시도에서 엔드포인트 정책/라우트가 기대와 다르게 동작

빠른 확인

Pod에서 DNS 확인:

nslookup my-bucket.s3.ap-northeast-2.amazonaws.com
nslookup s3.amazonaws.com

그리고 버킷 리전을 AWS CLI로 확인(가능하면 동일 Pod에서):

aws s3api get-bucket-location --bucket my-bucket
# 출력이 null이면 us-east-1 의미

해결 방향

  • 애플리케이션/SDK 리전을 버킷 리전과 일치
  • S3 엔드포인트 정책이 특정 버킷/리전만 허용하도록 과도하게 제한돼 있지 않은지 점검

원인 4: 프록시(HTTP_PROXY) 또는 서비스 메시가 301을 만든다

EKS 클러스터에 사내 프록시, Istio/Envoy, egress gateway, 혹은 보안 솔루션이 붙어 있으면 다음이 발생할 수 있습니다.

  • HTTPS 트래픽을 프록시가 중간에서 처리하며 http → https 또는 특정 도메인으로 301
  • 프록시가 Location 헤더의 도메인(예: bucket.s3.region.amazonaws.com)을 허용 목록에 없다고 차단

진단

Pod 환경 변수 확인:

env | egrep -i 'http_proxy|https_proxy|no_proxy'

curl로 프록시 우회 테스트:

# 프록시 환경이 있는 경우, 일시적으로 우회
curl -sv --noproxy "*" "https://my-bucket.s3.ap-northeast-2.amazonaws.com/" -o /dev/null

해결

  • NO_PROXY.amazonaws.com, .s3.<region>.amazonaws.com 등을 포함
  • 서비스 메시를 쓴다면 S3 도메인을 egress 제외(바이패스)하거나, egress gateway에서 TLS 원형 보존 설정 검토

원인 5: SDK/클라이언트가 301을 “실패”로 취급

일부 HTTP 클라이언트는 301/302를 자동으로 따라가지만, 다음 조건에선 실패할 수 있습니다.

  • PUT/POST 요청에서 리다이렉트 시 메서드/바디 변경
  • 리다이렉트 후 호스트가 바뀌며 SigV4 서명 불일치
  • 보안상 리다이렉트를 금지하도록 설정됨

해결 원칙

  • 리다이렉트를 따라가게 만들기보다, 리다이렉트가 발생하지 않게(정확한 리전/호스트) 구성
  • 프리사인 URL을 쓴다면 생성 시점부터 올바른 호스트/리전을 강제

실전 체크리스트 (10분 컷)

아래 순서대로 보면 대부분 원인이 드러납니다.

  1. Pod에서 curl -v로 301 응답 헤더 확인
    • x-amz-bucket-region / Location 캡처
  2. 애플리케이션이 구성한 S3 URL/엔드포인트 확인
    • s3.amazonaws.com 고정 여부
  3. SDK region 설정 확인
    • AWS_REGION, AWS_DEFAULT_REGION, 코드 내 region
  4. 프록시/서비스 메시 여부 확인
    • HTTP(S)_PROXY, NO_PROXY, egress 정책
  5. VPC Endpoint/DNS 확인
    • 프라이빗 환경에서 S3 엔드포인트 정책/라우트

(보너스) 301 다음에 403/SignatureDoesNotMatch로 이어진다면

301 자체는 “다른 곳으로 가라”는 뜻이지만, 실제 장애는 그 다음 단계에서 터집니다.

  • 301을 따라가며 호스트가 바뀜 → 기존 SigV4 서명은 다른 호스트 기준이라 무효
  • 결과적으로 403 SignatureDoesNotMatch 또는 AccessDenied로 관측

이 경우는 접근 권한 문제처럼 보이지만, 출발점은 리전/호스트 불일치인 경우가 많습니다. 403으로 보인다면 아래 글도 함께 보면 원인 분리가 빨라집니다.

결론: 301은 “네트워크”가 아니라 “엔드포인트/리전” 문제인 경우가 대부분

EKS Pod에서 S3 호출이 301로 실패할 때 핵심은 단순합니다.

  • S3가 알려주는 x-amz-bucket-regionLocation을 먼저 확인하고
  • SDK/애플리케이션이 처음부터 그 리전 엔드포인트로 요청하게 만들며
  • 프록시/서비스 메시/엔드포인트 정책이 리다이렉트 대상 도메인을 막지 않는지 점검합니다.

이 3가지만 정리하면, “Pod에서는 301로 실패하는데 로컬에서는 된다” 같은 애매한 케이스도 대부분 깔끔하게 해결됩니다.