Cloudera AI 에서 모델을 배포하면 failed to resolve reference ... 401 Unauthorized 가 뜨며 파드가 뜨지 않는 경우가 있다. 문구에 경로가 등장해 경로 문제로 오해하기 쉽지만, 실제로는 containerd 가 이미지 매니페스트를 HEAD 요청으로 조회하다 인증을 거부당한 것이다. Docker Registry 는 존재하지 않는 리포지터리에 대해서도 404 가 아니라 401 을 돌려주는 것이 표준 동작이므로, "인증 실패"와 "이미지 없음"이 같은 코드로 나타난다는 점이 진단의 출발점이다.
파드 상태에서 ErrImagePull · ImagePullBackOff 인지 확인하고 Events 에 찍힌 전체 이미지 경로를 그대로 읽는다.
kubectl -n <workbench-ns> get pods
kubectl -n <workbench-ns> describe pod <model-pod>
kubectl -n <workbench-ns> get pod <model-pod> -o wide
이름이 UUID 인 이미지는 Cloudera AI 가 모델 빌드로 만들어 낸 산출물이고, engine-deps · k8tz · kinit · ssh · fluent-bit 등은 기반 이미지다. Events 에 기반 이미지가 already present on machine 으로만 찍혀 있으면 그 노드는 레지스트리에서 pull 을 시도한 적이 없다는 뜻이므로, "다른 노드에서는 성공한다"는 전제부터 다시 검증해야 한다.
노드를 파기 전에 레지스트리를 본다. 모델 빌드는 한 노드에서 이미지를 만든 뒤 레지스트리로 push 하는 구조라, push 가 실패하면 빌드가 돌았던 노드에만 로컬 캐시로 남고 다른 노드에서는 401 이 난다.
# 노드 로컬 저장소 확인 (네트워크·인증을 타지 않는다)
ctr -n k8s.io images ls | grep <image-uuid>
crictl images | grep <image-uuid>
# 레지스트리에 있는지 확인
curl -sk -u <user> "https://<registry>:5000/v2/<project>/<image-uuid>/tags/list"
curl -skI -u <user> -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
"https://<registry>:5000/v2/<project>/<image-uuid>/manifests/latest"
curl -sk -u <user> "https://<registry>:5000/v2/_catalog?n=2000" | tr ',' '\n' | grep <image-uuid>
판정은 다음과 같다.
| 노드 로컬 | 레지스트리 | 해석 |
|---|---|---|
| 있음 | 없음 | push 실패. 원인 확정이며 노드는 볼 필요가 없다 |
| 있음 | 있음 | 레지스트리엔 있는데 특정 노드가 못 받는 것. 노드·네트워크 문제 |
| 없음 | 있음 | 그 노드는 실제로 pull 에 성공한 것. 노드 간 차이가 원인 |
401 이 "인증 실패"인지 "리포지터리 없음"인지 가르려면 정상 이미지로 대조한다. 다른 리포지터리는 200 인데 문제의 이미지만 401 이면 자격 증명은 정상이고 리포지터리가 없는 것이다.
curl -sk -u <user> "https://<registry>:5000/v2/<project>/ssh/tags/list"
push 단계 실패는 빌드 로그에 unauthorized, denied: requested access to the resource is denied, blob upload unknown 같은 문구로 남는다.
kubectl -n <ns> get pods | grep -i build
kubectl -n <ns> logs <build-pod> | tail -50
정상 노드와 나란히 같은 명령을 돌려 차이를 찾는다.
시간 동기화가 1순위다. 레지스트리 Bearer 토큰은 exp · nbf 를 검증하므로 노드 시계가 몇 분만 틀어져도 방금 받은 토큰이 만료로 거부되어 정확히 401 이 난다.
timedatectl status | grep -i -E 'synchronized|Local time'
chronyc tracking | grep -i 'System time'
인증 흐름을 손으로 재현한다. Docker Registry v2 는 레지스트리에서 401 과 WWW-Authenticate 헤더를 받고 별도 auth 엔드포인트에서 토큰을 받아오는 2단계 구조다. 두 번째 도메인만 막혀도 최종 401 이 된다.
curl -sv "https://<registry>:5000/v2/" 2>&1 | grep -i -E 'HTTP/|www-authenticate'
curl -v "<realm>?service=<service>&scope=repository:<repo>:pull"
TLS 인터셉션 여부는 인증서 발급자로 판별한다. 노드별로 issuer 나 serial 이 다르면 그 노드만 방화벽의 TLS 복호화를 타는 것이고, 이때 Authorization 헤더가 유실되어 401 이 난다.
openssl s_client -connect <registry>:5000 -servername <registry> </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -serial -dates
나가는 경로와 containerd 설정도 비교한다.
ip route get <registry-ip>
getent hosts <registry>
env | grep -i proxy
systemctl show containerd | grep -i Environment
grep -A10 'registry' /etc/containerd/config.toml
ls /etc/containerd/certs.d/<registry>/
마지막으로 캐시가 있는 정상 이미지를 crictl pull 로 다시 받아 본다. crictl pull 은 캐시가 있어도 레지스트리 확인을 다시 하므로, 이것까지 401 이면 특정 이미지 문제가 아니라 그 노드의 레지스트리 인증 전반이 깨진 것이다.
crictl pull <registry>:5000/<project>/ssh:<tag>
401 은 연결 자체는 성공했다는 뜻이다. 순수한 방화벽 차단이라면 timeout 이나 connection refused 가 난다. 따라서 401 이 보일 때 의심할 네트워크 시나리오는 차단이 아니라 중간 장비의 개입이다 — TLS 인터셉션으로 인한 인증 헤더 유실, 인증 프록시 경유, 토큰 발급 도메인만 차단, L7 방화벽의 메서드·URL 패턴 필터링이 여기에 해당한다. 방화벽 담당자에게는 레지스트리 도메인뿐 아니라 토큰 발급 도메인까지 허용하고 해당 IP 를 TLS 인터셉션 예외로 처리해 달라고 요청한다.
kubectl -n <ns> get secret <pull-secret> -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d
CDSW_APIV2_KEY 문제다.ctr 로 로컬 이미지를 조회할 때 -n k8s.io 를 빠뜨리면 default 네임스페이스를 보게 되어 아무것도 나오지 않는다.