Published on

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

Authors

서버가 멀쩡한데도 배포만 하면 Pod가 ImagePullBackOff로 멈추는 상황은 운영에서 꽤 흔합니다. 특히 AWS ECR을 쓰는 Kubernetes(EKS 포함)에서는 인증 토큰이 12시간마다 만료되고, 노드/파드의 IAM 권한 경계가 꼬이거나, imagePullSecrets가 갱신되지 않으면서 장애가 반복됩니다.

이 글은 “왜 ECR에서만 유독 자주 터지나”를 이벤트/로그로 확인하는 방법부터, 토큰 만료를 구조적으로 제거하는 설계까지 단계적으로 정리합니다.

관련해서 ImagePullBackOff의 더 넓은 원인(네트워크, DNS, 레지스트리, 이미지 태그 등)을 같이 보고 싶다면 아래 글도 참고하세요.

증상: 이벤트에서 ECR 인증 실패가 보이는 패턴

먼저 원인을 “추측”하지 말고 이벤트를 봅니다.

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

Events에서 흔히 보이는 메시지들:

  • Failed to pull image ... / ErrImagePull
  • rpc error: code = Unknown desc = failed to resolve reference ...
  • no basic auth credentials
  • pull access denied
  • denied: Your authorization token has expired

추가로 노드 런타임 로그도 힌트가 됩니다(환경에 따라 경로/명령이 다릅니다).

# containerd 기반 노드에서 자주 확인
sudo journalctl -u containerd --since "30 min ago" | tail -n 200

# kubelet 이벤트도 같이
sudo journalctl -u kubelet --since "30 min ago" | tail -n 200

여기서 token has expired 또는 no basic auth credentials가 보이면, 거의 항상 아래 범주 중 하나입니다.

ECR에서 ImagePullBackOff가 자주 발생하는 이유(핵심 메커니즘)

ECR은 Docker Registry API를 제공하지만, 인증이 “영구 패스워드”가 아니라 임시 인증 토큰 기반입니다.

  • aws ecr get-login-password로 받는 값은 유효기간 12시간
  • Kubernetes의 imagePullSecrets(도커 레지스트리 시크릿)는 기본적으로 자동 갱신되지 않음
  • 노드가 ECR을 직접 풀링하는 경우(대부분) 노드 IAM Role이 필요
  • 파드가 직접 ECR API를 호출하도록 구성하는 경우(특수 케이스) IRSA 또는 별도 자격증명 필요

즉, “한 번 로그인해두면 계속 된다”는 기대가 깨지는 구조입니다.

1단계: 지금 당장 원인 확정하기(3가지 체크)

1) 이미지 레퍼런스가 ECR인지, 리전이 맞는지

ECR 포맷은 보통 아래 형태입니다.

  • ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPO:TAG

리전이 틀리면 토큰/엔드포인트가 맞지 않아 인증 실패로 보일 수 있습니다.

kubectl get pod -n <namespace> <pod-name> -o jsonpath='{.spec.containers[0].image}'

2) 노드가 ECR에 접근 가능한 IAM 권한을 갖는지

노드 역할(EC2 instance profile 또는 EKS managed node group role)에 아래 권한이 필요합니다.

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

권한이 없는 경우 이벤트는 보통 no basic auth credentials 또는 pull access denied처럼 뜹니다.

3) imagePullSecrets를 쓰고 있다면 “만료된 값”인지

레거시로 docker-registry 시크릿을 만들어 쓰는 클러스터에서 특히 잦습니다.

kubectl get secret -n <namespace>

kubectl get secret -n <namespace> <secret-name> -o yaml

.dockerconfigjson이 오래된 값이면 12시간 이후부터 실패합니다.

2단계: 가장 흔한 해결책 3가지(상황별)

아래는 “정답이 하나”가 아니라 클러스터 운영 형태에 따라 선택이 달라집니다.

해결책 A: EKS라면 기본 권장 — 노드 IAM Role로 ECR 풀링

EKS에서는 일반적으로 노드가 이미지 풀링을 담당합니다. 이때는 imagePullSecrets 없이도 노드 역할에 ECR 권한만 있으면 안정적입니다.

체크 포인트:

  • 노드 그룹 IAM Role에 ECR 읽기 권한이 있는지
  • 노드가 프라이빗 서브넷이면 NAT 또는 VPC 엔드포인트로 ECR 접근이 가능한지

권장 정책 예시(최소 권한 형태로 커스텀 권장):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage"
      ],
      "Resource": "*"
    }
  ]
}

운영 팁:

  • 노드 역할이 바뀌었는데 기존 노드가 살아있으면, 새 권한이 반영되지 않아 “어떤 노드에서는 되고 어떤 노드에서는 안 되는” 현상이 납니다. 이 경우 문제 노드 드레인 후 교체가 빠른 해결입니다.
kubectl cordon <node>
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data

해결책 B: imagePullSecrets를 계속 써야 한다면 — 자동 갱신 CronJob

보안/정책/멀티클러스터 이유로 imagePullSecrets를 유지해야 한다면, 핵심은 “12시간 만료”를 운영적으로 흡수하는 자동화입니다.

아래는 kube-system(또는 전용 네임스페이스)에 CronJob을 두고, ECR 토큰을 주기적으로 갱신해 docker-registry 시크릿을 업데이트하는 패턴입니다.

전제:

  • CronJob이 실행되는 ServiceAccount가 kubectl로 시크릿을 업데이트할 RBAC 권한이 있어야 함
  • CronJob 파드가 AWS API를 호출할 수 있어야 함(노드 IAM 또는 IRSA)

CronJob 예시(개념용, 환경에 맞게 수정):

