Troubleshooting 2026.11.03 8 min read

Rootless Podman 빌드에서 사설 Registry Push까지 연결하기

우선 필요한 것은 인증한 빌드 계정이 이미지를 Push하고 클러스터가 Pull할 수 있는 경로였다. 이 글은 그중 빌드와 Registry 게시까지의 기록이다. 당시 Harbor나 GitLab을 설치한 단계까지 진행한 것은 아니었다.

Jenkins에서 JAR를 만들고 나니 Kubernetes가 가져갈 이미지를 어디에 둘지 정해야 했다. GitHub 소스 저장소와 Jenkins는 유지하면서 이미지 전달 경로를 내부에 두기로 했다. GHCR, GitLab, Harbor를 검토한 뒤 실제로 도입한 것은 Distribution v3.1.2였다.

우선 필요한 것은 인증한 빌드 계정이 이미지를 Push하고 클러스터가 Pull할 수 있는 경로였다. 이 글은 그중 빌드와 Registry 게시까지의 기록이다. 당시 Harbor나 GitLab을 설치한 단계까지 진행한 것은 아니었다.

Maven을 실행하기 전에 이미지 이름에서 멈췄다

이미지는 CI 전용 계정의 rootless Podman에서 빌드했다. Docker 소켓을 Jenkins에 전달하지 않고 계정 권한으로 작업하는 구성이었다. 그런데 첫 빌드는 소스를 컴파일하기도 전에 Dockerfile의 FROM에서 멈췄다.

Jenkins Agent 계정의 rootless Podman 실행 환경
Jenkins Agent 계정의 rootless Podman 실행 환경.
short-name "maven@sha256:…" did not resolve to an alias
and no unqualified-search registries are defined

Maven base image에는 태그와 digest가 있었지만 Registry 이름이 생략돼 있었다. digest를 지정했다고 이미지 이름의 출처까지 자동으로 정해지는 것은 아니었다. 오류가 발생한 지점도 Maven 테스트가 아니라 빌드 컨테이너를 생성할 이미지를 찾는 단계였다.

당시에는 빌드 계정의 containers 설정에 다음 값을 넣는 방식으로 진행했다.

unqualified-search-registries = ["docker.io"]

설정 확인도 한 번에 끝나지 않았다. 처음 사용한 .Registries.Search 조회는 <no value>를 반환했다. 설정이 적용되지 않았다고 보기 전에 조회 대상의 구조를 확인했고, index .Registries "search" 또는 JSON의 실제 키를 읽는 형식으로 정정했다.

FROM에 Registry를 명시하는 개선도 검토했지만, 이때 저장소에 반영했다고 단정하지 않았다. 먼저 확인한 것은 계정별 검색 설정을 정리한 뒤 빌드가 다음 단계로 진행한 결과였다.

다음 로그에서 Maven의 BUILD SUCCESS가 보였을 때도 확인은 남았다. 멀티스테이지 내부의 Java 빌드 성공과 최종 OCI 이미지 생성은 단계가 달랐다. 이미지 목록과 inspect를 별도 확인 항목으로 두었고, 이후 Push와 Pull 및 운영 이미지 검증으로 후속 경로를 확인했다.

Podman 이미지 목록과 태그·Digest
Podman 이미지 목록과 태그·Digest.

localhost 시험에서 전용 서비스 계정으로 옮겼다

Registry는 처음부터 외부에 열지 않았다. 빌드 계정에서 localhost 5000 포트에만 바인딩해 컨테이너 상태와 /v2/ 응답을 확인했다. 이 시험이 정상이어도 다른 VM의 Kubernetes 노드가 접속할 수 있는 상태는 아니었다.

외부 노드의 Pull 경로를 만들면서 같은 VM에 Registry 운영 전용 OS 계정을 추가했다. 빌드 계정과 Registry 서비스의 권한을 나누기 위한 선택이었다. 서비스는 별도 계정의 rootless Podman과 사용자 systemd 아래에서 실행하도록 구성했다.

rootless 볼륨은 계정별로 분리돼 있었다. 같은 이름의 볼륨을 만들더라도 빌드 계정의 데이터가 새 서비스 계정에 자동으로 이어지지 않았다. 이때는 아직 기존 이미지 Push가 없다는 전제에서 새 전용 볼륨을 준비했다. 이미 저장된 이미지를 자동 이관한 작업으로 설명하지 않았다.

