본문으로 건너뛰기
← Systems Notebook / Ops
MLOps Notes09 / 15

Harbor와 Helm 저장소: 이미지와 배포 선언을 구분하는 법

컨테이너 이미지와 Helm Chart의 저장 방식을 구분하고, Harbor와 GitLab 저장소를 Argo CD에 연결해 확인합니다.

이 글의 배경

HPE 엔지니어의 작업 자료를 바탕으로 정리한 글입니다. 개인 실험 기록이나 HPE 공식 문서와는 구분합니다. 본문의 환경과 버전은 글 작성 당시를 기준으로 합니다.

CI에서 컨테이너 이미지를 만든 뒤에는 이를 Git과 Harbor 중 어디에 저장할지 정해야 합니다. Helm Chart 역시 아카이브를 이미지와 같은 레지스트리에 저장할지, GitLab 프로젝트에 소스를 그대로 둘지 선택해야 합니다. 이 선택에 따라 Argo CD가 Harbor와 GitLab 중 어디에서 배포할 내용을 읽을지도 달라집니다.

“파일을 어디에 저장하는가”를 정하려면 먼저 “각 저장물이 무엇을 증명하는가”를 구분해야 합니다. 컨테이너 이미지는 실행할 바이트입니다. Helm Chart는 Kubernetes 리소스를 만들어 내는 템플릿이며, Git의 환경별 values는 현재 환경에 배포하기로 승인한 원하는 상태를 나타냅니다. 각 저장물의 역할과 수명 주기에 맞춰 저장소를 선택해야 합니다.

이 글에서 다루는 것

  • 컨테이너 이미지의 manifest, layer, tag, digest 관계
  • Docker Registry와 Harbor가 같은 말이 아닌 이유
  • Harbor의 Project, Robot Account, 스캔, 불변 태그, 복제와 Proxy Cache
  • Helm Chart의 version, appVersion, 이미지 태그를 구분하는 방법
  • “GitLab Helm Chart Repo”가 가리킬 수 있는 두 가지 방식
  • Git 차트 소스와 GitLab Helm Package Registry를 직접 구성하고 검증하는 방법
  • Argo CD가 각 방식의 Chart를 읽는 방법과 운영에서의 선택 기준

실습 환경과 치환값

항목 문서 예시 의미
GitLab gitlab.lab.example.com 소스와 Chart 프로젝트가 있는 GitLab
Harbor harbor.lab.example.com OCI 컨테이너 레지스트리
Harbor Project mlops-training 실습 이미지의 논리적 경계
Chart Git 프로젝트 platform/web-deploy Chart 소스와 환경별 values 저장소
GitLab Chart Project ID ${CHART_PROJECT_ID} Package Registry API가 사용할 숫자 ID
이미지 저장소 harbor.lab.example.com/mlops-training/web 애플리케이션 이미지 경로
예시 호스트 web.lab.example.com HTTPRoute가 사용할 문서용 도메인
Harbor 자격증명 ${HARBOR_ROBOT_USER}, ${HARBOR_ROBOT_PASSWORD} CI 전용 Robot Account
GitLab 패키지 자격증명 ${GITLAB_PACKAGE_USER}, ${GITLAB_PACKAGE_TOKEN} 읽기 전용 소비 자격증명

비밀번호와 토큰은 모두 환경변수 표기로만 사용합니다. 실제 값을 Markdown, Chart values, Git remote URL에 기록하지 않습니다.

컨테이너 이미지에는 무엇이 저장되는가

컨테이너 이미지는 여러 요소가 연결된 콘텐츠 주소형 객체입니다. 하나의 큰 압축 파일로 생각하기보다 다음 요소가 어떤 역할을 맡는지 살펴보면 저장 구조를 이해하기 쉽습니다.

  • Layer: 파일 시스템의 변경분입니다. 같은 layer는 여러 이미지가 공유할 수 있습니다.
  • Config: 실행 명령, 환경변수, 작업 디렉터리 같은 이미지 설정입니다.
  • Manifest: 어떤 config와 layer digest로 이미지가 구성되는지 기록합니다.
  • Image index: 여러 CPU 아키텍처용 manifest를 하나의 이름 아래 묶을 수 있습니다.
  • Digest: 콘텐츠로 계산한 sha256:... 식별자입니다. 콘텐츠가 같으면 digest도 같습니다.
  • Tag: 사람이 읽기 쉬운 가변 별칭입니다. 1.4.2, main, latest가 여기에 해당합니다.

