Published on

EKS ImagePullBackOff - ECR 인증·토큰 만료 해결

Authors

서버리스가 아닌 이상, 쿠버네티스 장애의 상당수는 결국 권한네트워크로 수렴합니다. EKS에서 ImagePullBackOff가 뜨면 특히 AWS ECR 인증 토큰(12시간 유효)과 IAM 권한, 그리고 노드 또는 파드가 어떤 자격증명 경로를 타는지부터 의심해야 합니다.

이 글은 EKS + ECR 조합에서 이미지 풀 실패를 재현 가능한 방식으로 진단하고, 토큰 만료 및 인증 체인을 안정화하는 방법을 단계별로 정리합니다.

관련해서 IRSA 자체가 흔들리는 케이스도 자주 엮입니다. IRSA 타임아웃 문제는 별도 글인 EKS IRSA에서 AssumeRoleWithWebIdentity 0s 타임아웃 해결도 함께 참고하면 좋습니다.

증상: ImagePullBackOff에서 무엇을 먼저 봐야 하나

ImagePullBackOff는 “이미지 풀에 실패했고, kubelet이 재시도 백오프에 들어갔다”는 결과 상태입니다. 원인은 이벤트에 거의 다 드러납니다.

가장 먼저 아래를 확인합니다.

kubectl describe pod -n <namespace> <pod-name>

MDX 빌드 에러 방지를 위해 위의 <namespace>, <pod-name> 같은 꺾쇠 표기는 반드시 코드로 감쌌습니다.

Events 섹션에서 자주 보이는 메시지 패턴은 다음과 같습니다.

  • failed to authorize: rpc error: code = Unknown desc = failed to fetch anonymous token
  • no basic auth credentials
  • pull access denied
  • denied: User is not authorized to perform: ecr:GetDownloadUrlForLayer
  • i/o timeout 또는 TLS handshake timeout

이 글은 그중에서도 ECR 인증·토큰 만료 계열을 중심으로 다룹니다. 네트워크나 DNS가 의심되면 CNI/노드 상태도 같이 봐야 하며, 노드 컨디션이 흔들릴 때는 K8s NodeNotReady - CNI 플러그인 장애 복구 가이드도 도움이 됩니다.

ECR 인증은 누가, 어디서, 어떻게 하나

핵심은 “이미지를 당기는 주체는 파드가 아니라 노드의 kubelet”이라는 점입니다.

  • 기본 동작: 노드(EC2 워커)가 자신의 IAM Role(인스턴스 프로파일)로 ECR에 접근해 이미지를 pull
  • 예외 동작: imagePullSecrets를 지정하면 kubelet이 해당 도커 레지스트리 크리덴셜을 사용
  • 오해 포인트: IRSA는 주로 파드 내부에서 AWS API를 호출할 때 쓰이며, 이미지 pull 단계에서는 기본적으로 적용되지 않습니다

즉, ECR pull 권한 문제는 대부분 노드 IAM Role 또는 imagePullSecrets 구성 문제입니다.

원인 1: 노드 IAM Role에 ECR Pull 권한이 없음

노드 역할에 다음 권한이 필요합니다.

  • ecr:GetAuthorizationToken
  • ecr:BatchCheckLayerAvailability
  • ecr:GetDownloadUrlForLayer
  • ecr:BatchGetImage

관리형 정책으로는 보통 AmazonEC2ContainerRegistryReadOnly가 사용됩니다.

노드 역할 확인

kubectl get nodes -o wide

노드가 어떤 ASG/노드그룹인지 확인한 뒤, AWS 콘솔 또는 CLI로 노드 IAM Role을 확인합니다.

예시로 노드그룹을 CLI로 확인하면:

aws eks describe-nodegroup \
  --cluster-name <cluster> \
  --nodegroup-name <nodegroup> \
  --query 'nodegroup.nodeRole'

그 역할에 AmazonEC2ContainerRegistryReadOnly가 붙어 있는지 확인합니다.

