Harbor 컨테이너 레지스트리 Kubernetes 배포 가이드

이 포스트는 Kubernetes 클러스터에 Harbor 컨테이너 레지스트리를 Helm을 사용하여 배포하고, NGINX Ingress Controller와 VirtualServer 리소스를 활용해 배포한 Harbor로 연결하도록 구성하는 방법에 관해 설명합니다.
배포한 Harbor로 클러스터 외부의 VM에서 이미지를 push 하는 방법과 클러스터 내의 Pod를 배포할 때 Harbor의 이미지를 pull 하여 사용하는 방법도 다룹니다.

목차

1. Harbor란?
2. 버전 정보
3. Kubernetes 클러스터에 Harbor 배포
 3-1. Helm을 사용한 Harbor 배포
 3-2. NGINX Ingress Controller의 VirtualServer 구성을 통한 Harbor 연결
4. Harbor 활용
 4-1. Harbor 컨테이너 레지스트리로 이미지 push
 4-2. Harbor의 이미지로 클러스터의 Pod 배포
5. 결론

1. Harbor란?

Harbor는 오픈소스 컨테이너 이미지 레지스트리입니다. VMware가 처음 개발한 Harbor는 CNCF(Cloud Native Computing Foundation)의 프로젝트로, 쿠버네티스와 같은 클라우드 네이티브 환경에서 컨테이너 이미지를 효율적이고 안전하게 관리할 수 있도록 설계되었습니다.

Harbor에는 다음과 같은 특징이 있습니다.

1. 보안 기능

  • 컨테이너 이미지 취약점 스캐닝
  • 이미지 서명 및 유효성 검증
  • RBAC(Role-Based Access Control)를 통한 세밀한 접근 제어
  • LDAP/AD, OIDC 등 다양한 인증 방식 지원

2. 이미지 관리

  • 이미지 복제 및 분배 기능
  • 이미지 보존 정책 설정
  • 프로젝트 단위의 격리된 저장소 관리
  • 이미지 태그 관리 및 정리

3. 사용 편의성

  • 직관적인 웹 사용자 인터페이스
  • REST API를 통한 자동화 지원
  • Kubernetes의 원활한 통합
  • Helm 차트 저장소 기능

2. 버전 정보

  • Kubernetes: v1.30.3
  • Container Runtime: Containerd – 1.7.2
  • Helm: v3.16.4
  • Harbor
    • Helm Chart : 1.16.0
    • App : 2.12.0

3. Kubernetes 클러스터에 Harbor 배포

클러스터에 Harbor를 Helm을 사용하여 Ingress 타입으로 배포하여 구성하고, Ingress 리소스를 VirtualServer 리소스 형식에 맞게 변경하여 다시 구성하는 방법도 알아보겠습니다.
NGINX Ingress Controller가 사전 구성된 클러스터에서 진행됩니다.

3-1. Helm을 사용한 Harbor 배포

Helm이 설치되어 Kubernetes 클러스터에 접근이 가능한 환경에서 진행합니다.

1. Harbor를 배포할 네임스페이스를 생성합니다.

$ kubectl create ns harbor

2. Helm Repo를 추가/업데이트하고, 구성에 사용할 설정 파일(values.yaml)을 다운로드합니다.

$ helm repo add harbor https://helm.goharbor.io
$ helm repo update

$ curl -O "https://raw.githubusercontent.com/goharbor/harbor-helm/1.16.0/values.yaml"

최신 브랜치(1.16.0) 정보는 harbor-helm GitHub 리포지토리를 참고하세요.
main 브랜치의 경우 개발 중인 버전으로 안정적인 버전 사용이 권장됩니다.

3. 배포 설정을 위해 values.yaml 파일을 열고 편집합니다.
이 포스트에서는 Ingress 타입으로 배포했으며 사전에 구성된 PVC(PersistentVolumeClaim)을 사용해 구성했습니다.