harbor.lab.example.com/mlops-training/web:1.4.2에서 앞부분은 레지스트리와 저장소, 마지막 1.4.2는 tag입니다. tag는 다른 manifest를 가리키도록 다시 push할 수 있지만 digest는 콘텐츠 자체를 가리킵니다. 그래서 재현성이 중요한 운영 환경에서는 digest가 가장 강한 식별자이고, tag를 쓴다면 Harbor의 불변 규칙으로 덮어쓰기를 막아야 합니다.

Docker Registry와 Harbor의 차이

Registry는 OCI Distribution API를 제공해 artifact를 push하고 pull하는 서비스 범주입니다. CNCF Distribution의 Registry 구현은 이미지 저장과 전달, TLS, 기본 인증, 알림 같은 기본 기능을 제공합니다. Harbor도 이 표준 API를 제공하므로 docker, podman, buildctl, Kubernetes 같은 클라이언트에서 일반 Registry처럼 사용할 수 있습니다.

Harbor는 그 Registry 기능 위에 조직 운영에 필요한 관리 계층을 더한 플랫폼입니다.

요구사항 기본 Registry에서 직접 구성 Harbor에서 제공하는 표면
팀별 격리 별도 인증·인가 설계 Project와 역할 기반 접근 제어
CI 계정 외부 계정 체계 설계 Project/System Robot Account
취약점 확인 별도 Scanner와 UI 연동 Scanner 연동, push 시 스캔, 보안 허브
덮어쓰기 방지 운영 규칙 또는 외부 정책 Project 단위 Tag Immutability
보존·용량 스토리지 정책 직접 구성 Retention, Quota, Garbage Collection
여러 사이트 배포 복제 도구 별도 구축 Registry 간 Replication
외부 이미지 캐시 Pull-through cache 구성 Proxy Cache Project
감사 로그 수집 체계 별도 구성 Project 로그와 Audit Log 기능
서명 artifact 별도 저장·연결 Cosign/Notation 서명을 accessory로 연결

“Harbor가 필요한가?”를 판단할 때는 필요한 운영 기능을 살펴봐야 합니다. 이미지 push/pull만 필요한 개인 실습에는 작은 Registry도 충분합니다. 팀과 환경의 접근 범위를 구분하고 취약점·보존·복제·감사를 관리해야 하는 운영 환경에서는 Harbor가 이러한 기능을 제공하므로 직접 구현할 작업이 줄어듭니다.

Harbor, Nexus, GitLab Package Registry는 서로 대체재인가

일부 기능이 겹치지만 주 역할이 다릅니다.

저장소 이 시리즈에서의 주 역할 대표 소비자
Harbor 컨테이너 이미지와 OCI artifact BuildKit, containerd, Kubernetes
Nexus Repository Maven, PyPI, npm 같은 의존성 proxy/group 빌드 도구와 패키지 관리자
GitLab Helm Package Registry 패키징된 Helm Chart .tgz 배포 Helm, Argo CD
GitLab Git Repository Chart 소스와 환경별 원하는 상태 개발자, CI, Argo CD

의존성을 받는 저장소와 완성된 이미지를 보관하는 저장소는 별도로 선택할 수 있습니다. 따라서 Nexus에서 Python 패키지를 받아도 컨테이너 이미지까지 Nexus에 저장할 필요는 없습니다. 이 구성에서는 Nexus가 의존성 수급과 캐시를, Harbor가 실행 이미지를, GitLab이 배포 선언을 담당합니다.

Helm Chart의 세 가지 버전을 분리해서 생각하기

Chart.yaml에는 version과 appVersion이 있고, values.yaml에는 다시 이미지 tag가 있습니다. 같은 숫자로 맞출 수는 있지만 의미는 다릅니다.

