Published on

EKS ImagePullBackOff 403 - ECR 권한·토큰 만료 해결

Authors

서론

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 Forbidden
    • denied: User ... is not authorized to perform: ecr:BatchGetImage
  • 인증 토큰/자격증명형
    • no basic auth credentials
    • failed to authorize: rpc error ... failed to fetch anonymous token: 403
    • pull 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:GetAuthorizationToken
  • ecr:BatchCheckLayerAvailability
  • ecr:GetDownloadUrlForLayer
  • ecr: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)에서는 두 군데가 맞아야 합니다.

  1. EKS 노드 Role(또는 Fargate role)에 ECR 접근 권한
  2. 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.api
  • com.amazonaws.<region>.ecr.dkr
  • com.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이 뜰 때 핵심은 다음 두 질문입니다.

  1. 이미지를 pull하는 주체가 누구인가? (대부분 노드 Role)
  2. ECR 인증 토큰을 Secret으로 고정해두지 않았는가? (12시간 만료)

이 두 가지만 명확히 분리하면, 문제의 80%는 빠르게 해결됩니다. 그 다음은 교차 계정 리포지토리 정책, VPC 엔드포인트(ecr.api/ecr.dkr/s3), DNS/네트워크 순으로 좁혀가면 됩니다.

추가로, EKS에서 인증/네트워크가 섞여 보이는 장애를 계층적으로 분해하는 방법은 EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS도 함께 읽어보면 진단 속도가 더 빨라집니다.