03. NHN Container Registry (NCR)

정세현·2026년 7월 22일

NHN AX Internship Notes

목록 보기
8/16

0. 한 문장 정의

Docker 컨테이너 이미지를 저장·관리·배포하는 프라이빗 레지스트리 서비스.
NKS와 연동되어 컨테이너 기반 배포 파이프라인의 중심축이 된다.

Docker Hub의 프라이빗 버전 + NHN Cloud 인증 체계 결합이라고 보면 된다.


1. 왜 별도 레지스트리가 필요한가

Docker Hub 사용 시 문제NCR
소스 코드가 담긴 이미지가 공개될 위험프라이빗 + NHN Cloud 인증
Pull rate limit (무료 계정 제한)제한 없음
네트워크 지연 (해외 리전)같은 리전 → 빠른 pull
인터넷 게이트웨이 필수Private URI로 폐쇄망에서도 사용 가능

2. 구성 요소 4단계

용어를 정확히 구분해야 콘솔에서 헤매지 않는다.

레지스트리 (Registry)
  └─ 이미지 (Image)          예: my-api
       └─ 아티팩트 (Artifact) 예: sha256:a1b2c3...  ← 실체
            └─ 태그 (Tag)     예: 1.0.3, latest     ← 별명

2-1. 레지스트리

이미지를 보관·관리하는 최상위 단위. 모든 이미지는 반드시 하나의 레지스트리에 속한다.
레지스트리별로 웹훅 같은 관리 설정을 지정한다.

이름 규칙: 영문 소문자 또는 숫자로 시작, 이후 소문자·숫자·_·.·- 사용 가능. 최대 255자.

2-2. 이미지

아티팩트들의 집합. docker push / docker pull로 다룬다. 레지스트리 내에서 이름이 유일해야 한다.

2-3. 아티팩트

실제 빌드 결과물. 하나 이상의 레이어를 가지며, 매니페스트의 해시값인 다이제스트(digest) 로 식별된다.
태그가 없을 수도, 여러 개일 수도 있다.

저장 공간 최적화: 여러 아티팩트가 공유하는 공통 레이어는 한 번만 저장된다.
그래서 Spring Boot 이미지를 100번 push해도 실제 용량은 100배가 되지 않는다.
단, Dockerfile 레이어 순서를 잘 짜야 이 이점을 누린다 (아래 5번 참고).

2-4. 태그

아티팩트를 구분하는 식별자. 사용자가 버전을 부여하는 수단.

이름 규칙: 영문 소문자·숫자·_로 시작, 이후 소문자·숫자·_·.·-. 최대 128자.


3. 다이제스트 vs 태그 — 실무에서 제일 중요한 구분

태그다이제스트
예시my-api:1.0.3my-api@sha256:a1b2c3...
성격가변. 다른 아티팩트에 다시 붙일 수 있음불변. 내용이 바뀌면 값도 바뀜
용도사람이 읽기 쉬움재현 가능한 배포

latest를 쓰면 안 되는 이유

image: my-api:latest    # x
  1. 파드가 재시작될 때마다 다른 이미지가 뜰 수 있다 → 같은 replicas인데 버전이 섞임
  2. kubectl apply를 해도 이미지 태그 문자열이 그대로면 K8s가 변경 없음으로 판단해 롤아웃이 안 일어난다
  3. 롤백이 불가능하다. 이전 latest가 뭐였는지 알 수 없음
image: my-api:1.0.3                    # 최소한 이렇게
image: my-api@sha256:a1b2c3...         # 완전히 재현 가능

태그 전략 예시

1.0.3               # 시맨틱 버전 (릴리스)
1.0.3-a3f9c21       # 버전 + Git 커밋 SHA (추적 가능)
main-a3f9c21        # 브랜치 + 커밋 SHA (개발용)

4. 주요 기능

기본

  • Docker 이미지 매니페스트 v2 호환 → 표준 docker CLI 그대로 사용
  • HTTPS 암호화 + NHN Cloud 인증/권한 관리
  • NHN Cloud Object Storage 기반 → 확장성·안정성 확보

웹 콘솔

  • 레지스트리 생성/삭제/조회
  • 이미지·아티팩트 조회/검색/삭제
  • 태그 생성/조회/삭제
  • 웹훅 설정 → 배포 파이프라인 구성
  • 이미지 정리(cleanup)·보호(protection) 설정

고급 기능

기능설명언제 쓰나
리전 간 복제레지스트리를 다른 리전으로 복제멀티 리전 서비스, DR
Private URI인터넷 게이트웨이 없는 인스턴스에서도 접근보안 요구사항이 높은 폐쇄망 클러스터
이미지 캐시외부 레지스트리 이미지를 캐싱Docker Hub rate limit 회피, pull 속도 개선
취약점 스캐닝이미지 내 알려진 CVE 탐지CI에서 심각도 High 이상이면 배포 차단
이미지 신뢰(Trust)서명된 이미지만 허용공급망 공격 방어
웹훅push 등 이벤트 발생 시 외부 호출CI/CD 자동 트리거

5. 실전 워크플로우

5-1. 로그인 → 빌드 → 푸시

# 1. 로그인 (콘솔에서 발급받은 계정/토큰 사용)
docker login <레지스트리주소>

# 2. 빌드 — 태그를 레지스트리 경로로 지정
docker build -t <레지스트리주소>/my-api:1.0.3 .

# 3. 푸시
docker push <레지스트리주소>/my-api:1.0.3

# 4. 확인
docker pull <레지스트리주소>/my-api:1.0.3

