- Published on
EKS에서 ECR ImagePullBackOff 429 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
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가 발생하기 쉽습니다.
- 노드 수 × 파드 수 만큼 pull이 중복될 수 있음
- 같은 이미지라도 노드 로컬 캐시가 없으면 노드마다 다시 받습니다.
- 컨테이너 런타임의 병렬 다운로드
- containerd는 레이어를 병렬로 받는데, 노드가 여러 대면 병렬성이 기하급수적으로 증가합니다.
- 스케일 이벤트가 ‘버스트 트래픽’을 만들기 쉬움
- 오토스케일은 “필요한 순간에 한꺼번에” 늘어납니다.
결국 해결 전략은 간단합니다.
- (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) 체크리스트: 가장 현실적인 우선순위
운영에서 바로 적용하기 좋은 순서로 정리합니다.
imagePullPolicy가Always로 남발되는지 확인 → 가능하면IfNotPresent- 롤링 업데이트
maxSurge축소로 동시 pull 제한 - 노드 프리풀 DaemonSet 도입(스케일 아웃 버스트 제거)
- containerd 동시 다운로드 제한(노드 단위 버스트 완화)
- 네트워크 경로 최적화(VPC 엔드포인트, NAT 병목 제거)
- 이미지 크기/레이어 최적화
마무리
EKS에서 ECR 429로 인한 ImagePullBackOff는 대부분 **"동시 pull 버스트"**가 본질입니다. 따라서 “권한/토큰(403)”과 달리, 해결의 중심은 캐시(프리풀) + 동시성 제어(롤아웃/런타임) + 네트워크 안정화입니다.
특히 오토스케일 환경이라면 프리풀 DaemonSet + 보수적인 롤링 업데이트 설정만으로도 재발률이 크게 떨어집니다. 이후 트래픽/비용 관점에서 VPC 엔드포인트와 이미지 최적화까지 확장하면, 429뿐 아니라 배포 시간과 장애 반경도 함께 줄일 수 있습니다.