aws iam list-attached-role-policies --role-name <node-role-name>

해결

aws iam attach-role-policy \
  --role-name <node-role-name> \
  --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly

권한을 붙인 뒤에도 기존 실패 파드가 계속 백오프에 있을 수 있으니, 파드를 재생성하거나 rollout을 트리거합니다.

kubectl rollout restart deploy -n <namespace> <deployment>

원인 2: ECR 토큰(Authorization Token) 만료

ECR 로그인 토큰은 기본적으로 12시간 유효합니다. 이 토큰 만료로 ImagePullBackOff가 반복되는 케이스는 보통 아래 조건 중 하나를 만족합니다.

  • 사람이 수동으로 imagePullSecrets를 만들어서 장기간 유지하려고 함
  • 외부 CI가 만든 도커 config를 그대로 시크릿으로 넣고 갱신을 안 함
  • 노드가 정상적인 IAM 경로로 ECR 토큰을 못 받아오고, 오래된 크리덴셜에 의존하게 됨

중요한 결론은 간단합니다.

  • 가능하면 imagePullSecrets로 ECR 토큰을 고정하지 말고, 노드 IAM Role 기반으로 pull 하게 구성
  • 꼭 시크릿을 써야 한다면, 토큰 갱신 자동화를 반드시 넣기

(권장) 시크릿 없이 노드 IAM로만 풀게 만들기

가장 안정적인 방식입니다.

  • imagePullSecrets 제거
  • 노드 IAM Role에 ECR read 권한 부여
  • 프라이빗 ECR을 쓰더라도 동일

디플로이먼트에서 imagePullSecrets가 있는지 확인:

kubectl get deploy -n <namespace> <deployment> -o yaml | sed -n '1,200p'

있다면 제거 후 적용합니다.

(불가피) ECR imagePullSecrets 자동 갱신

아래는 ECR 토큰을 주기적으로 갱신하는 크론잡 예시입니다. 아이디어는 다음과 같습니다.

  1. aws ecr get-login-password로 토큰을 얻고
  2. kubectl create secret docker-registry ... --dry-run=client -o yaml로 시크릿 매니페스트를 만들고
  3. kubectl apply로 갱신

전제 조건:

  • 크론잡 파드가 AWS API를 호출할 수 있어야 하므로 IRSA 또는 노드 권한이 필요
  • kubectl을 실행할 수 있도록 서비스어카운트에 RBAC 권한 필요

RBAC

apiVersion: v1
kind: ServiceAccount
metadata:
  name: ecr-secret-refresher
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ecr-secret-refresher
  namespace: <namespace>
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ecr-secret-refresher
  namespace: <namespace>
subjects:
  - kind: ServiceAccount
    name: ecr-secret-refresher
    namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: ecr-secret-refresher

CronJob

아래 예시는 amazon/aws-cli 이미지를 쓰고, 컨테이너에서 kubectl을 내려받아 실행하는 단순 버전입니다. 운영에서는 보통 kubectl 포함 이미지(예: 내부 유틸 이미지)를 별도로 준비하는 편이 더 안정적입니다.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: ecr-secret-refresher
  namespace: kube-system
spec:
  schedule: "0 */6 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: ecr-secret-refresher
          restartPolicy: OnFailure
          containers:
            - name: refresh
              image: amazon/aws-cli:2.15.57
              env:
                - name: AWS_REGION
                  value: ap-northeast-2
                - name: NAMESPACE
                  value: <namespace>
                - name: SECRET_NAME
                  value: ecr-pull-secret
                - name: ECR_REGISTRY
                  value: <account-id>.dkr.ecr.ap-northeast-2.amazonaws.com
              command:
                - /bin/sh
                - -lc
                - |
                  set -euo pipefail

                  curl -sSL -o /usr/local/bin/kubectl \
                    "https://dl.k8s.io/release/$(curl -sSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
                  chmod +x /usr/local/bin/kubectl

                  PASS="$(aws ecr get-login-password --region "$AWS_REGION")"

                  kubectl create secret docker-registry "$SECRET_NAME" \
                    -n "$NAMESPACE" \
                    --docker-server="$ECR_REGISTRY" \
                    --docker-username=AWS \
                    --docker-password="$PASS" \
                    --dry-run=client -o yaml | kubectl apply -f -

