Published on

EKS ImagePullBackOff 429 Too Many Requests 해결

Authors

서버리스가 아닌 이상, 쿠버네티스 장애의 절반은 “컨테이너 이미지를 못 당겨서” 시작합니다. 특히 EKS에서 노드가 늘거나(오토스케일), 대규모 롤아웃이 발생할 때 ImagePullBackOff와 함께 이벤트에 429 Too Many Requests가 찍히면 레지스트리의 rate limit(요청 제한) 또는 동시 pull 폭주를 의심해야 합니다. 문제는 단순히 “잠깐 기다리면 된다”가 아니라, 다음 스케일아웃/재배포 때 동일하게 터진다는 점입니다.

이 글에서는 EKS 환경에서 429 기반 ImagePullBackOff정확히 재현 가능한 방식으로 진단하고, ECR 미러링/캐시/프리풀/배포 전략으로 근본적으로 줄이는 실전 해법을 정리합니다.

증상과 429의 의미

대표적인 현상은 다음과 같습니다.

  • Pod 상태: ImagePullBackOff 또는 ErrImagePull
  • 이벤트 메시지에 429 Too Many Requests 포함
  • 특정 시점(노드 스케일아웃, 롤링 업데이트, 장애 복구 후 재스케줄)에서 집중 발생

쿠버네티스 입장에서 429는 “이미지 레지스트리가 요청을 더 못 받겠다”는 뜻입니다. 원인은 보통 아래 중 하나(또는 복합)입니다.

  • Docker Hub / GHCR / 기타 외부 레지스트리의 rate limit
  • 노드 수 증가 + 다수 Pod 동시 기동으로 동시 pull 폭주
  • imagePullPolicy: Always로 인해 불필요한 pull 증가
  • 노드 로컬 캐시 부재(새 노드가 늘 때마다 동일 이미지 재다운로드)
  • NAT Gateway/프록시/방화벽 구간에서 재시도 폭증(결과적으로 레지스트리 관점에서 폭주)

1단계: 이벤트로 “정확히 429인지” 확인

가장 먼저 Pod 이벤트를 확인해 429가 실제 원인인지(예: 401, 403, DNS, TLS 등과 구분) 확정합니다.

# 특정 Pod 확인
kubectl describe pod -n <ns> <pod>

# 네임스페이스 전체에서 pull 관련 이벤트만 빠르게 보기
kubectl get events -n <ns> --sort-by=.lastTimestamp \
  | egrep -i "pull|image|back-off|429|too many"

이벤트 예시는 대략 아래 형태입니다.

  • Failed to pull image ...: rpc error: ... 429 Too Many Requests
  • Back-off pulling image ...

만약 kubectl logs/exec 자체가 안 되는 상황이면(노드/네트워크/웹소켓/RBAC 등), 먼저 그 경로부터 복구해야 진단이 수월합니다. 이 케이스는 아래 글의 체크리스트가 도움이 됩니다.

2단계: 어떤 레지스트리에서 429가 나는지 분리

이미지 이름을 보면 대개 감이 오지만, 실제로는 멀티 스테이지/사이드카/InitContainer 등에서 다른 레지스트리를 당길 수 있습니다.

# Pod의 모든 이미지(컨테이너+initContainer) 확인
kubectl get pod -n <ns> <pod> -o jsonpath='{range .spec.initContainers[*]}{.image}{"\n"}{end}{range .spec.containers[*]}{.image}{"\n"}{end}'

여기서 예를 들어 다음과 같이 분류합니다.

  • docker.io/... → Docker Hub rate limit 가능성 높음
  • ghcr.io/... → GitHub Container Registry 제한/토큰 정책 확인
  • quay.io/... → Quay 제한/인증 정책 확인
  • <account>.dkr.ecr.<region>.amazonaws.com/... → ECR 자체는 비교적 여유롭지만, 프록시/엔드포인트/권한 문제는 별도

3단계: 재현 패턴 찾기(스케일아웃/롤아웃/노드 교체)

429는 “평상시엔 멀쩡하다가 특정 이벤트에서만” 터지는 경우가 많습니다.

  • HPA/Cluster Autoscaler/Karpenter로 노드가 급증
  • Deployment 롤링 업데이트로 많은 Pod가 동시에 재기동
  • Spot 중단/노드 교체로 동일 이미지가 새 노드에서 재다운로드

