Published on

Kubernetes ImagePullBackOff 401 - ECR·IRSA·imagePullSecrets

Authors

서버가 잘 뜨다가도 어느 날 갑자기 ImagePullBackOff와 함께 401 Unauthorized가 터지면, 대부분은 ECR 인증(토큰) 경로가 끊겼거나 잘못된 주체가 인증을 시도하고 있는 상황입니다. 문제는 쿠버네티스에서 “이미지 풀”은 Pod가 아니라 노드(kubelet) 가 수행한다는 점, 그리고 EKS에서는 여기에 IRSA(ServiceAccount 역할), 노드 IAM 역할, imagePullSecrets, 프라이빗 ECR 엔드포인트/네트워크가 얽히면서 원인 파악이 복잡해진다는 점입니다.

이 글은 ImagePullBackOff 401을 “무조건 docker login 다시” 같은 임시 처방이 아니라, 원인별로 재현 가능한 증거(이벤트/로그/권한)로 좁히고 재발 방지까지 가는 절차를 정리합니다.

> 쿠버네티스 장애를 체계적으로 좁히는 관점이 필요하다면, 서비스 장애 관점의 체크리스트도 함께 보면 좋습니다: Kubernetes LLM 서비스 502 504 간헐 장애와 스트리밍 끊김을 끝내는 NGINX Ingress와 Gunicorn Uvicorn 실전 튜닝

1) 먼저 “정확히 401인지” 이벤트로 확정하기

ImagePullBackOff는 결과 상태일 뿐이고, 진짜 단서는 Pod 이벤트에 있습니다.

kubectl describe pod -n <ns> <pod>

아래 같은 메시지가 핵심입니다.

  • Failed to pull image ...: rpc error: code = Unknown desc = Error response from daemon: pull access denied
  • ... 401 Unauthorized
  • ... no basic auth credentials
  • ... denied: User ... is not authorized to perform: ecr:BatchGetImage

또한 노드에서 어떤 런타임(containerd/dockerd)이 쓰이는지에 따라 메시지가 다르게 보일 수 있습니다.

자주 보이는 401 계열 메시지 해석

  • no basic auth credentials: imagePullSecret이 없거나, 잘못된 레지스트리 주소로 생성됨
  • 401 Unauthorized: 토큰 만료/불일치, 혹은 ECR 권한이 없는 주체가 인증 시도
  • denied: not authorized to perform ecr:*: 인증은 됐지만 IAM 정책이 부족

2) ECR 인증 구조: “누가” 이미지를 풀어오나?

여기서 가장 흔한 오해가 발생합니다.

  • 이미지 Pull 주체는 기본적으로 노드의 kubelet입니다.
  • IRSA는 “Pod 안에서 AWS API 호출”에 주로 쓰이며, 이미지 Pull에는 기본적으로 적용되지 않는 구성이 많습니다(특히 표준 kubelet pull 경로).
  • 따라서 ECR 401의 1차 점검 대상은 대개 노드 IAM(Role) + 노드 네트워크 + ECR 엔드포인트 접근입니다.

다만, 다음 케이스에서는 imagePullSecrets가 결정적입니다.

  • ECR이 아닌 외부 프라이빗 레지스트리
  • ECR이라도 kubelet이 사용할 자격 증명을 imagePullSecrets로 주입하는 패턴(운영 정책/툴링에 따라 존재)

3) 원인 A: ECR 토큰 만료(특히 imagePullSecrets에 토큰을 박아둔 경우)

ECR 로그인 토큰은 기본적으로 12시간 만료입니다. kubectl create secret docker-registry로 생성한 Secret에 ECR 토큰을 그대로 넣어두면, 시간이 지나면 401이 납니다.

증상

  • 배포 직후엔 정상
  • 일정 시간이 지나 신규 노드/재스케줄링 시에만 실패
  • 이벤트에 401 Unauthorized 또는 no basic auth credentials

해결 1) ECR 토큰을 Secret에 고정 저장하지 말기(권장)

EKS라면 가장 깔끔한 방식은 노드 IAM에 ECR Pull 권한을 부여하고, Secret 기반 로그인 의존을 없애는 것입니다.

필요 권한(최소 예시):

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

> GetAuthorizationTokenResource: *가 필요합니다(ECR API 특성).

해결 2) 어쩔 수 없이 Secret을 써야 한다면: 자동 갱신

예: CronJob으로 10~11시간마다 secret을 갱신합니다.

AWS_REGION=ap-northeast-2
ACCOUNT_ID=123456789012
SECRET_NAME=ecr-pull-secret
NS=default

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

kubectl -n $NS delete secret $SECRET_NAME --ignore-not-found
kubectl -n $NS create secret docker-registry $SECRET_NAME \
  --docker-server=${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com \
  --docker-username=AWS \
  --docker-password="$TOKEN"

그리고 Pod/ServiceAccount에 연결:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: default
imagePullSecrets:
  - name: ecr-pull-secret

4) 원인 B: 노드 IAM Role에 ECR Pull 권한이 없음(또는 잘못된 Role)

EKS에서 가장 흔한 케이스입니다. 노드 그룹을 새로 만들었는데, Launch Template/Managed Node Group의 IAM Role이 바뀌면서 ECR 권한이 빠지기도 합니다.

빠른 확인

  • 노드에 붙은 IAM Role 확인(EC2 콘솔/eksctl/terraform 상태)
  • 해당 Role에 AmazonEC2ContainerRegistryReadOnly 또는 최소 정책 부여 여부 확인

AWS 관리형 정책을 쓰면 빠릅니다.

  • AmazonEC2ContainerRegistryReadOnly

하지만 조직 정책상 커스텀 최소권한이 필요하면 앞 절의 최소 액션을 기준으로 작성합니다.