Mac Mini M4(ARM) 주의: 로컬에서 그냥 빌드하면 arm64 이미지가 나온다.
NKS 노드가 x86이면 exec format error로 파드가 죽는다.

docker buildx build --platform linux/amd64 -t <레지스트리>/my-api:1.0.3 --push .

멀티 아키텍처가 필요하면 --platform linux/amd64,linux/arm64.

5-2. NKS에서 NCR 이미지 가져오기 (imagePullSecret)

프라이빗 레지스트리이므로 클러스터에 인증 정보를 알려줘야 한다.

kubectl create secret docker-registry ncr-secret \
  --docker-server=<레지스트리주소> \
  --docker-username=<사용자> \
  --docker-password=<비밀번호또는토큰> \
  -n <네임스페이스>
spec:
  imagePullSecrets:
    - name: ncr-secret
  containers:
    - name: app
      image: <레지스트리주소>/my-api:1.0.3

모든 파드에 매번 적기 귀찮다면 — ServiceAccount에 붙이면 자동 적용된다:

kubectl patch serviceaccount default -n <네임스페이스> \
  -p '{"imagePullSecrets":[{"name":"ncr-secret"}]}'

imagePullSecret네임스페이스별로 만들어야 한다. default에 만들고 prod에서 쓰려 하면 실패.
ImagePullBackOff의 흔한 원인 1순위.

5-3. Spring Boot용 Dockerfile (레이어 캐시 최적화)

# ---- Build stage ----
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app

# (1) 의존성 파일만 먼저 복사 → 소스만 바뀌면 이 레이어는 캐시 재사용
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon || true

# (2) 소스 복사 후 빌드
COPY src src
RUN ./gradlew bootJar --no-daemon -x test

# (3) fat jar를 레이어로 분해 (Spring Boot layertools)
RUN java -Djarmode=layertools -jar build/libs/*.jar extract --destination extracted

# ---- Runtime stage ----
FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app

# 비루트 사용자로 실행
RUN useradd -m -u 1001 appuser
USER appuser

# 변경 빈도가 낮은 레이어부터 복사 → 캐시 효율 극대화
COPY --from=builder /app/extracted/dependencies/ ./
COPY --from=builder /app/extracted/spring-boot-loader/ ./
COPY --from=builder /app/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/extracted/application/ ./

EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]

왜 이렇게 하나

  • 의존성(수십~수백 MB)은 거의 안 바뀌고, 내 코드(수 MB)만 자주 바뀐다
  • 레이어를 분리하면 바뀐 레이어만 push/pull → 배포 속도가 몇 배 빨라지고 NCR 저장 공간도 절약
  • fat jar 통째로 COPY하면 코드 한 줄만 바꿔도 전체 레이어가 새로 생성된다

5-4. CI/CD 연동 (GitHub Actions 예시)

name: build-and-push
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Login to NCR
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.NCR_REGISTRY }}
          username: ${{ secrets.NCR_USERNAME }}
          password: ${{ secrets.NCR_PASSWORD }}

      - name: Build & Push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64
          push: true
          tags: |
            ${{ secrets.NCR_REGISTRY }}/my-api:${{ github.sha }}
            ${{ secrets.NCR_REGISTRY }}/my-api:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to NKS
        run: |
          echo "${{ secrets.KUBECONFIG }}" > kubeconfig
          KUBECONFIG=kubeconfig kubectl set image deployment/my-api \
            app=${{ secrets.NCR_REGISTRY }}/my-api:${{ github.sha }}
          KUBECONFIG=kubeconfig kubectl rollout status deployment/my-api --timeout=5m

kubectl set image커밋 SHA 태그를 쓰는 게 핵심. 매번 태그가 달라지므로 롤아웃이 확실히 트리거된다.


6. 운영 팁

이미지 정리 (Cleanup)

CI가 커밋마다 push하면 이미지가 폭증한다. 정리 규칙을 걸어두자.

  • "태그 없는(dangling) 아티팩트는 7일 후 삭제"
  • "최근 N개 태그만 유지"

이미지 보호 (Protection)

운영에 배포된 태그(1.0.3, prod 등)는 삭제·덮어쓰기 방지를 걸어둔다.
누군가 실수로 지우면 롤백 불가 사태가 난다.

취약점 스캐닝을 CI에 연결

push 후 스캔 결과를 확인하고, Critical/High가 있으면 배포를 막는 게 이상적.
베이스 이미지를 -alpine이나 distroless로 바꾸면 취약점 수가 크게 줄어든다.

이미지 크기 줄이기

방법효과
멀티 스테이지 빌드JDK(400MB+) → JRE(200MB대)
-jre 또는 -jre-alpine 베이스큰 폭 감소
.dockerignore 작성빌드 컨텍스트 축소 (.git, build/, node_modules 제외)
distroless 베이스최소 크기 + 공격 표면 최소화 (단 셸이 없어 디버깅 어려움)

7. 체크리스트

  • 레지스트리 / 이미지 / 아티팩트 / 태그의 차이를 설명할 수 있다
  • docker loginbuildpush를 성공했다
  • --platform linux/amd64로 빌드해야 하는 이유를 안다
  • imagePullSecret을 만들어 NKS에서 pull에 성공했다
  • latest 태그를 쓰면 안 되는 이유 3가지를 말할 수 있다
  • 멀티 스테이지 + layertools Dockerfile을 작성해봤다
  • 웹훅 또는 CI로 push → 배포를 자동화해봤다
profile
I'm the best

0개의 댓글