expose:
  type: ingress       # ingress 리소스를 통해 Harbor 노출
  tls:
    enabled: true
    certSource: auto  # 자체서명 인증서 자동 생성 / 기존 secret 인증서 사용 시 secret
    auto:             
      commonName: ""
    secret:
    # secrets 설정 시 인증서가 포함된 secret 이름 지정
    # key 이름은 tls.crt : 인증서, tls.key : 키
      secretName: ""
  ingress:
    hosts:
      core: harbor.devopssong.com  # 도메인 설정
    controller: default
    className: "nginx"
    annotations:
      nginx.ingress.kubernetes.io/ssl-redirect: "true"
      nginx.ingress.kubernetes.io/proxy-body-size: "0"
    labels: {}

......

externalURL: https://harbor.devopssong.com  # ingress.hosts.core와 동일하게 설정

persistence:
  enabled: true                               # PVC 사용 설정
  resourcePolicy: "keep"                      # Helm delete에도 PVC 설정
  persistentVolumeClaim:
    registry:
      existingClaim: "harbor-registry-pvc"    # regisrty용 PVC 이름 작성
      storageClass: ""
    jobservice:
      jobLog:
        existingClaim: "harbor-jobservice-pvc"
        storageClass: ""
    database:
      existingClaim: "harbor-db-pvc"  
      storageClass: ""
    redis:
      existingClaim: "harbor-redis-pvc"
      storageClass: ""
    trivy:
      existingClaim: "harbor-trivy-pvc"
      storageClass: ""

......

harborAdminPassword: "Harbor12345"    # 초기 admin 계정 비밀번호

......

values.yaml 파일에 구성할 수 있는 모든 매개변수의 상세한 정보는 harbor-helm GitHub 리포지토리를 참고하세요.

4. Helm을 통해 vaules.yaml 파일의 설정으로 Harbor를 배포합니다.

$ helm install k8s-harbor harbor/harbor -n harbor -f values.yaml
  • k8s-harbor : 릴리즈 이름 지정
  • -n harbor : 배포 네임스페이스 지정
  • -f values.yaml : 사용할 설정 yaml 파일 지정
$ helm ls

NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
k8s-harbor      harbor          1               2025-01-09 06:52:40.560761148 +0000 UTC deployed        harbor-1.16.0   2.12.0

5. 배포된 리소스를 확인할 수 있습니다.

$ kubectl get all -n harbor

NAME                                         READY   STATUS    RESTARTS      AGE
pod/k8s-harbor-core-6f48898655-nr9g7         1/1     Running   0             61s
pod/k8s-harbor-database-0                    1/1     Running   0             61s
pod/k8s-harbor-jobservice-6bc675757c-6n7dk   1/1     Running   2 (56s ago)   61s
pod/k8s-harbor-portal-776c954b64-99n8g       1/1     Running   0             61s
pod/k8s-harbor-redis-0                       1/1     Running   0             61s
pod/k8s-harbor-registry-7dc7648b8c-74fhc     2/2     Running   0             61s
pod/k8s-harbor-trivy-0                       1/1     Running   0             61s

NAME                            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE
service/k8s-harbor-core         ClusterIP   10.107.255.58    <none>        80/TCP              62s
service/k8s-harbor-database     ClusterIP   10.110.140.93    <none>        5432/TCP            62s
service/k8s-harbor-jobservice   ClusterIP   10.105.143.127   <none>        80/TCP              62s
service/k8s-harbor-portal       ClusterIP   10.102.78.233    <none>        80/TCP              62s
service/k8s-harbor-redis        ClusterIP   10.97.73.152     <none>        6379/TCP            62s
service/k8s-harbor-registry     ClusterIP   10.103.127.124   <none>        5000/TCP,8080/TCP   62s
service/k8s-harbor-trivy        ClusterIP   10.96.127.87     <none>        8080/TCP            62s

NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/k8s-harbor-core         1/1     1            1           61s
deployment.apps/k8s-harbor-jobservice   1/1     1            1           61s
deployment.apps/k8s-harbor-portal       1/1     1            1           61s
deployment.apps/k8s-harbor-registry     1/1     1            1           61s

NAME                                               DESIRED   CURRENT   READY   AGE
replicaset.apps/k8s-harbor-core-6f48898655         1         1         1       61s
replicaset.apps/k8s-harbor-jobservice-6bc675757c   1         1         1       61s
replicaset.apps/k8s-harbor-portal-776c954b64       1         1         1       61s
replicaset.apps/k8s-harbor-registry-7dc7648b8c     1         1         1       61s

