- Published on
EKS ImagePullBackOff 403 - ECR 권한·토큰 만료 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
EKS에서 Pod가 ImagePullBackOff로 멈추고, 이벤트에는 403 Forbidden(혹은 pull access denied, failed to fetch oauth token) 같은 메시지가 찍히는 경우가 있습니다. 이 증상은 겉보기엔 “이미지가 없나?”로 보이지만, 실제로는 ECR 인증 토큰(12시간) 문제 또는 ECR에 접근할 IAM 권한 문제인 경우가 대부분입니다. 특히 EKS에서는 이미지 풀 주체가 “Pod”가 아니라 노드의 kubelet(컨테이너 런타임) 이라는 점을 놓치면 계속 삽질하게 됩니다.
이 글은 다음을 목표로 합니다.
ImagePullBackOff + 403을 이벤트/로그로 정확히 분류하기 n- ECR 권한 주체가 노드 IAM Role인지, IRSA인지 구분하기- 토큰 만료/미갱신(레지스트리 인증)과 권한 부족(정책/리소스) 문제를 재현 가능한 체크리스트로 해결하기
관련해서 네트워크/인증이 엮인 EKS 장애 진단 흐름은 EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS 글의 접근법(증상→계층 분리→원인 좁히기)도 같이 참고하면 좋습니다.
1) 먼저: 진짜 403인지, 어디에서 403인지 확인
ImagePullBackOff는 결과(백오프)일 뿐이고, 원인은 이벤트에 있습니다.
kubectl -n <ns> describe pod <pod>
Events: 섹션에서 다음 패턴을 찾습니다.
- 권한 부족형(정책/리소스)
failed to pull image ...: rpc error: code = Unknown desc = failed to resolve reference ...: unexpected status from HEAD request ... 403 Forbiddendenied: User ... is not authorized to perform: ecr:BatchGetImage
- 인증 토큰/자격증명형
no basic auth credentialsfailed to authorize: rpc error ... failed to fetch anonymous token: 403pull access denied ... may require 'docker login'
여기서 중요한 포인트:
- ECR은 Docker Registry API를 쓰기 때문에, kubelet/컨테이너 런타임이 ECR에서 토큰을 받아오는 단계(GetAuthorizationToken)와 이미지 레이어를 가져오는 단계(BatchGetImage/GetDownloadUrlForLayer)가 분리됩니다.
- 403이 뜬다고 해서 무조건
ecr:*가 다 필요한 게 아니라, 어떤 API에서 막혔는지가 관건입니다.
2) 이미지 풀 주체는 누구인가: 노드 IAM Role vs IRSA
EKS에서 기본적으로 이미지를 pull하는 주체는 노드(EC2)의 IAM Role입니다.
- Managed Node Group / Self-managed Node: 노드 인스턴스 프로파일의 Role이 ECR에 접근
- Fargate: Fargate Pod Execution Role이 ECR에 접근
- IRSA: 원칙적으로 “애플리케이션이 AWS API 호출”할 때 쓰는 방식이며, 이미지 pull 자체는 대개 노드가 수행합니다.
예외/혼동 포인트:
imagePullSecrets를 써서 프라이빗 레지스트리 인증을 Pod 단위로 주입할 수는 있지만, ECR은 보통 그렇게 운영하지 않습니다(토큰 12시간 만료로 운영 부담 증가).- “IRSA 붙였는데도 403”은 흔히 이미지 풀은 IRSA가 아니라 노드 IAM Role이어서 발생합니다.
노드 IAM Role 확인
kubectl -n kube-system get cm aws-auth -o yaml
여기서 mapRoles에 노드 Role이 매핑되어 있는지 확인합니다(클러스터 조인 관점). ECR 권한은 이 Role(인스턴스 프로파일)에 붙어 있어야 합니다.
노드에서 직접 확인하는 방법(SSM 또는 SSH 가능할 때):
# 노드에서
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# 출력된 role 이름으로
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
3) ECR 403의 대표 원인 6가지(우선순위)
원인 A) 노드 Role에 ECR 권한이 없다 (가장 흔함)
노드 Role(또는 Fargate execution role)에 최소 아래 권한이 필요합니다.
ecr:GetAuthorizationTokenecr:BatchCheckLayerAvailabilityecr:GetDownloadUrlForLayerecr:BatchGetImage
대부분은 AWS 관리 정책 AmazonEC2ContainerRegistryReadOnly를 붙이면 해결됩니다.
aws iam attach-role-policy \
--role-name <node-role-name> \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
하지만 “정책 붙였는데도 403”이면 아래를 추가로 의심합니다.
원인 B) ECR Repository Policy가 계정/Role을 막고 있다(교차 계정에서 특히)
교차 계정 ECR(예: Account A의 EKS가 Account B의 ECR 이미지 pull)에서는 두 군데가 맞아야 합니다.
- EKS 노드 Role(또는 Fargate role)에 ECR 접근 권한
- ECR 리포지토리 정책에서 해당 Role/계정을 허용
리포지토리 정책 예시(간단형):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPullFromOtherAccount",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::<EKS_ACCOUNT_ID>:root"},
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchCheckLayerAvailability"
]
}
]
}
주의: GetAuthorizationToken은 리포지토리 정책이 아니라 ECR(레지스트리) 레벨 성격이라, 호출 주체 IAM에 있어야 합니다.
원인 C) ECR 토큰(12시간) 만료 + imagePullSecrets로 고정해둠
ECR은 aws ecr get-login-password로 얻는 토큰이 12시간 유효합니다. 이를 Kubernetes docker-registry 타입 Secret으로 만들어 imagePullSecrets에 고정하면, 12시간 후부터 새로 뜨는 Pod가 403/인증 실패로 무너질 수 있습니다.
문제 패턴:
- 기존에 떠 있던 Pod는 정상(이미지 캐시)
- 새로 스케줄된 Pod만
ImagePullBackOff - 노드 교체/스케일아웃 시 대량 장애
해결 방향:
- ECR은 노드 IAM Role 기반 pull로 전환(권장)
- 부득이하게 Secret을 써야 한다면, 주기적으로 Secret을 갱신하는 CronJob 운영
Secret 갱신 CronJob 예시(개념 샘플):
apiVersion: batch/v1
kind: CronJob
metadata:
name: refresh-ecr-secret
namespace: kube-system
spec:
schedule: "0 */6 * * *" # 6시간마다
jobTemplate:
spec:
template:
spec:
serviceAccountName: ecr-secret-refresher
restartPolicy: OnFailure
containers:
- name: refresh
image: public.ecr.aws/aws-cli/aws-cli:2.15.0
env:
- name: AWS_REGION
value: ap-northeast-2
command:
- /bin/sh
- -c
- |
PASS=$(aws ecr get-login-password --region $AWS_REGION)
kubectl -n <ns> create secret docker-registry ecr-pull \
--docker-server=<account>.dkr.ecr.$AWS_REGION.amazonaws.com \
--docker-username=AWS \
--docker-password="$PASS" \
--dry-run=client -o yaml | kubectl apply -f -
실무 팁: 이 방식은 운영 복잡도가 올라가므로, 가능하면 노드 Role로 해결하세요.
원인 D) 리전/레지스트리 주소 불일치
이미지 URL이 클러스터 리전과 다르거나 오타가 있으면, 권한이 있어도 403/401처럼 보일 수 있습니다.
- 올바른 형식:
<account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>
확인:
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.containers[0].image}'; echo
원인 E) VPC 엔드포인트/프록시 환경에서 ECR API/DOCKER 엔드포인트 일부만 열림
프라이빗 서브넷에서 NAT 없이 ECR을 쓰려면 보통 아래 VPC 엔드포인트가 필요합니다.
com.amazonaws.<region>.ecr.apicom.amazonaws.<region>.ecr.dkrcom.amazonaws.<region>.s3(레이어 다운로드가 S3를 경유할 수 있음)
보안그룹/엔드포인트 정책이 미묘하게 막히면 403/timeout이 섞여 나옵니다. 이때는 네트워크 레이어(엔드포인트 정책, SG, NACL)와 DNS(CoreDNS)까지 같이 봐야 합니다. 네트워크/인증 이슈를 단계적으로 분리하는 방식은 EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS에서 다룬 흐름과 동일합니다.
원인 F) 노드 시간이 틀어져 서명/토큰 검증 실패
드물지만 NTP 이슈로 시간이 크게 틀어지면 AWS 서명 검증이 실패해 인증 문제가 발생할 수 있습니다. 노드에서 timedatectl로 확인하고, chrony 설정을 점검합니다.
4) 재현 가능한 진단 루틴(실전 체크리스트)
4.1 Pod 이벤트로 “인증 vs 권한 vs 네트워크” 1차 분류
kubectl -n <ns> describe pod <pod> | sed -n '/Events/,$p'
no basic auth credentials→ 토큰/자격증명 경로is not authorized to perform: ecr:*→ IAM/리포지토리 정책i/o timeout,TLS handshake timeout→ 네트워크/DNS/프록시
4.2 노드 Role에 최소 ECR 권한이 있는지 시뮬레이션
AWS CLI로 정책 시뮬레이터를 돌리면 “진짜로 허용되는지”를 빠르게 확인할 수 있습니다.
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::<acct>:role/<node-role> \
--action-names ecr:GetAuthorizationToken ecr:BatchGetImage ecr:GetDownloadUrlForLayer \
--resource-arns "*"
교차 계정이면 리포지토리 ARN을 넣고 ecr:BatchGetImage 등을 대상으로 확인합니다.
4.3 (가능하면) 문제 노드에서 ECR 로그인/풀 테스트
노드에 접속 가능할 때 가장 확실합니다.
- containerd 환경에서
crictl을 쓸 수 있으면:
# 노드에서
aws ecr get-login-password --region <region> | \
ctr -n k8s.io images pull --user AWS:$(cat) <acct>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>
환경마다 도구가 다르니(ctr, crictl, docker) 명령은 조정하되, 핵심은 노드가 같은 경로로 ECR에 접근 가능한지입니다.
4.4 imagePullSecrets 사용 여부 확인(토큰 만료 의심)
kubectl -n <ns> get sa <serviceaccount> -o yaml | yq '.imagePullSecrets'
kubectl -n <ns> get pod <pod> -o yaml | yq '.spec.imagePullSecrets'
여기서 ECR용 Secret이 보이면 만료 루프 가능성이 큽니다. 특히 “어제부터 갑자기” 류의 장애는 이 케이스가 많습니다.
5) 권장 해결책(운영 안정성 기준)
해결책 1) 노드 IAM Role에 ECR ReadOnly 부여(표준)
- Managed Node Group이면 노드 Role에
AmazonEC2ContainerRegistryReadOnly를 붙입니다. - 커스텀 정책을 쓴다면 앞서 언급한 4개 액션을 포함하세요.
장점: 토큰 갱신을 쿠버네티스가 신경 쓸 필요가 없습니다.
해결책 2) 교차 계정이면 “노드 Role 권한 + ECR repo policy”를 세트로 맞추기
- EKS 쪽: 노드 Role이 ECR API 호출 가능
- ECR 쪽: 리포지토리 정책이 pull을 허용
둘 중 하나만 맞추면 403이 계속 납니다.
해결책 3) imagePullSecrets를 쓰고 있다면 제거하거나 자동 갱신
- 가능하면 제거하고 노드 Role로 전환
- 불가능하면 CronJob으로 6~11시간 주기로 갱신(12시간 만료 전에)
운영 관점에서 “토큰 만료로 새 배포가 멈추는” 상황은 장애 파급이 크므로, 이 부분을 가장 먼저 정리하는 것을 추천합니다.
6) 마무리: 403은 ‘권한’만이 아니라 ‘인증 갱신’ 문제일 수 있다
EKS에서 ImagePullBackOff와 함께 403이 뜰 때 핵심은 다음 두 질문입니다.
- 이미지를 pull하는 주체가 누구인가? (대부분 노드 Role)
- ECR 인증 토큰을 Secret으로 고정해두지 않았는가? (12시간 만료)
이 두 가지만 명확히 분리하면, 문제의 80%는 빠르게 해결됩니다. 그 다음은 교차 계정 리포지토리 정책, VPC 엔드포인트(ecr.api/ecr.dkr/s3), DNS/네트워크 순으로 좁혀가면 됩니다.
추가로, EKS에서 인증/네트워크가 섞여 보이는 장애를 계층적으로 분해하는 방법은 EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS도 함께 읽어보면 진단 속도가 더 빨라집니다.