Published on

EKS에서 ECR ImagePullBackOff 429 해결법

Authors

EKS에서 배포가 잘 되던 서비스가 갑자기 ImagePullBackOff로 무너지고, 이벤트를 보면 429 Too Many Requests가 찍히는 경우가 있습니다. 특히 오토스케일(Cluster Autoscaler, Karpenter)로 노드가 한꺼번에 늘거나, 롤링 업데이트로 파드가 동시에 재기동될 때 자주 터집니다.

이 글에서는 ECR에서의 429가 왜 생기는지, 그리고 **"당장 살리는 응급처치"**부터 **"구조적으로 429를 줄이는 설계"**까지 단계별로 정리합니다.

아래 내용은 403(권한/토큰)과는 결이 다릅니다. 403이 의심되면 먼저 이 글을 참고하세요: EKS ImagePullBackOff 403 - ECR 권한·토큰 만료 해결

증상 확인: 정말 429인가?

먼저 파드 이벤트에서 429를 확정합니다.

kubectl describe pod <pod> -n <ns>

이벤트에서 보통 아래처럼 보입니다.

  • Failed to pull image ...
  • rpc error: code = Unknown desc = ... 429 Too Many Requests
  • 또는 toomanyrequests 류 메시지

노드 레벨에서도 containerd 로그에 힌트가 있습니다.

# EKS AL2 기준(노드에 SSH/SSM 접속)
sudo journalctl -u containerd -n 200 --no-pager

핵심은 **"동시에 너무 많은 pull"**이 발생했는지입니다. 보통 다음 상황에서 동시 pull이 폭증합니다.

  • 신규 노드가 여러 대 동시에 조인(스케일 아웃)
  • 큰 이미지(수백 MB~수 GB)를 많은 파드가 동시에 pull
  • 같은 이미지를 여러 네임스페이스/워크로드가 동시에 사용
  • imagePullPolicy: Always로 매번 당김

원인 이해: ECR 429가 나는 메커니즘

ECR은 백엔드적으로 레이트 리밋/스로틀링이 존재합니다. 특히 이미지 레이어 다운로드는 다음 특징 때문에 429가 발생하기 쉽습니다.

  1. 노드 수 × 파드 수 만큼 pull이 중복될 수 있음
    • 같은 이미지라도 노드 로컬 캐시가 없으면 노드마다 다시 받습니다.
  2. 컨테이너 런타임의 병렬 다운로드
    • containerd는 레이어를 병렬로 받는데, 노드가 여러 대면 병렬성이 기하급수적으로 증가합니다.
  3. 스케일 이벤트가 ‘버스트 트래픽’을 만들기 쉬움
    • 오토스케일은 “필요한 순간에 한꺼번에” 늘어납니다.

결국 해결 전략은 간단합니다.

  • (A) pull 트래픽을 줄인다: 캐시/프리풀/정책 변경
  • (B) pull 동시성을 낮춘다: 롤아웃 속도 제어, 런타임 설정
  • (C) 네트워크 경로를 안정화한다: NAT/엔드포인트/대역폭

1) 즉시 효과: imagePullPolicy 점검(Always 남발 금지)

가장 먼저 확인할 것은 imagePullPolicy입니다.

  • 태그가 :latest이면 기본값이 Always
  • 태그가 고정 버전(:1.2.3)이면 기본값이 IfNotPresent

가능하면 고정 태그 + IfNotPresent 조합을 권장합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    spec:
      containers:
      - name: api
        image: <account>.dkr.ecr.ap-northeast-2.amazonaws.com/myapi:1.2.3
        imagePullPolicy: IfNotPresent

주의: 보안/컴플라이언스 상 “항상 최신 이미지 강제”가 필요하면, 아래의 프리풀/동시성 제어로 접근하는 게 안전합니다.

2) 구조적 해결 1: 노드 단위 프리풀(Pre-pull)로 버스트 제거

"노드가 새로 생겼을 때"가 가장 위험합니다. 새 노드에 파드가 몰리면서 모든 이미지가 동시에 pull되기 때문입니다. 해결은 노드가 Ready 되기 전에 필요한 이미지를 미리 받아 캐시를 채우는 것입니다.

방법 A: DaemonSet으로 프리풀(가장 흔한 패턴)

아래는 모든 노드에서 특정 이미지를 pull만 하고 종료(혹은 sleep)하는 DaemonSet 예시입니다.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: image-prepull
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: image-prepull
  template:
    metadata:
      labels:
        app: image-prepull
    spec:
      priorityClassName: system-node-critical
      tolerations:
      - operator: Exists
      containers:
      - name: prepull
        image: <account>.dkr.ecr.ap-northeast-2.amazonaws.com/myapi:1.2.3
        imagePullPolicy: Always
        command: ["/bin/sh", "-c"]
        args:
          - "echo 'prepull done'; sleep 3600"
      terminationGracePeriodSeconds: 0
  • imagePullPolicy: Always로 “항상 당겨서 캐시를 채우고”, 실제 워크로드는 IfNotPresent로 캐시를 활용하는 방식이 실전에서 안정적입니다.
  • 여러 이미지를 프리풀해야 한다면 컨테이너를 여러 개 두거나, 별도 레지스트리 프록시를 고려합니다.

방법 B: Karpenter/Autoscaler와 함께 ‘웜업’ 설계