특히 Karpenter를 쓰는 환경에서 노드가 충분히 늘지 않거나, 늘긴 늘었는데 새 노드에서 pull이 폭주해 429가 나는 경우가 자주 섞여 보입니다. 스케일링 자체 이슈와 pull 이슈를 분리해서 봐야 합니다.

해결 전략 개요(우선순위)

429 문제는 “한 가지 설정”으로 끝나기 어렵고, 아래를 조합할수록 재발 확률이 급격히 떨어집니다.

  1. 외부 레지스트리 이미지를 ECR로 미러링(가장 확실)
  2. 노드에 이미지 프리풀(특히 애드온/핵심 이미지)
  3. 불필요한 pull 감소(태그 전략 + imagePullPolicy 조정)
  4. 롤아웃/스케줄링 동시성 제한(서지/버짓/점진 배포)
  5. 레지스트리 인증/토큰 적용(Docker Hub 유료/토큰, GHCR PAT 등)
  6. (가능하면) 노드 로컬 캐시/프록시 레지스트리 도입

아래부터는 실무에서 가장 효과가 큰 순서로 구체적인 방법을 설명합니다.

해법 1: 외부 이미지를 ECR로 미러링(권장)

Docker Hub의 익명/무료 계정 rate limit은 스케일아웃 시 치명적입니다. 가장 확실한 방법은 외부 이미지를 ECR에 복제해 내부 레지스트리처럼 쓰는 것입니다.

1) 수동 미러링(즉시 대응)

# 1) 외부 이미지 pull
docker pull docker.io/library/nginx:1.25

# 2) ECR 로그인
aws ecr get-login-password --region ap-northeast-2 \
  | docker login --username AWS --password-stdin <account>.dkr.ecr.ap-northeast-2.amazonaws.com

# 3) 태깅 후 push
docker tag nginx:1.25 <account>.dkr.ecr.ap-northeast-2.amazonaws.com/mirror/nginx:1.25
docker push <account>.dkr.ecr.ap-northeast-2.amazonaws.com/mirror/nginx:1.25

이후 Deployment에서 이미지를 ECR 경로로 바꿉니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: nginx
          image: <account>.dkr.ecr.ap-northeast-2.amazonaws.com/mirror/nginx:1.25
          imagePullPolicy: IfNotPresent

2) 자동 미러링(지속 운영)

운영에서는 CI에서 다음을 자동화하는 방식이 일반적입니다.

  • upstream 이미지 digest 고정(nginx@sha256:...)
  • 정기적으로 upstream 업데이트 감지
  • ECR에 동일 digest로 push
  • 쿠버네티스는 ECR만 바라보게 고정

이렇게 하면 외부 레지스트리 rate limit, 네트워크 변동, 인증 정책 변경의 영향을 크게 줄일 수 있습니다.

해법 2: imagePullPolicy와 태그 전략 재점검

imagePullPolicy: Always는 편하지만, 대규모 롤아웃에서 외부 레지스트리에 불필요한 부하를 줍니다.

  • 개발 환경: Always가 유용할 수 있음
  • 운영 환경: **버전 태그/다이제스트 고정 + IfNotPresent**가 안정적

권장 패턴:

  • 태그를 latest 대신 1.2.3 같은 불변 버전으로
  • 더 강하게는 digest pinning
containers:
  - name: app
    image: <account>.dkr.ecr.ap-northeast-2.amazonaws.com/app@sha256:0123abcd...
    imagePullPolicy: IfNotPresent

digest를 쓰면 “태그는 같지만 내용이 바뀌어서 다시 받아야 하는” 혼란이 사라져, pull 트래픽도 예측 가능해집니다.

해법 3: 프리풀(Pre-pull)로 스케일아웃 폭주 완화

노드가 새로 뜰 때마다 동일 이미지를 동시에 당기면 429가 터집니다. 이를 줄이는 전형적인 방법이 DaemonSet 프리풀입니다.

DaemonSet을 이용한 프리풀 예시

아래는 각 노드에서 이미지를 미리 pull만 하고 종료하지 않도록(혹은 sleep) 유지하는 패턴입니다.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: prepull-core-images
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: prepull
  template:
    metadata:
      labels:
        app: prepull
    spec:
      tolerations:
        - operator: Exists
      containers:
        - name: prepull
          image: <account>.dkr.ecr.ap-northeast-2.amazonaws.com/mirror/nginx:1.25
          command: ["/bin/sh", "-c", "echo prepulled && sleep 3600"]
          imagePullPolicy: IfNotPresent