apiVersion: batch/v1
kind: CronJob
metadata:
  name: ecr-secret-refresh
  namespace: kube-system
spec:
  schedule: "0 */6 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: ecr-secret-refresh-sa
          restartPolicy: OnFailure
          containers:
            - name: refresh
              image: public.ecr.aws/aws-cli/aws-cli:2.15.0
              env:
                - name: AWS_REGION
                  value: ap-northeast-2
                - name: ECR_REGISTRY
                  value: "ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com"
                - name: SECRET_NAME
                  value: ecr-pull-secret
                - name: SECRET_NAMESPACE
                  value: default
              command:
                - /bin/sh
                - -c
                - |
                  set -e
                  PASS=$(aws ecr get-login-password --region "$AWS_REGION")
                  kubectl -n "$SECRET_NAMESPACE" create secret docker-registry "$SECRET_NAME" \
                    --docker-server="$ECR_REGISTRY" \
                    --docker-username=AWS \
                    --docker-password="$PASS" \
                    --dry-run=client -o yaml | kubectl apply -f -

파드에서 시크릿을 쓰는 쪽은 아래처럼 지정합니다.

spec:
  imagePullSecrets:
    - name: ecr-pull-secret

운영 팁:

  • 스케줄은 6시간 또는 8시간 등으로 잡아 만료 전에 갱신하세요.
  • 여러 네임스페이스에서 쓰면, 네임스페이스별로 시크릿을 만들거나 ServiceAccount에 붙여 표준화하는 편이 관리가 쉽습니다.

해결책 C: 프라이빗 서브넷에서 자주 터진다면 — VPC 엔드포인트 점검

ECR은 단순히 한 엔드포인트만 쓰지 않습니다.

  • API 호출용 ecr.api
  • 이미지 레이어 다운로드용 ecr.dkr
  • 그리고 실제 레이어는 S3를 통해 내려오는 경우가 많아 S3 경로도 영향

NAT가 불안정하거나 비용 절감으로 NAT를 제거했다면, 아래 VPC 엔드포인트 구성이 필요할 수 있습니다.

  • Interface endpoint: com.amazonaws.REGION.ecr.api
  • Interface endpoint: com.amazonaws.REGION.ecr.dkr
  • Gateway endpoint: com.amazonaws.REGION.s3

이 경우 이벤트는 인증 만료처럼 보이기보다는 i/o timeout, connection reset, TLS handshake timeout 등 네트워크 오류로 섞여 나옵니다.

3단계: “토큰 만료”를 장애로 만들지 않는 운영 설계

1) 가능하면 imagePullSecrets 의존을 줄이기

EKS에서 표준 패턴은 “노드 역할로 ECR 풀링”입니다. imagePullSecrets는 다음 경우에만 고려하는 편이 안전합니다.

  • 노드 역할에 레지스트리 접근 권한을 줄 수 없는 조직 정책
  • 멀티 레지스트리(외부 프라이빗 레지스트리) 혼용
  • 특정 네임스페이스/워크로드만 별도 자격증명이 필요한 경우

2) 이미지 태그 전략: latest 지양, 재현 가능한 태그 사용

latest는 캐시/풀 정책과 결합되면 문제를 더 찾기 어렵게 만듭니다.

  • 동일 태그인데 노드마다 다른 레이어를 보고 있을 수 있음
  • 재배포 시점에만 터지는 것처럼 보일 수 있음

권장:

  • Git SHA 태그 또는 빌드 번호 태그
  • 필요하면 imagePullPolicy: Always를 명시하되, 네트워크/레지스트리 안정성이 전제
containers:
  - name: app
    image: ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:git-1a2b3c4d
    imagePullPolicy: IfNotPresent

3) 노드 교체/스케일링 이벤트와 겹칠 때를 대비

토큰 만료/권한 문제는 새 노드가 붙는 순간(스케일아웃/롤링업데이트) 폭발하는 경우가 많습니다.

  • 신규 노드 역할에 정책이 누락
  • Launch template 변경이 일부 노드에만 반영

따라서 “노드 그룹 변경 후 즉시 간단한 테스트 파드로 ECR 풀링 확인”을 습관화하면 좋습니다.

kubectl run ecr-pull-test \
  --image=ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:git-1a2b3c4d \
  --restart=Never

kubectl describe pod ecr-pull-test

실전 트러블슈팅 체크리스트(빠른 정리)

  • 이벤트에 token has expired가 보이면
    • imagePullSecrets 자동 갱신이 없는지 확인
    • 가능하면 노드 IAM Role 기반으로 전환
  • 이벤트에 no basic auth credentials가 보이면
    • 노드 IAM Role에 ecr:GetAuthorizationToken 포함 여부
    • 레지스트리 주소/리전 오타
  • 이벤트에 i/o timeout 등이 보이면
    • NAT/VPC 엔드포인트(ecr.api, ecr.dkr, s3) 점검
  • “어떤 노드는 되고 어떤 노드는 안 됨”이면
    • 노드 역할/런치 템플릿 변경이 혼재
    • 문제 노드 드레인 후 교체

마무리

ImagePullBackOff 자체는 증상이고, ECR 환경에서는 특히 토큰 12시간 만료노드 IAM 권한/네트워크 경로가 핵심 원인으로 반복됩니다. 가장 안정적인 방향은 “노드가 ECR을 풀링할 수 있는 권한과 네트워크를 갖추고, 불필요한 imagePullSecrets 의존을 줄이는 것”입니다.

다만 조직 정책상 시크릿 기반 인증을 유지해야 한다면, CronJob으로 만료 전에 갱신하는 자동화를 넣어 장애를 구조적으로 제거하세요.

추가 원인까지 폭넓게 점검하려면 아래 글에서 체크리스트를 확장해 보시면 좋습니다.