NAME                                   READY   AGE
statefulset.apps/k8s-harbor-database   1/1     61s
statefulset.apps/k8s-harbor-redis      1/1     61s
statefulset.apps/k8s-harbor-trivy      1/1     61s


$ kubectl get ingress -n harbor

NAME                 CLASS   HOSTS                   ADDRESS   PORTS     AGE
k8s-harbor-ingress   nginx   harbor.devopssong.com             80, 443   83s
  • harbor-core: Harbor의 핵심 서비스로, UI, API 요청 및 사용자 인증을 처리합니다.
  • harbor-database: Harbor의 데이터베이스(PostgreSQL)로, 사용자 정보 및 메타데이터를 저장합니다.
  • harbor-jobservice: 이미지 스캔, 복제와 같은 백그라운드 작업(작업 스케줄링)을 처리합니다.
  • harbor-portal: Harbor의 웹 사용자 인터페이스(UI)를 제공합니다.
  • harbor-redis: Harbor의 캐시 및 세션 관리를 위한 Redis 서비스를 제공합니다.
  • harbor-registry: Docker 레지스트리로서 이미지를 저장하고 제공하는 역할을 합니다.
  • harbor-trivy: 이미지 취약점 스캐너로, Harbor에 업로드된 이미지의 보안 스캔을 수행합니다.

6. Hosts 파일에 NGINX Ingress Controller의 LB Service IP와 구성한 도메인을 추가하면 웹에서 접속할 수 있습니다.

Harbor login page
Harbor main

초기 ID(admin)과 비밀번호(이 포스트의 경우 Harbor12345)로 로그인 할 수 있습니다.

3-2. NGINX Ingress Controller의 VirtualServer 구성을 통한 Harbor 연결

최초 배포 시 구성되는 Ingress 리소스를 사용할 수 있지만, NGINX Ingress Controller/NGINX Plus Ingress Controller와의 더 나은 통합 및 기능 사용을 위해 VirtualServer 리소스로 구성하겠습니다.
최초 구성 시 Ingress 리소스 구성은 아래와 같습니다.

k8s-harbor-ingress
apiVersion: v1
items:
- apiVersion: networking.k8s.io/v1
  kind: Ingress
  metadata:
    annotations:
      ingress.kubernetes.io/proxy-body-size: "0"
      ingress.kubernetes.io/ssl-redirect: "true"
      meta.helm.sh/release-name: k8s-harbor
      meta.helm.sh/release-namespace: harbor
      nginx.ingress.kubernetes.io/proxy-body-size: "0"
      nginx.ingress.kubernetes.io/ssl-redirect: "true"
    generation: 1
    labels:
      app: harbor
      app.kubernetes.io/instance: k8s-harbor
      app.kubernetes.io/managed-by: Helm
      app.kubernetes.io/name: harbor
      app.kubernetes.io/part-of: harbor
      app.kubernetes.io/version: 2.12.0
      chart: harbor
      heritage: Helm
      release: k8s-harbor
    name: k8s-harbor-ingress
    namespace: harbor
  spec:
    ingressClassName: nginx
    rules:
    - host: harbor.devopssong.com
      http:
        paths:
        - backend:
            service:
              name: k8s-harbor-core
              port:
                number: 80
          path: /api/
          pathType: Prefix
        - backend:
            service:
              name: k8s-harbor-core
              port:
                number: 80
          path: /service/
          pathType: Prefix
        - backend:
            service:
              name: k8s-harbor-core
              port:
                number: 80
          path: /v2/
          pathType: Prefix
        - backend:
            service:
              name: k8s-harbor-core
              port:
                number: 80
          path: /c/
          pathType: Prefix
        - backend:
            service:
              name: k8s-harbor-portal
              port:
                number: 80
          path: /
          pathType: Prefix
    tls:
    - hosts:
      - harbor.devopssong.com
      secretName: k8s-harbor-ingress

위 Ingres 리소스를 VirtualServer 리소스에 맞게 변환하면 아래와 같습니다.