yaml
apiVersion: v2
name: web
type: application
version: 0.3.0
appVersion: "1.4.2"
yaml
image:
  repository: harbor.lab.example.com/mlops-training/web
  tag: "6d8f47c0b1a6b61c..."
  • version: Chart 패키지와 템플릿 구조의 버전이며 SemVer를 사용합니다.
  • appVersion: 사람이 참고하는 애플리케이션 버전 메타데이터입니다. Helm이 배포를 결정하는 값은 아닙니다.
  • image.tag: Pod가 실제로 pull할 이미지 참조입니다.
  • Git commit: Chart와 values가 왜 바뀌었는지 설명하는 감사 단위입니다.

가장 흔한 오류는 Chart version만 올리고 이미지 tag는 그대로 두거나, appVersion만 바꾸면 Deployment 이미지도 바뀐다고 생각하는 것입니다. 실제 템플릿이 .Values.image.tag를 참조한다면 그 값이 변경되어야 새 이미지가 배포됩니다.

“GitLab Helm Chart Repo”의 두 가지 의미

방식 A: Git에 Chart 소스를 저장

text
web-deploy/
└── charts/
    └── web/
        ├── Chart.yaml
        ├── values.yaml
        ├── values-dev.yaml
        ├── values-stg.yaml
        ├── values-prod.yaml
        └── templates/

Argo CD는 Git 저장소의 charts/web 경로를 pull하고 내부적으로 Helm 템플릿을 렌더링합니다. Chart와 환경값이 commit에 함께 고정되므로 Git revert로 원하는 상태를 되돌리기 쉽습니다. 이 시리즈의 end-to-end 실습은 이 방식을 사용합니다.

방식 B: GitLab Helm Package Registry에 .tgz 발행

CI가 helm package로 Chart archive를 만들고 GitLab API의 channel에 업로드합니다. Argo CD는 Git이 아니라 Helm repository URL, chart 이름, chart version으로 읽습니다. 다른 팀이 재사용할 Chart를 배포하기에는 편하지만, 실제 환경이 어느 패키지 버전을 쓸지는 별도 Git 선언에 고정해야 합니다.

비교 Git Chart 소스 GitLab Helm Package Registry
저장 형태 Chart.yaml, templates, values web-0.3.0.tgz
Argo CD source repoURL + targetRevision + path repoURL + chart + targetRevision
변경 검토 일반 Git diff와 MR 패키지 생성 전 소스 MR
환경별 원하는 상태 자연스럽게 함께 저장 별도 release/Application Git 필요
배포 단위 재사용 저장소 구조에 의존 버전화된 패키지로 명확
주의점 자동화 계정의 Git 쓰기 권한 같은 이름·버전 중복 업로드 정책

GitLab의 Helm Package Registry는 같은 chart 이름과 version의 중복 업로드를 허용하며, 조회할 때 가장 최근 업로드를 반환합니다. 따라서 0.3.0을 다시 업로드하지 못하게 CI와 권한 정책으로 막고 매 빌드마다 새 version을 사용해야 합니다. Package Registry에 넣었다는 사실만으로 불변성이 보장되지는 않습니다.

전체 아키텍처

단계별 흐름 / 01이미지와 Helm Chart의 저장 경로
앱 소스GitLab
GitLab CI빌드와 배포 선언 갱신
Harborimage: commit SHA
01 / 05
이미지 생성과 보관

GitLab CI는 rootless 방식으로 이미지를 빌드하고 commit SHA로 식별해 Harbor에 push합니다.

1

앱 소스 → GitLab CICI 실행

2

GitLab CI → Harborrootless build·push

단계를 선택하면 자동 재생이 멈춥니다. 선의 번호와 아래 설명을 함께 읽어 주세요.

전체 단계 한눈에 읽기
  1. 이미지 생성과 보관

    GitLab CI는 rootless 방식으로 이미지를 빌드하고 commit SHA로 식별해 Harbor에 push합니다.

    • 앱 소스 → GitLab CI: CI 실행
    • GitLab CI → Harbor: rootless build·push
  2. 권장 경로는 Git Chart

    CI가 GitLab Git Chart 저장소의 values에 image tag를 갱신합니다.

    • GitLab CI → Git Chart 저장소: values의 image tag 갱신
  3. Package Registry는 대안

    helm package로 Helm Package Registry를 사용할 수도 있습니다. Git Chart와 반드시 함께 거쳐야 하는 단계는 아닙니다.

    • GitLab CI → Helm Package Registry: 선택: helm package
    • Helm Package Registry → Argo CD: package source 대안
  4. 선언을 렌더하고 동기화

    권장 Git Chart 경로에서 Argo CD가 Git을 읽고 Helm을 렌더해 Kubernetes에 반영합니다.

    • Git Chart 저장소 → Argo CD: Git + Helm 렌더
    • Argo CD → Kubernetes: 선언 동기화
  5. 이미지는 Harbor에서 pull

    클러스터가 필요한 이미지를 Harbor에 요청합니다. Chart 소스와 실행 이미지의 저장 위치는 서로 다릅니다.

    • Kubernetes → Harbor: 이미지 pull 요청