노드가 늘어나는 순간에 곧바로 트래픽 파드를 올리지 말고, 웜업(프리풀) → 서비스 파드 스케줄 순서가 되도록 설계하면 429 빈도가 크게 줄어듭니다.

  • 예: 신규 노드에는 먼저 프리풀 DaemonSet이 뜨고, 서비스 Deployment는 topologySpreadConstraints/podAntiAffinity로 분산

3) 구조적 해결 2: 롤링 업데이트 동시성(서지/언어베일러블) 제한

ECR 429는 “동시에 너무 많이”가 핵심이므로, 배포 동시성을 줄이면 효과가 큽니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  • maxSurge를 크게 잡으면 새 파드가 한꺼번에 생겨 pull이 폭증합니다.
  • maxUnavailable: 0은 가용성에 좋지만, 그만큼 서지가 늘 수 있으니 maxSurge를 보수적으로 잡는 게 중요합니다.

추가로 HPA가 동시에 scale-out을 유발한다면, 배포 시간대에는 HPA 상한을 임시로 낮추거나(운영 정책에 따라) behavior.scaleUp을 완만하게 만드는 것도 방법입니다.

4) 구조적 해결 3: containerd 병렬 다운로드(동시성) 제한

노드 1대에서도 이미지 레이어 다운로드가 병렬로 일어나며, 노드가 여러 대면 곱해집니다. 따라서 런타임 레벨에서 pull 동시성을 제한하면 429가 완화될 수 있습니다.

EKS AMI/OS에 따라 설정 위치가 다르지만, containerd는 대체로 /etc/containerd/config.toml을 사용합니다(없으면 기본값 동작).

예시(환경에 맞게 적용 필요):

# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry]
  max_concurrent_downloads = 3

적용 후 재시작:

sudo systemctl restart containerd

주의:

  • 관리형 노드그룹은 사용자 데이터/부트스트랩로 영속 적용해야 합니다.
  • Bottlerocket은 설정 방식이 다릅니다(설정 API 사용).

5) 네트워크 관점: NAT 병목/비용/불안정도 함께 점검

ECR pull은 네트워크를 많이 씁니다. NAT Gateway를 통해 나가면 다음 문제가 겹칠 수 있습니다.

  • NAT 대역폭/커넥션 병목 → pull 지연 → 재시도 증가 → 429 악화
  • NAT 비용 폭증(특히 대규모 스케일 아웃 + 대용량 이미지)

NAT 비용/트래픽이 의심되면 이 글의 체크리스트가 도움이 됩니다: VPC NAT Gateway 비용 폭증 10분 진단·절감

권장 방향은 다음 중 하나입니다.

  • 프라이빗 서브넷에서 ECR을 VPC 엔드포인트로 접근(인터페이스 엔드포인트)
  • 최소한 NAT를 멀티 AZ로 분산하고, 라우팅/보안그룹/NACL로 병목을 제거

또한 pull이 느려 타임아웃/핸드셰이크 문제가 동반되면 429와 함께 증상이 섞여 보일 수 있습니다. 네트워크 타임아웃 이슈는 다음 글도 참고하세요: EKS TLS handshake timeout 원인·해결 9가지

6) 운영 팁: 재시도(백오프)와 관측 포인트

파드 이벤트/노드 로그로 “버스트”를 수치화

  • 특정 시간대에 Failed to pull image가 급증하는지
  • 신규 노드 조인 직후에만 발생하는지
  • 특정 이미지(특히 큰 이미지)에서만 발생하는지

간단히는 이벤트를 모아보면 패턴이 보입니다.

kubectl get events -A --sort-by=.lastTimestamp | tail -n 50

이미지 자체 최적화도 효과가 큼

  • 멀티스테이지 빌드로 이미지 크기 줄이기
  • 불필요한 레이어 줄이기(레이어 수가 많으면 요청 수 증가)
  • base image를 통일해 노드 캐시 재사용률 올리기

이건 429를 “완전히” 없애진 못해도, 스케일 이벤트 때의 버스트를 크게 줄입니다.

7) 체크리스트: 가장 현실적인 우선순위

운영에서 바로 적용하기 좋은 순서로 정리합니다.

  1. imagePullPolicyAlways로 남발되는지 확인 → 가능하면 IfNotPresent
  2. 롤링 업데이트 maxSurge 축소로 동시 pull 제한
  3. 노드 프리풀 DaemonSet 도입(스케일 아웃 버스트 제거)
  4. containerd 동시 다운로드 제한(노드 단위 버스트 완화)
  5. 네트워크 경로 최적화(VPC 엔드포인트, NAT 병목 제거)
  6. 이미지 크기/레이어 최적화

마무리

EKS에서 ECR 429로 인한 ImagePullBackOff는 대부분 **"동시 pull 버스트"**가 본질입니다. 따라서 “권한/토큰(403)”과 달리, 해결의 중심은 캐시(프리풀) + 동시성 제어(롤아웃/런타임) + 네트워크 안정화입니다.

특히 오토스케일 환경이라면 프리풀 DaemonSet + 보수적인 롤링 업데이트 설정만으로도 재발률이 크게 떨어집니다. 이후 트래픽/비용 관점에서 VPC 엔드포인트와 이미지 최적화까지 확장하면, 429뿐 아니라 배포 시간과 장애 반경도 함께 줄일 수 있습니다.