harbor-vs.yaml
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
  name: harbor-vs
  namespace: harbor
  labels:
    app: harbor
    app.kubernetes.io/instance: k8s-harbor
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: harbor
    app.kubernetes.io/part-of: harbor
    app.kubernetes.io/version: 2.12.0
    chart: harbor
    heritage: Helm
    release: k8s-harbor
spec:
  host: harbor.devopssong.com
  tls:
    secret: k8s-harbor-ingress
    redirect:
      enable: true
  upstreams:
    - name: harbor-core
      service: k8s-harbor-core
      port: 80
      # body size의 제한을 해제합니다. 미설정 시 이미지 push에서 413 에러 발생
      client-max-body-size: 0m  
    - name: harbor-portal
      service: k8s-harbor-portal
      port: 80
      client-max-body-size: 0m
  routes:
    - path: /api/
      action:
        pass: harbor-core
    - path: /service/
      action:
        pass: harbor-core
    - path: /v2/
      action:
        pass: harbor-core
    - path: /c/
      action:
        pass: harbor-core
    - path: /
      action:
        pass: harbor-portal
$  kubectl apply -n harbor -f harbor-vs.yaml

virtualserver.k8s.nginx.org/harbor-vs created

$ kubectl get vs -n harbor

NAME        STATE   HOST                    IP    PORTS   AGE
harbor-vs   Valid   harbor.devopssong.com                 30s

4. Harbor 활용

Docker가 설치된 환경에서 Harbor로 이미지를 push 하도록 설정하는 방법과, Kubernetes 클러스터에서 Pod를 배포할 때 Harbor의 이미지를 pull 하여 배포하도록 설정하는 방법을 알아보겠습니다.

4-1. Harbor 컨테이너 레지스트리로 이미지 push

1. 웹에서 Harbor로 로그인하여, 프로젝트 메뉴에서 새 프로젝트 버튼을 클릭하여 프로젝트를 생성합니다.

Harbor 프로젝트
Harbor 프로젝트 생성

2. 생성한 프로젝트 우측에서 레지스트리 인증서(ca.crt)를 다운로드합니다. push 명령어도 확인할 수 있습니다.

Harbor 푸시 명령어

3. /etc/hosts 파일에 NGINX Ingress Controller의 LB Service IP와 구성한 도메인을 추가합니다.

175.***.***.*** harbor.devopssong.com

4. Docker가 설치된 환경에 아래와 같이 디렉터리를 생성하고, 다운받은 ca.crt 파일을 옮깁니다.
디렉터리 이름은 설정한 도메인과 동일하게 설정합니다.

$ sudo mkdir -p /etc/docker/certs.d/harbor.devopssong.com

$ sudo cp ca.crt /etc/docker/certs.d/harbor.devopssong.com

5. 명령어를 사용해 Harbor 레지스트리에 로그인합니다.

$ docker login harbor.devopssong.com
Username: admin
Password: Harbor12345

Login Succeeded

6. Harbor로 push 할 이미지의 태그를 변경하고, push 합니다.
명령어 예시의 REPOSITORY는 nginx-harbor로 구성했습니다.

$ docker tag nginx:latest harbor.devopssong.com/devopssong/nginx-harbor:latest

$ docker push harbor.devopssong.com/devopssong/nginx-harbor:latest

The push refers to repository [harbor.devopssong.com/devopssong/nginx-harbor]
61ef4e878aac: Pushed 
a0c145a29c8d: Pushed
a1fe8b721bb1: Pushed
19b722697f76: Pushed 
ffe4285e2906: Pushed 
7dca41ff1486: Pushed 
c3548211b826: Pushed 
latest: digest: sha256:2ebf3d369d813bcc6a531ba43e1859bd91ad5c8217ae33b821f5ffada06a6cd4 size: 1778

7. 이미지를 확인합니다.

4-2. Harbor의 이미지로 클러스터의 Pod 배포

Harbor에서 이미지를 pull 하기 위해 이전과 같이 admin 사용자가 아닌 봇 계정을 생성하여 권한을 설정 후 pull 하도록 구성할 수 있습니다.

1. 관리자 계정으로 로그인하여 프로젝트의 로봇 계정 탭에서 새 로봇 계정 버튼을 클릭합니다.

2. 봇 계정을 생성합니다.

클러스터의 Pod 배포를 위한 pull 권한만 부여했습니다.