도식 원문
flowchart LR
    src["GitLab 앱 소스"] --> ci["GitLab CI"]
    ci -->|rootless build·push| harbor["Harbor<br/>image: commit SHA"]
    ci -->|values의 image tag 갱신| gitChart["GitLab Git Chart 저장소<br/>운영 권장"]
    ci -. 선택: helm package .-> pkg["GitLab Helm Package Registry"]
    gitChart -->|Git + Helm 렌더| argo["Argo CD"]
    pkg -. package source 대안 .-> argo
    argo --> cluster["Kubernetes"]
    cluster -. pull .-> harbor

그림의 점선 경로는 선택할 수 있는 대안입니다. 같은 애플리케이션을 배포할 때는 Git Chart source와 packaged Chart 중 어느 쪽을 기준으로 삼을지 하나로 정해야 합니다. 다른 쪽은 artifact를 배포하거나 재사용하는 목적으로만 사용합니다.

실습 1: Harbor Project와 CI용 Robot Account 준비

Harbor 관리 화면에서 다음 순서로 준비합니다.

  1. Projects → New Project에서 private project mlops-training을 만듭니다.
  2. Project의 Robot Accounts에서 CI 전용 계정을 만듭니다.
  3. 해당 Project에 필요한 Pull Repository, Push Repository 권한만 부여합니다.
  4. 만료 기간을 설정하고 생성 직후 secret을 안전한 비밀 관리 시스템에 보관합니다. Harbor는 생성 후 같은 secret을 다시 보여 주지 않습니다.
  5. Configuration에서 push 시 자동 스캔을 켭니다.
  6. Tag Immutability에서 릴리스 tag를 덮어쓰지 못하게 규칙을 만듭니다.

GitLab 앱 프로젝트의 Settings → CI/CD → Variables에는 다음 두 값을 등록합니다.

Key 값 권장 속성
HARBOR_ROBOT_USER 생성된 Robot Account 이름 Masked, Protected
HARBOR_ROBOT_PASSWORD 생성 시 한 번 받은 secret Masked and hidden, Protected

Robot Account 이름에는 특수문자가 포함될 수 있으므로 shell에서는 항상 "$HARBOR_ROBOT_USER"처럼 인용합니다.

실습 2: GitLab에 Chart 소스 저장소 만들기

빈 private 프로젝트 platform/web-deploy를 만들고 clone합니다.

bash
git clone https://gitlab.lab.example.com/platform/web-deploy.git
cd web-deploy
mkdir -p charts
helm create charts/web

생성된 charts/web/Chart.yaml을 다음처럼 정리합니다.

yaml
apiVersion: v2
name: web
description: Example web application chart
type: application
version: 0.1.0
appVersion: "1.0.0"

charts/web/values.yaml에서 핵심 값을 다음처럼 둡니다. helm create가 생성한 다른 기본값은 유지해도 됩니다.

yaml
replicaCount: 1

image:
  repository: harbor.lab.example.com/mlops-training/web
  pullPolicy: IfNotPresent
  tag: "REPLACED_BY_CI"

imagePullSecrets:
  - name: harbor-pull

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false

gateway:
  enabled: true
  name: platform-gateway
  namespace: nginx-gateway
  sectionName: https
  hostname: web.lab.example.com

Helm 4의 helm create가 templates/httproute.yaml을 이미 만들었다면 그 내용을 다음으로 교체합니다. 파일이 없는 Chart에서만 새로 추가합니다. 기본 httpRoute values와 이 글의 gateway values는 구조가 다르므로 두 템플릿을 함께 남기지 않습니다.

yaml
{{- if .Values.gateway.enabled }}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: {{ include "web.fullname" . }}
  labels:
    {{- include "web.labels" . | nindent 4 }}