이 방식은 토큰 만료로 인한 no basic auth credentials류 문제를 구조적으로 제거합니다.

원인 3: ECR 리포지토리 정책 또는 KMS 암호화 이슈

권한을 줬는데도 denied가 계속되면 리포지토리 정책을 확인해야 합니다.

  • 리포지토리가 다른 AWS 계정에 있고 cross-account pull을 하는데 리포지토리 정책이 없음
  • ECR이 KMS로 암호화되어 있고, 노드 역할에 KMS decrypt 권한이 없음

리포지토리 정책 확인

aws ecr get-repository-policy \
  --repository-name <repo> \
  --region <region>

cross-account라면 리포지토리 정책에 pull 주체(노드 역할 ARN 또는 계정 루트)에 대한 허용이 필요합니다.

원인 4: 이미지 레퍼런스/태그 문제로 위장된 인증 실패

가끔은 인증이 아니라 단순히 이미지가 존재하지 않거나 태그가 틀린데, 현상은 비슷하게 보입니다.

  • manifest unknown
  • not found

확인:

aws ecr describe-images \
  --repository-name <repo> \
  --image-ids imageTag=<tag> \
  --region <region>

또한 EKS가 있는 리전과 ECR 리전이 다른데, 레지스트리 도메인을 잘못 적는 실수도 흔합니다.

  • 올바른 예: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:1.2.3

원인 5: 시간 드리프트로 STS 서명/토큰 검증 실패

ECR 토큰을 받아오는 과정에서 STS 서명 검증이 실패하면 인증이 실패로 보일 수 있습니다. 특히 노드 또는 파드의 시간이 크게 틀어지면 SignatureDoesNotMatch, RequestExpired가 발생할 수 있습니다.

이 경우는 이미지 pull뿐 아니라 AWS API 전반이 불안정해지므로, 시간 동기화를 먼저 의심해야 합니다. 관련 증상과 해결은 EKS Pod 시간 드리프트로 STS·TLS 실패 해결하기에 정리해두었습니다.

빠른 체크리스트: 10분 안에 좁히는 순서

  1. kubectl describe pod 이벤트에서 에러 문자열을 확보
  2. 노드 IAM Role에 AmazonEC2ContainerRegistryReadOnly 또는 동등 권한이 있는지 확인
  3. imagePullSecrets를 쓰고 있다면 토큰 만료 갱신 전략이 있는지 확인
  4. cross-account 또는 KMS 암호화 ECR이면 리포지토리 정책과 KMS 권한 확인
  5. 리전/레지스트리 도메인/태그 오타 확인
  6. 시간 드리프트, 노드 상태, 네트워크 타임아웃은 별도 트랙으로 병렬 확인

마무리: 재발 방지 설계 포인트

  • 가능하면 ECR pull은 노드 IAM Role 기반으로 단순화하고, imagePullSecrets는 최소화
  • 어쩔 수 없이 시크릿을 쓴다면, 12시간 토큰 만료를 전제로 CronJob 갱신을 표준 운영에 포함
  • 운영 환경에서는 권한 경로(노드 역할, IRSA, 시크릿)를 문서로 고정해 “누가 ECR에 접근하는가”를 팀 합의로 명확히 하기

ImagePullBackOff는 겉으로는 파드 문제처럼 보이지만, 실제로는 클러스터 인증 체인의 설계 문제인 경우가 많습니다. 위 절차대로 “이벤트 문자열로 분류하고, pull 주체를 확정하고, 토큰/권한/정책을 정리”하면 대부분 재발 없이 정리됩니다.