3. 생성 후 나타나는 값을 복사해 둡니다.

4. 복사한 값을 사용하여 Docker가 설치된 환경에서 로그인합니다.
~/.docker/confing.json 파일이 생성됩니다.

$ docker login harbor.devopssong.com
Username: robot$devopssong+kuberentes
Password: sW2dzFf10ZDQBk1anFAFBlwaHdio84wW

Login Succeeded

5. Harbor의 이미지를 사용하는 Pod가 배포될 네임스페이스에 Secret을 생성합니다.
이전 과정에서 생성된 config.json 파일을 사용합니다.

test 네임스페이스에 harbor-bot-secret 이름으로 생성했습니다.

$ kubectl create secret generic harbor-bot-secret \
    --from-file=.dockerconfigjson=config.json \
    --type=kubernetes.io/dockerconfigjson
    -n test

secret/harbor-bot-secret created

6. 워커 노드의 /etc/hosts 파일에 NGINX Ingress Controller의 LB Service IP와 도메인을 추가합니다.
워커 노드 설정은 모든 워커 노드에 적용합니다. (특정 워커 노드에만 Pod를 배포하는 경우 제외)

175.***.***.*** harbor.devopssong.com

7. 워커 노드에 다음과 같이 도메인 이름으로 /etc/containerd 하위에 디렉터리를 생성하고, 4-1 섹션에서 다운받은 레지스트리 인증서를 생성한 디렉터리에 옮깁니다.

$ sudo mkdir -p /etc/containerd/certs.d/harbor.devopssong.com
$ sudo cp ca.crt /etc/containerd/certs.d/harbor.devopssong.com

8. 워커 노드의 Containerd 설정 파일(/etc/containerd/config.toml)을 편집합니다.

$ sudo vi /etc/containerd/config.toml

......

    [plugins."io.containerd.grpc.v1.cri".registry]
      config_path = "/etc/containerd/certs.d"

......

config_path 값을 /etc/containerd/certs.d로 설정합니다. (Containerd 1.x 버전 기준)

[plugins."io.containerd.cri.v1.images".registry]
   config_path = "/etc/containerd/certs.d"

Containerd 2.x 버전의 경우 위와 같은 위치에 설정을 적용합니다.
상세한 설정 방법은 Containerd 문서를 참고하세요.

9. Containerd를 재시작합니다.

$ sudo systemctl restart containerd

10. 생성한 Secret과 이미지를 사용하여 Pod를 Deployment로 배포합니다.

nginx-harbor.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx
  name: nginx-harbor
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      imagePullSecrets:
        - name: harbor-bot-secret
      containers:
      - image: harbor.devopssong.com/devopssong/nginx-harbor:latest
        name: nginx
$ kubectl apply -f nginx-harbor.yaml -n test

deployment.apps/nginx-harbor created

$ kubectl get po -n test

NAME                           READY   STATUS              RESTARTS   AGE
nginx-harbor-db56898c6-h6tpf   1/1     Running             0          3s

5. 결론

이번 포스트에서는 Kubernetes 클러스터에 Harbor를 배포하고, 배포한 Harbor를 활용하는 방법에 대해 알아봤습니다. 레지스트리 인증서를 사용하여 Harbor에 이미지를 push 하기 위한 구성과 Harbor의 이미지를 pull 해서 클러스터에 Pod를 배포하기 위해 필요한 구성을 알아봤습니다.

이처럼 클러스터 내부에 Harbor를 배포하면 내부에서 이미지를 Pull 하여 외부 네트워크를 거치지 않아 빠른 속도로 배포가 가능합니다. 또한 사용자별 레지스트리 권한 관리 및 이미지 push/pull을 통해 효율적이고 안전하게 클러스터의 이미지를 관리할 수 있습니다.

Kubernetes 클러스터의 진입점이 되는 NGINX Ingress Controller에 전용 대시보드, JWT 인증과 같은 추가적인 기능을 제공하는 NGINX Plus Ingress Controller를 체험해 보고 싶으시다면, NGINX STORE를 통해 문의해 NGINX의 상업용 구독을 무료로 체험해 보세요.

NGINX STORE를 통한 솔루션 도입 및 기술지원 무료 상담 신청

* indicates required