spec:
  parentRefs:
    - name: {{ .Values.gateway.name }}
      namespace: {{ .Values.gateway.namespace }}
      sectionName: {{ .Values.gateway.sectionName }}
  hostnames:
    - {{ .Values.gateway.hostname | quote }}
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: {{ include "web.fullname" . }}
          port: {{ .Values.service.port }}
{{- end }}

환경별 override 파일을 만듭니다.

charts/web/values-dev.yaml

yaml
replicaCount: 1
image:
  tag: "REPLACED_BY_CI"
gateway:
  hostname: web-dev.lab.example.com

charts/web/values-prod.yaml

yaml
replicaCount: 3
image:
  tag: "APPROVED_IMMUTABLE_TAG"
gateway:
  hostname: web.lab.example.com

공유 Gateway가 다른 namespace의 HTTPRoute를 받으려면 Gateway listener의 allowedRoutes.namespaces 정책이 해당 애플리케이션 namespace를 허용해야 합니다. HTTPRoute만 만들고 Gateway 정책을 확인하지 않으면 Route의 Accepted 조건이 False가 될 수 있습니다.

로컬에서 Chart 검증

bash
helm lint charts/web -f charts/web/values-dev.yaml
helm template web charts/web -f charts/web/values-dev.yaml > rendered.yaml
grep -nE 'kind: (Deployment|Service|HTTPRoute)' rendered.yaml

예상 결과에는 세 종류의 리소스가 모두 나타나야 합니다.

text
kind: Service
kind: Deployment
kind: HTTPRoute

렌더 결과에 실제 비밀번호나 token이 들어 있지 않은지도 확인한 뒤 push합니다.

bash
git add charts/web
git commit -m "feat(web): add initial Helm chart"
git branch -M main
git push -u origin main

실습 3: Chart를 GitLab Package Registry에도 발행하기

이 단계는 Package Registry 방식을 비교하려는 선택 실습입니다. web-deploy 프로젝트 루트에 다음 .gitlab-ci.yml을 추가합니다.

yaml
stages:
  - validate
  - package
  - publish

variables:
  CHART_DIR: charts/web
  CHART_VERSION: "0.1.${CI_PIPELINE_IID}"

lint-chart:
  stage: validate
  image: alpine:3.21
  before_script:
    - apk add --no-cache helm
  script:
    - helm lint "$CHART_DIR" -f "$CHART_DIR/values-dev.yaml"
    - helm template web "$CHART_DIR" -f "$CHART_DIR/values-dev.yaml" > /dev/null

package-chart:
  stage: package
  image: alpine:3.21
  needs:
    - lint-chart
  before_script:
    - apk add --no-cache helm
  script:
    - mkdir -p dist
    - helm package "$CHART_DIR" --version "$CHART_VERSION" --destination dist
  artifacts:
    paths:
      - dist/*.tgz
    expire_in: 1 day

publish-chart:
  stage: publish
  image: alpine:3.21
  needs:
    - package-chart
  before_script:
    - apk add --no-cache curl
  script:
    - >-
      curl --fail-with-body --request POST
      --user "gitlab-ci-token:${CI_JOB_TOKEN}"
      --form "chart=@dist/web-${CHART_VERSION}.tgz"
      "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/api/stable/charts"
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

CI_PIPELINE_IID를 patch version에 넣었기 때문에 같은 프로젝트 안에서는 매 pipeline이 새 Chart version을 만듭니다. 업로드는 같은 프로젝트의 Package Registry이므로 장기 Personal Access Token 대신 Job 실행 중에만 유효한 ${CI_JOB_TOKEN}을 사용합니다.

bash
git add .gitlab-ci.yml
git commit -m "ci: validate and publish Helm package"
git push

Pipeline이 성공하면 Deploy → Package Registry에서 web Chart와 0.1.<pipeline IID> version을 확인합니다.

Argo CD 연결 방식

먼저 이 글의 두 source만 허용하는 전용 AppProject를 만듭니다. 앞 글에서 만든 이름이 비슷한 Project를 재사용하면 source allowlist가 달라 동기화가 거부될 수 있습니다.

yaml
# argocd/web-artifacts-project.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: web-artifacts
  namespace: argocd
spec:
  sourceRepos:
    - https://gitlab.lab.example.com/platform/web-deploy.git
    - https://gitlab.lab.example.com/api/v4/projects/*/packages/helm/stable
  destinations:
    - server: https://kubernetes.default.svc
      namespace: web-dev
  namespaceResourceWhitelist:
    - group: apps
      kind: Deployment
    - group: ""
      kind: Service
    - group: gateway.networking.k8s.io
      kind: HTTPRoute

