왜 문제가 되는가
프라이빗 레지스트리에 올린 이미지를 파드가 당겨오려 할 때 흔히 ErrImagePull 또는 ImagePullBackOff가 발생한다. 원인은 대부분 인증이다. kubelet은 노드에서 이미지를 pull하는데, 이때 레지스트리 자격 증명을 어디서도 찾지 못하면 익명 요청으로 처리되어 401/403을 받는다. 로컬에서 docker pull이 잘 되던 이미지가 클러스터에서 실패하는 이유는, 개발자 노트북의 ~/.docker/config.json에 저장된 로그인 정보가 클러스터 노드에는 없기 때문이다.
핵심은 인증 주체가 사용자가 아니라 노드의 kubelet이라는 점이다. 따라서 자격 증명을 파드 스펙에 연결된 형태로 클러스터에 심어줘야 한다. 이 역할을 하는 것이 kubernetes.io/dockerconfigjson 타입의 시크릿, 즉 image pull secret이다.
이미지 풀 시크릿 생성
가장 직접적인 방법은 kubectl create secret docker-registry다. 내부적으로 도커 config 형식의 JSON을 base64로 인코딩해 시크릿에 담는다.
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=ci-bot \
--docker-password="$REGISTRY_TOKEN" \
[email protected] \
--namespace=prod
# 생성된 시크릿 확인 (디코드)
kubectl get secret regcred -n prod \
-o jsonpath='{.data.\.dockerconfigjson}' | base64 -d
주의할 점은 --docker-server 값이 이미지 참조의 레지스트리 호스트와 정확히 일치해야 한다는 것이다. docker.io 이미지는 서버를 https://index.docker.io/v1/로 지정해야 매칭된다. 호스트가 다르면 시크릿이 있어도 kubelet이 해당 레지스트리용으로 인식하지 못한다.
파드와 서비스어카운트에 연결
시크릿을 만들었다고 자동으로 쓰이지 않는다. 파드가 imagePullSecrets로 참조하거나, 파드가 쓰는 서비스어카운트에 붙여야 한다. 후자를 권장한다. 개별 파드마다 지정할 필요 없이 네임스페이스의 모든 워크로드에 일괄 적용되기 때문이다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: prod
imagePullSecrets:
- name: regcred
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: prod
spec:
replicas: 2
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
# serviceAccountName 생략 시 default 사용 → 위 SA의 secret 상속
containers:
- name: api
image: registry.example.com/team/api:1.4.2
시크릿은 네임스페이스 스코프다. prod에 만든 시크릿을 staging 파드가 쓸 수 없으므로 네임스페이스마다 배포해야 한다.
방식 비교
| 방식 | 장점 | 단점 |
|---|---|---|
| 파드 imagePullSecrets 직접 지정 | 명시적, 파드별 제어 | 매니페스트마다 반복 |
| 서비스어카운트에 부착 | 네임스페이스 일괄 적용 | 범위가 넓어 감사 시 주의 |
| 클라우드 IAM 연동(ECR/GCR 등) | 토큰 자동 갱신, 시크릿 무관리 | 클라우드 종속, 초기 설정 복잡 |
토큰 만료와 자동 갱신
실무에서 가장 자주 겪는 함정은 토큰 만료다. AWS ECR의 인증 토큰은 12시간짜리다. docker-registry 시크릿에 이 토큰을 박아두면 반나절 뒤 pull이 깨진다. 정적 시크릿 대신 IAM 기반 인증(IRSA, Workload Identity)을 쓰면 kubelet이 노드 역할로 토큰을 즉석에서 발급받는다. 정적 시크릿을 유지해야 한다면 크론으로 주기적 갱신을 자동화한다.
#!/usr/bin/env bash
set -euo pipefail
# ECR 토큰을 재발급해 시크릿을 교체 (CronJob 등에서 6시간 주기 실행)
TOKEN=$(aws ecr get-login-password --region ap-northeast-2)
kubectl create secret docker-registry regcred \
--docker-server="$ACCOUNT.dkr.ecr.ap-northeast-2.amazonaws.com" \
--docker-username=AWS \
--docker-password="$TOKEN" \
--namespace=prod \
--dry-run=client -o yaml | kubectl apply -f -
--dry-run=client와 kubectl apply 조합은 기존 시크릿을 무중단으로 덮어쓰는 관용구다. delete 후 create하면 그 사이 pull이 실패할 수 있으니 피한다.
디버깅 체크리스트
pull 실패 시 원인을 좁히는 순서다.
kubectl describe pod의 Events에서 401/403인지 not found인지 구분한다. 403은 인증, not found는 태그·경로 문제다.- 시크릿의
docker-server가 이미지 호스트와 일치하는지 디코드해 확인한다. - 시크릿이 파드와 같은 네임스페이스에 있는지 본다.
- 서비스어카운트에 붙였다면 파드가 실제로 그 SA를 쓰는지(
serviceAccountName) 확인한다. - 노드에서 직접
crictl pull로 kubelet 관점의 재현을 시도한다.
보안상 주의점
시크릿의 .dockerconfigjson은 base64일 뿐 암호화가 아니다. etcd 저장 시 암호화(EncryptionConfiguration)를 켜고, RBAC으로 시크릿 get 권한을 최소화한다. 또한 pull 전용 자격 증명은 읽기 전용 권한으로 발급해 유출 시 이미지 덮어쓰기 피해를 막는다. 매니페스트에 base64 시크릿을 커밋하는 것은 사실상 평문 노출이므로, GitOps 환경에서는 SealedSecrets나 외부 시크릿 연동으로 원문이 레포에 남지 않게 한다.