포인트:

  • 실제 서비스 이미지(또는 공통 베이스 이미지)를 대상으로 적용
  • 노드 그룹별(온디맨드/스팟/워크로드별)로 라벨 셀렉터를 걸어 범위를 제한
  • 프리풀 DS 자체가 또 다른 대량 pull을 만들 수 있으니, 미러링(ECR)과 같이 적용하는 것이 안전

해법 4: 롤아웃 동시성 줄이기(서지/버짓)

429는 “한 번에 너무 많이”가 본질입니다. 쿠버네티스 배포 전략으로 동시성을 낮추면 효과가 큽니다.

Deployment rollingUpdate 튜닝

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

PodDisruptionBudget로 급격한 축출 방지

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
spec:
  minAvailable: 90%
  selector:
    matchLabels:
      app: web

PDB는 직접적으로 pull을 줄이진 않지만, 노드 드레인/업그레이드/중단 상황에서 한꺼번에 Pod가 내려갔다가 다시 올라오며 pull 폭주하는 패턴을 완화합니다.

해법 5: 레지스트리 인증으로 rate limit 상향(Docker Hub/GHCR)

외부 레지스트리를 계속 써야 한다면, 인증을 통해 rate limit을 완화할 수 있습니다.

imagePullSecret 생성(Docker Hub 예)

kubectl create secret docker-registry dockerhub-creds \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username='<username>' \
  --docker-password='<token-or-password>' \
  --docker-email='<email>' \
  -n <ns>

Deployment에 적용:

spec:
  template:
    spec:
      imagePullSecrets:
        - name: dockerhub-creds

주의할 점:

  • 토큰 만료/회전 정책이 있으면 pull 실패가 401/403으로 바뀌어 나타납니다.
  • 조직 단위로는 External Secrets/Secrets Manager 연동으로 회전을 자동화하는 편이 안전합니다.

해법 6: 노드/런타임 레벨에서 pull 동시성 제한(고급)

컨테이너 런타임(containerd)은 내부적으로 병렬 다운로드를 수행합니다. 극단적인 경우 노드 단에서 동시성을 제한하거나, 프록시 레지스트리를 두어 캐시를 공유하는 방식이 필요할 수 있습니다.

  • 노드 AMI 커스터마이징(운영 난이도 상승)
  • Harbor/Artifactory/Nexus 같은 프록시 레지스트리 캐시
  • ECR Pull Through Cache(가능한 경우)로 upstream 캐시

이 영역은 “가장 강력하지만 운영 복잡도가 커지는” 카드이므로, 보통은 ECR 미러링 + 프리풀 + 롤아웃 동시성 제어 조합으로 먼저 해결한 뒤 필요할 때만 확장합니다.

체크리스트: 현장에서 가장 많이 놓치는 것들

  • imagePullPolicy: Always + latest 조합이 운영에 남아 있지 않은가?
  • 사이드카/InitContainer가 Docker Hub 이미지를 당기고 있지 않은가?
  • 노드가 자주 교체(Spot, 노드그룹 업데이트)되며 캐시가 계속 날아가고 있지 않은가?
  • HPA/배포가 동시에 걸리면서 순간 Pod 수가 폭증하지 않는가?
  • 외부 레지스트리 인증 토큰이 만료되어 재시도가 폭증하고 있지 않은가?

빠른 결론(추천 조합)

  • 가장 확실한 해결: 외부 이미지를 ECR로 미러링하고, 운영은 ECR만 바라보게 변경
  • 스케일아웃 안정화: 핵심 이미지는 DaemonSet 프리풀로 새 노드에서의 초기 폭주를 완화
  • 재발 방지: IfNotPresent + 버전 태그/다이제스트 고정 + 롤아웃 동시성 제한

429 기반 ImagePullBackOff는 “레지스트리 문제”처럼 보이지만, 실제로는 배포/스케일링/캐시 설계 문제인 경우가 많습니다. 위 조합으로 한 번 구조를 잡아두면, 노드가 10배 늘어도 이미지 pull에서 발목 잡힐 일이 크게 줄어듭니다.