Package Registry URL의 wildcard 범위가 조직 정책에 비해 넓다면 실제 ${CHART_PROJECT_ID}를 반영한 정확한 URL로 더 좁힙니다.

bash
kubectl apply -f argocd/web-artifacts-project.yaml

권장: Git Chart 소스를 직접 읽기

yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-dev
  namespace: argocd
spec:
  project: web-artifacts
  source:
    repoURL: https://gitlab.lab.example.com/platform/web-deploy.git
    targetRevision: main
    path: charts/web
    helm:
      releaseName: web
      valueFiles:
        - values-dev.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: web-dev
  syncPolicy:
    managedNamespaceMetadata:
      labels:
        gateway-access: "true"
    automated:
      enabled: true
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

GitLab HTTPS URL은 .git 접미사를 포함합니다. 일부 GitLab 구성은 접미사가 없는 주소를 redirect하는데 Argo CD의 저장소 연결이 이 redirect를 따라가지 않아 연결 오류가 날 수 있습니다.

대안: GitLab Helm Package Registry를 읽기

Argo CD에는 Helm repository 자격증명을 별도 Secret으로 등록합니다. 아래는 구조를 설명하기 위한 템플릿이며, 실제 Secret manifest를 Git에 평문으로 commit하지 않습니다.

yaml
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-web-helm
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  type: helm
  name: gitlab-web
  url: https://gitlab.lab.example.com/api/v4/projects/${CHART_PROJECT_ID}/packages/helm/stable
  username: ${GITLAB_PACKAGE_USER}
  password: ${GITLAB_PACKAGE_TOKEN}

Application은 path 대신 chart를 사용하고 Chart version을 명시합니다.

yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-dev-package
  namespace: argocd
spec:
  project: web-artifacts
  source:
    repoURL: https://gitlab.lab.example.com/api/v4/projects/${CHART_PROJECT_ID}/packages/helm/stable
    chart: web
    targetRevision: 0.1.42
    helm:
      valuesObject:
        image:
          repository: harbor.lab.example.com/mlops-training/web
          tag: "6d8f47c0b1a6b61c"
  destination:
    server: https://kubernetes.default.svc
    namespace: web-dev
  syncPolicy:
    managedNamespaceMetadata:
      labels:
        gateway-access: "true"
    automated:
      enabled: true
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

운영에서는 targetRevision: "*"처럼 최신 Chart를 자동 추종하지 않습니다. Application version과 이미지 참조를 Git에 고정하고 MR로 바꿔야 “어떤 승인으로 무엇이 배포됐는가”를 재구성할 수 있습니다.

검증

Harbor

  • mlops-training Project가 private인지 확인합니다.
  • CI Robot Account에 다른 Project 권한이 없는지 확인합니다.
  • push된 artifact 상세 화면에서 digest와 scan 결과를 확인합니다.
  • 같은 릴리스 tag를 다시 push했을 때 불변 규칙이 거부하는지 별도 테스트 Project에서 검증합니다.

Git Chart 소스

bash
helm lint charts/web -f charts/web/values-prod.yaml
helm template web charts/web -f charts/web/values-prod.yaml | grep 'image:'
git log --oneline -- charts/web

렌더된 image:가 승인한 Harbor 경로와 불변 tag를 가리켜야 합니다.

GitLab Helm Package Registry

bash
helm repo add gitlab-web \
  --username "$GITLAB_PACKAGE_USER" \
  --password "$GITLAB_PACKAGE_TOKEN" \
  "https://gitlab.lab.example.com/api/v4/projects/${CHART_PROJECT_ID}/packages/helm/stable"
helm repo update
helm search repo gitlab-web/web --versions

예상 결과에 pipeline에서 만든 0.1.<IID> version이 나타나야 합니다.

자주 발생하는 문제

Harbor push가 401 Unauthorized 또는 denied로 끝난다