계정 분리로 관리 권한은 나눴지만 두 역할은 여전히 같은 VM과 디스크, 하부 NFS의 영향을 받았다. 별도 Registry 서버나 독립적인 장애 영역을 만든 구성은 아니었다.

사설 CA와 인증을 붙이고 Quadlet으로 관리했다

실제 환경은 Registry의 내부 IP로 접속했다. 서버 인증서에도 해당 IP를 SAN으로 넣어 접속 대상과 인증서의 식별자가 맞도록 구성했다. 이후 클라이언트에서도 TLS 검증을 유지한 채 연결하는 것이 목표였다.

CA 개인키는 클라이언트에 배포하지 않았다. Registry 컨테이너에는 서버 인증서와 서버 키, htpasswd 인증 파일만 읽기 전용으로 제공했다. Registry용 CA는 나중에 애플리케이션 HTTPS를 붙일 때 사용한 CA와 별개의 체계였다.

서비스는 Quadlet의 .container 파일로 관리했다. 핵심은 Registry 데이터 볼륨과 TLS·인증 설정을 사용자 systemd 서비스에 연결하는 것이었다. 당시 설정의 일부는 다음과 같았다.

Environment=REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server.crt
Environment=REGISTRY_HTTP_TLS_KEY=/certs/server.key
Environment=REGISTRY_AUTH=htpasswd
Environment=REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd

서비스 계정의 linger를 활성화하고 사용자 서비스로 기동했다. 비밀번호나 키의 내용은 설정 예시에 넣지 않았다. 인증은 htpasswd 기반 Basic Auth였으며 repository별 세밀한 Push/Pull ACL까지 구성했다고 설명하지 않았다.

401과 200을 서로 다른 성공 조건으로 봤다

TLS와 인증을 붙인 뒤에는 /v2/ 응답을 두 단계로 확인했다. Registry CA를 지정하고 인증 없이 요청하면 401, 올바른 사용자 인증을 더하면 200과 빈 JSON 객체를 기대했다. 당시 점검에서는 이 두 결과가 정상이라고 기록했다.

401은 TLS 검사에 유용했다. 지정한 CA로 인증서를 검증한 뒤 Registry의 HTTP 인증 요구까지 도달한 결과였기 때문이다. 200이 아니라는 이유로 TLS 실패로 판단하면 인증서 신뢰와 사용자 인증을 한꺼번에 섞게 된다.

Registry CA 검증과 미인증 HTTPS 401 응답
Registry CA 검증과 미인증 HTTPS 401 응답.

이후 CI Agent의 신뢰 저장소에 Registry CA를 적용하고, 빌드 계정에서 이미지를 태그해 로그인과 Push를 진행했다. 태그 목록 확인까지 이어서 게시 경로를 검증했다. 이때의 ci-test는 수동 연결 시험용 태그였다.

최종 CI에서는 전체 소스 SHA와 빌드 번호, manifest digest를 사용하고 Jenkins Credentials로 인증 정보를 주입했다. 최초 수동 login 시험과 이 후속 자동화는 구분했다. 또한 초기에 확인한 rootless=true만으로 최종 런타임 제한 검사까지 끝났다고 계산하지 않았다.

Registry 인증 200과 이미지 태그·Manifest Digest
Registry 인증 200과 이미지 태그·Manifest Digest.

보안·SBOM 검사는 후속 CI의 독립된 단계로 확인했다.

Trivy·SBOM 보안 단계의 실제 Jenkins 출력
Trivy 검사와 SBOM 생성 단계 성공.

Registry가 응답하고 Push가 끝난 다음에는 다른 클라이언트가 남았다. CI Agent의 Podman이 신뢰한 인증서를 Kubernetes 노드의 containerd도 올바르게 읽는지 확인해야 했다. 다음 문제는 바로 그 경계에서 나타났다.

Distribution에는 이번 구성에서 별도 관리 GUI가 없다. 게시 여부는 실제 /v2/ 응답, repository·tag·manifest 메타데이터와 digest로 확인했다. 2026년 10월 19일 읽기 전용 확인에서는 CA·접속 이름 검증을 통과한 무인증 /v2/ 요청의 401을 다시 확인했다.

SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 ↑ ↓ 결과 이동