노드에서 직접 확인(SSM 또는 SSH 가능 시)

containerd 기반이라면 다음이 유용합니다.

# 노드에서
sudo crictl pull <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>

여기서도 401/denied가 나면 쿠버네티스가 아니라 노드 권한/네트워크 문제에 가깝습니다.

5) 원인 C: IRSA를 “이미지 Pull 인증”에 쓰려고 해서 생기는 오해

IRSA는 Pod에 AWS 권한을 부여하는 정석이지만, 기본 kubelet 이미지 pull은 노드 권한을 사용합니다. 그래서 다음과 같은 잘못된 결론이 나옵니다.

  • “ServiceAccount에 role annotation 달았는데도 401”

이건 정상입니다. IRSA가 이미지 pull 경로에 연결되지 않았기 때문입니다.

그럼 IRSA는 언제 관련 있나?

  • Pod 내부에서 ECR을 직접 호출(예: initContainer에서 aws ecr get-login-password 수행)
  • 또는 별도 컨트롤러/웹훅이 IRSA를 이용해 imagePullSecrets를 동적으로 생성

즉, IRSA만으로 이미지 pull이 해결될 거라고 기대하면 안 됩니다.

6) 원인 D: 레지스트리 주소/리포지토리/리전 불일치

의외로 많습니다.

  • 123.dkr.ecr.ap-northeast-2.amazonaws.com인데 토큰은 us-east-1로 발급
  • 이미지 URL에 오타(계정ID/리전/리포지토리명)
  • ECR Public과 Private 혼동

확인 포인트:

kubectl get pod -n <ns> <pod> -o jsonpath='{.spec.containers[*].image}'

그리고 ECR에서 실제 존재 여부:

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

7) 원인 E: ECR 리포지토리 정책(Resource policy) 또는 KMS 암호화

교차 계정(Prod 계정의 ECR을 Dev/EKS 계정에서 Pull)에서는 노드 IAM Role 권한만으로 부족하고, ECR 리포지토리 정책에서 해당 Principal을 허용해야 합니다.

리포지토리 정책 예시(개념):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCrossAccountPull",
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::111122223333:role/eks-node-role"},
      "Action": [
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchCheckLayerAvailability"
      ]
    }
  ]
}

또한 ECR이 KMS 키로 암호화되어 있고, 그 키 정책에서 노드 Role을 허용하지 않으면 권한 오류가 납니다(에러가 401보다는 AccessDenied로 보이는 경우가 많지만, 환경에 따라 혼동됩니다).

8) 원인 F: 프라이빗 서브넷 + VPC 엔드포인트 구성 문제(ECR API/DKR)

프라이빗 서브넷 노드가 NAT 없이 ECR에 접근하려면 보통 다음이 필요합니다.

  • com.amazonaws.<region>.ecr.api (Interface Endpoint)
  • com.amazonaws.<region>.ecr.dkr (Interface Endpoint)
  • com.amazonaws.<region>.s3 (Gateway Endpoint) — 레이어 다운로드가 S3를 경유
  • 엔드포인트 보안그룹에서 노드 SG 인바운드 443 허용
  • DNS 설정(Enable DNS hostnames/support)

네트워크가 막히면 401이 아니라 timeout이 흔하지만, 프록시/미러/중간 장비가 끼면 인증 실패처럼 보이기도 합니다.

9) imagePullSecrets 점검 체크리스트(있다면)

Secret이 존재해도 형식이 틀리면 kubelet이 못 씁니다.

kubectl get secret -n <ns> <secret> -o yaml

확인 포인트:

  • type: kubernetes.io/dockerconfigjson
  • .data[.dockerconfigjson] 존재
  • docker server가 정확히 ECR 도메인과 일치

디코딩해서 서버 주소 확인:

kubectl get secret -n <ns> <secret> \
  -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq .

10) “재발 방지” 관점의 권장 아키텍처

운영에서 가장 안정적인 조합은 보통 다음입니다.

  1. EKS 노드 IAM Role에 ECR read 권한 부여 (Secret 기반 토큰 저장 제거)
  2. 애플리케이션이 AWS API를 호출해야 하면 그때 IRSA 적용
  3. 프라이빗 서브넷이면 ECR API/DKR + S3 엔드포인트를 IaC로 고정
  4. 교차 계정이면 ECR repo policy + (필요 시) KMS key policy까지 함께 관리

이렇게 하면 “12시간 후 갑자기 401” 같은 토큰 만료성 장애를 구조적으로 제거할 수 있습니다.

11) 실전 트러블슈팅 순서(10분 컷)

  1. kubectl describe pod정확한 에러 문자열 확보(401 vs no basic auth vs AccessDenied)
  2. 이미지 URL에서 계정/리전/리포/태그 오타 제거
  3. 노드 Role에 ecr:GetAuthorizationToken 포함 여부 확인
  4. (Secret 사용 중이면) Secret이 ECR 토큰을 담고 있는지, 만료 가능성 확인
  5. 프라이빗 클러스터면 VPC 엔드포인트(ecr.api/ecr.dkr/s3)와 SG 443 확인
  6. 교차 계정이면 ECR 리포지토리 정책 확인

부록으로, 인증/권한 문제를 체크리스트 기반으로 접근하는 습관은 외부 API에서도 동일하게 유효합니다. 접근 방식 자체는 다음 글의 구조와도 유사합니다: OpenAI Responses API 401 403 인증오류 점검 가이드

원하시면, 사용 중인 환경(EKS/온프렘, containerd 여부, 프라이빗 서브넷/NAT, imagePullSecrets 사용 여부, 에러 이벤트 원문)을 기준으로 가장 가능성 높은 원인 1~2개로 좁힌 뒤 필요한 IAM 정책/엔드포인트/매니페스트를 정확히 만들어 드릴게요.