Project가 먼저 생성됐는지, Robot Account 이름 전체를 사용했는지, Push Repository 권한이 있는지 확인합니다. 비밀번호 끝의 공백이나 줄바꿈도 인증 실패를 만듭니다. CI 로그에 인증 JSON이나 비밀번호를 출력하지 않습니다.

이미지는 있는데 Kubernetes가 ImagePullBackOff다

애플리케이션 namespace에 imagePullSecret이 없거나 ServiceAccount가 그것을 참조하지 않을 수 있습니다. Harbor Project가 private인지, Secret의 registry host가 정확히 harbor.lab.example.com인지, 노드가 Harbor 인증서 체인을 신뢰하는지도 확인합니다.

Helm Package 업로드 직후 검색되지 않는다

Pipeline의 curl --fail-with-body 응답과 Package Registry 화면을 확인합니다. 서버 측 처리가 끝나기 전에 조회했을 수도 있습니다. Chart.yaml의 version이 유효한 SemVer인지도 확인합니다.

같은 Chart version인데 내용이 달라졌다

GitLab Helm Package Registry는 중복 version을 허용합니다. CI에서 version을 단조 증가시키고, 이미 발행한 version을 다시 만들지 못하게 release 규칙과 권한을 설계합니다. 소비자는 범위가 아니라 명시적 version을 고정합니다.

Argo CD가 Git 저장소에 연결하지 못한다

private GitLab 프로젝트에는 read_repository 범위의 Deploy Token 같은 읽기 전용 자격증명이 필요합니다. URL에 .git을 붙이고, 사설 CA를 사용한다면 인증서 검증을 끄는 대신 argocd-tls-certs-cm에 CA 체인을 등록합니다.

HTTPRoute가 생성됐지만 트래픽이 오지 않는다

다음 명령으로 Route 조건을 확인합니다.

bash
kubectl -n web-dev describe httproute web

Accepted, ResolvedRefs 조건이 True인지 확인합니다. parent Gateway 이름·namespace·listener sectionName, hostname DNS, Gateway listener의 allowedRoutes가 흔한 실패 지점입니다.

보안과 운영 원칙

  • Harbor Project는 기본적으로 private으로 만들고 CI에는 project-scoped Robot Account를 사용합니다.
  • Robot Account secret은 만료와 회전 절차를 갖추고 GitLab에서 Masked and hidden, Protected 변수로 관리합니다.
  • 릴리스 tag는 Harbor Immutability Rule로 보호합니다. 캐시용 tag처럼 의도적으로 이동해야 하는 tag는 규칙을 분리합니다.
  • push 시 스캔만 믿지 말고 배포 허용 임계값과 예외 승인 절차를 정합니다.
  • 서명은 이미지와 같은 Harbor에 보관할 수 있지만, 검증을 강제할 정책 지점도 별도로 설계합니다.
  • GitLab Helm Package Registry의 중복 version 특성을 고려해 재발행을 금지합니다.
  • Chart values에는 비밀번호를 넣지 않습니다. SOPS, External Secrets Operator 같은 별도 비밀 전달 방식을 사용합니다.
  • 사설 CA를 쓰더라도 TLS 검증을 끄지 말고 Runner, BuildKit, Kubernetes 노드, Argo CD에 신뢰 체인을 배포합니다.

요약

Registry는 OCI artifact를 push하고 pull하는 표준 서비스입니다. Harbor는 여기에 Project RBAC, Robot Account, 스캔, 보존, 불변성, 복제와 감사 기능을 더한 운영 플랫폼입니다. Helm Chart는 Kubernetes 선언을 생성하는 템플릿이므로 컨테이너 이미지와 구분해야 합니다. Chart version과 실제 이미지 tag도 별도로 관리해야 합니다.

GitLab에서 Chart를 관리할 때는 Git 소스를 직접 저장하거나 Package Registry에 .tgz를 발행할 수 있습니다. 환경별 원하는 상태와 승격 이력을 함께 검토하려면 Git Chart 소스를 사용하는 방식이 적합합니다. 다른 곳에서 재사용할 패키지를 배포해야 한다면 Package Registry를 함께 사용할 수 있습니다. 이 경우에도 환경이 선택한 정확한 Chart와 이미지 version은 Git에 고정해야 합니다.

공식 참고자료