스터디(Study)/CI·CD Study

GitOps Cookbook 5장: 헬름

Chann._.y 2025. 10. 25.
728x90

5.0 들어가며

Helm은 쿠버네티스 애플리케이션을 “차트(chart)”라는 단위로 패키징하고, 템플릿 렌더링, 배포, 업그레이드, 롤백을 일관되게 다룰 수 있게 해주는 도구다. Helm은 kustomize처럼 매니페스트를 조합/변경할 수 있지만, 동시에 apt/yum 같은 패키지 관리자처럼 버전 있는 배포 아티팩트로 다룬다는 점이 다르다.

운영 환경 기준에서 Helm 차트는 단순한 YAML 묶음이 아니라 배포 단위, 롤백 단위, 그리고 조직 표준(보안 컨텍스트, 라벨 정책 등)을 코드화해 강제하는 수단이다. Helm을 잘 설계하면 애플리케이션 팀과 플랫폼/보안 팀의 경계를 명확하게 만들 수 있다.


5.1 헬름 프로젝트 생성

Helm 차트란 무엇인가

Helm 차트는 애플리케이션을 배포하는 데 필요한 쿠버네티스 리소스(Deployment, Service, ConfigMap 등)와 그 리소스를 렌더링하기 위한 템플릿, 그리고 템플릿에 주입될 기본값 집합(values)을 하나로 묶은 패키지다.

의견: Helm 차트는 “우리 서비스가 어떻게 쿠버네티스에 올라가야 하는가”를 코드화한 표준 배포 단위다. 이걸 팀마다 따로 만드는 게 아니라 조직적으로 공통 베이스 차트를 잡고 서비스들이 그걸 상속/확장하게 만들면, 보안/규제/모니터링 같은 비機능 요구사항을 중앙에서 통제할 수 있다.

로컬 클러스터 준비 (kind)

로컬이나 CI 파이프라인에서 반복적으로 배포 테스트를 하려면 독립된 쿠버네티스 환경이 필요하다. kind는 Docker 컨테이너 기반 쿠버네티스 클러스터라서 테스트에 적합하다.

kind create cluster --name myk8s --image kindest/node:v1.32.8 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
EOF

위 설정은 NodePort 일부를 호스트 포트로 맵핑해준다. 즉, 클러스터 안의 서비스를 로컬 브라우저나 curl로 바로 확인할 수 있다. QA나 디버깅에 유리하다.

차트 디렉터리 구성

mkdir pacman
mkdir pacman/templates
cd pacman

핵심 파일은 세 가지다.

  1. Chart.yaml
  2. values.yaml
  3. templates/*.yaml

헬름 요소 간 관계

Chart.yaml

apiVersion: v2
name: pacman
description: A Helm chart for Pacman
type: application
version: 0.1.0        # 차트 자체 버전(템플릿/구조가 바뀌면 증가)
appVersion: "1.0.0"   # 실제 애플리케이션 버전(컨테이너 이미지 버전 등과 연결)

version(차트 버전)과 appVersion(앱 버전)은 역할이 다르다.

  • version: 배포 템플릿 자체의 버전. 운영/보안 정책 변경 등에도 올라간다.
  • appVersion: 실제 서비스 코드/이미지 버전.

의견: 이 분리는 DevOps 팀과 애플리케이션 팀 사이의 책임 경계를 만들 수 있다. 예를 들어 DevOps 팀이 securityContext나 라벨 정책을 강화할 때는 차트 version만 올리면 된다. 반대로 앱 팀이 신규 애플리케이션 릴리스를 내보낼 때는 appVersion과 이미지 태그만 바꿔도 된다.

values.yaml

image:
  repository: quay.io/gitops-cookbook/pacman-kikd
  tag: "1.0.0"
  pullPolicy: Always
  containerPort: 8080

replicaCount: 1
securityContext: {}

values.yaml은 템플릿에서 .Values로 참조할 기본값들을 모아둔 곳이다. 환경별(개발/스테이징/운영) 차이는 결국 values 레이어에서 커버하게 된다.

templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}
  labels:
    app.kubernetes.io/name: {{ .Chart.Name }}
    {{- if .Chart.AppVersion }}
    app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
    {{- end }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Chart.Name }}
    spec:
      containers:
        - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 14 }}
          name: {{ .Chart.Name }}
          ports:
            - containerPort: {{ .Values.image.containerPort }}
              name: http
              protocol: TCP

중요 포인트:

  • .Values로 values.yaml 값을 템플릿에 주입한다.
  • image.tag | default .Chart.AppVersion 구조로 두면, 별도 태그를 안 줘도 appVersion이 이미지 태그로 사용된다.
  • toYaml과 nindent는 복잡한 YAML 블록(securityContext 등)을 깨끗하게 들여쓰기 해서 주입할 때 필수다.

templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: {{ .Chart.Name }}
  name: {{ .Chart.Name }}
spec:
  ports:
    - name: http
      port: {{ .Values.image.containerPort }}
      targetPort: {{ .Values.image.containerPort }}
  selector:
    app.kubernetes.io/name: {{ .Chart.Name }}

템플릿 결과 미리 보기

실제 배포 전에 Helm이 생성할 최종 YAML을 로컬에서 확인할 수 있다.

helm template .

값 오버라이드 시뮬레이션도 가능하다.

helm template --set replicaCount=3 .

의견: CI 단계에서 helm template 결과를 정책 엔진(OPA/Gatekeeper, Kyverno 등)에 넣어 사전 검증하면, 배포 전에 규정 위반 리소스(예: privileged=true 컨테이너)가 걸러지기 때문에 운영 안정성이 올라간다.


5.2 템플릿 간 코드 공유

여러 템플릿(Deployment, Service 등)에서 동일한 라벨/selector 블록을 반복해서 적다 보면 유지보수가 어려워진다. 레이블 표준이 바뀌면 파일마다 다 고쳐야 하는 상황이 나온다.

Helm은 _helpers.tpl 파일을 통해 재사용 가능한 템플릿 조각을 정의하고, include로 주입할 수 있게 해준다.

예: selector 라벨 공통화

templates/_helpers.tpl:

{{- define "pacman.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
{{- end }}

이제 Deployment/Service 템플릿에서 다음처럼 사용한다.

selector:
  matchLabels:
    {{- include "pacman.selectorLabels" . | nindent 6 }}
template:
  metadata:
    labels:
      {{- include "pacman.selectorLabels" . | nindent 8 }}
# Service에서도 동일하게
selector:
  {{- include "pacman.selectorLabels" . | nindent 6 }}

만약 라벨 정책을 바꾸고 싶으면 _helpers.tpl만 수정하면 된다. 예를 들어 버전 라벨까지 모든 리소스에 공통으로 포함시키고 싶다면:

{{- define "pacman.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
{{- end }}

의견: _helpers.tpl은 DevOps/플랫폼 팀의 무기다. 여기서 공통 라벨(서비스 오너, 비용 청구 태그, 환경 구분 등), 공통 어노테이션(모니터링/메트릭 스크레이프용), 공통 securityContext 등을 정의해 두면, 애플리케이션 팀이 Helm 차트를 복사/확장하더라도 중요한 표준이 유지된다.


5.3 컨테이너 이미지 갱신

실제 운영에서는 컨테이너 이미지를 새 버전으로 교체하고(release), 문제가 있으면 되돌리고(rollback), 환경별 값을 다르게 적용하는 작업이 반복된다. Helm은 이 사이클을 표준화한다.

설치

helm install pacman .

설치가 끝나면 Helm은 “릴리스(release)” 단위로 이 배포를 추적한다. 예를 들어 릴리스 이름이 pacman이라면 helm list로 상태를 볼 수 있다.

Helm은 릴리스의 전체 스펙(매니페스트, values 등)을 쿠버네티스 Secret sh.helm.release.v1.<릴리스명>.v<revision> 형태로 저장한다. 이 Secret은 롤백 시점 복구에 활용된다. Helm 아키텍처는 릴리스 메타데이터를 쿠버네티스 리소스로 남겨서 이력과 복구 포인트를 확보한다는 점을 공식적으로 설명한다.

helm list
kubectl get secret | grep pacman
kubectl get deploy,pod,svc,ep

업그레이드 (이미지 버전 교체)

컨테이너 이미지를 1.0.0 → 1.1.0으로 업데이트한다고 하자. 우선 values.yaml의 이미지 태그 또는 Chart.yaml의 appVersion을 바꾼다.

# values.yaml 일부
image:
  repository: quay.io/gitops-cookbook/pacman-kikd
  tag: "1.1.0"
  pullPolicy: Always
  containerPort: 8080
# Chart.yaml 일부
version: 0.1.0
appVersion: "1.1.0"

그리고:

helm upgrade pacman .

확인:

helm history pacman
kubectl get deploy,replicaset -owide
kubectl get secret | grep pacman

쿠버네티스는 새로운 ReplicaSet을 만들고 기존 Pod를 롤링 방식으로 교체한다. 다운타임 없이 점진적으로 rollout이 진행된다.

롤백

업그레이드 후 에러가 났다면 이전 revision으로 즉시 롤백할 수 있다.

helm history pacman
helm rollback pacman 1

의견: 롤백 속도가 빠르다는 점은 Helm 배포 방식의 큰 장점이다. 야간 장애나 신규 릴리스 직후 오류에 대응할 때 “git revert+빌드+재배포” 대신 “helm rollback”으로 수 분 내 복구가 가능하다.

values 오버라이드로 환경 분리

같은 차트를 여러 환경에 배포할 때는 values 파일만 다르게 준다. 예: newvalues.yaml에서 이미지 태그만 오버라이드:

image:
  tag: "1.2.0"

렌더링만 먼저 확인:

helm template pacman -f newvalues.yaml .

운영에서는 helm upgrade pacman . -f values-prod.yaml처럼 환경 전용 values 파일을 사용한다. 이렇게 하면 “한 차트/여러 환경” 구조가 유지된다.


5.4 헬름 차트 패키징 및 배포

Helm 차트가 어느 정도 안정되면 팀 내부 또는 외부로 공유할 수 있어야 한다. 대표적인 방식은 두 가지다.

  1. 전통적인 Helm 차트 저장소(Chart Repository)
  2. OCI 호환 레지스트리(컨테이너 레지스트리 재활용)

전통적 Helm 차트 저장소

Helm은 차트를 .tgz 아카이브로 패키징할 수 있다.

helm package .
# pacman-0.1.0.tgz 생성

그리고 helm repo index 명령을 사용하면 현재 디렉터리를 차트 저장소(repo) 형태로 만들 수 있다. 이 디렉터리에는 index.yaml(메타데이터)와 .tgz 차트 파일이 들어간다. 이 구조를 HTTP로 서비스하면 다른 사용자가 helm repo add로 사용할 수 있다. Helm 공식 문서는 이 저장소를 단순한 HTTP 서버/S3/GCS 등으로 제공할 수 있다고 설명한다.

helm repo index .
cat index.yaml

의견: 이 방식은 내부 전용 차트 레포(프라이빗 Helm repo)를 쉽게 만들 수 있다는 장점이 있다. 단점은 Helm 전용 repo를 별도 관리해야 한다는 점이다.

OCI (컨테이너 레지스트리 기반) 방식

Helm 3.x에서는 차트를 OCI 아티팩트로 다룰 수 있다. 즉, Helm 차트를 Docker 이미지와 같은 컨테이너 레지스트리에 push/pull할 수 있다. Helm 문서에 따르면 v3.8 이후 OCI 지원은 기본이며, Docker Hub, Harbor, GHCR, ECR, GCR 등에서 차트를 OCI 아티팩트로 취급할 수 있다.

예: Bitnami nginx 차트는 OCI 주소(oci://registry-1.docker.io/bitnamicharts/nginx)로 바로 pull/install이 가능하다. Helm은 helm pull, helm install oci://... --version ... 같은 식으로 OCI 레지스트리를 직접 사용할 수 있다.

의견: OCI 방식의 장점은 컨테이너 이미지와 차트를 동일한 경로(동일 레지스트리)에서 다룰 수 있다는 점이다.

  • 별도 Helm repo 인프라가 필요 없다.
  • 인증/권한/감사 로깅을 기존 레지스트리 정책 그대로 쓴다.
  • CI/CD에서 “이미지 빌드 → 레지스트리 push → 동일 레지스트리에 차트 push”라는 단일 공급망(supply chain) 경로를 만들 수 있다.
    보안·컴플라이언스·추적성 측면에서 유리하다.

Bitnami 공개 카탈로그 변화

Bitnami는 오랫동안 Postgres, Redis, Nginx 등 OSS 스택의 컨테이너 이미지와 Helm 차트를 안정적으로 제공해왔다. 하지만 Broadcom 편입 이후 Bitnami는 ‘Bitnami Secure Images’(하드닝된, 상업 지원 가능한 이미지/차트) 모델을 밀고 있으며, 기존의 무료/공개 카탈로그 일부는 legacy로 전환되어 업데이트가 제한되고 있다.

요점:

  • Bitnami Secure Images는 CVE 대응, 하드닝, 장기 지원 같은 가치를 전면에 둔 상업 모델이다. Broadcom은 이를 공식적으로 발표하며, 기존 무료 배포 모델에서의 지원 축소를 언급했다.
  • 기존 무료 이미지/차트는 더 이상 지속적으로 최신 보안 패치를 제공하지 않을 수 있다. 커뮤니티 이슈에서도 “이제는 기존 태그가 사실상 frozen 상태”라는 우려가 공유되고 있다.

의견: 조직 입장에서 의미하는 건 간단하다.
(1) 외부 공개 차트/이미지를 그대로 프로덕션에 두는 전략은 점점 더 위험해진다.
(2) 내부 Harbor/GHCR 등 OCI 레지스트리에 공인된 차트/이미지 버전을 미러링하고, 그걸 통해서만 배포하도록 표준화할 필요가 있다.
(3) Helm 차트 공급망(“이 차트는 어디서 왔는가? 누가 검증했는가?”)을 관리 대상으로 올려야 한다.


5.5 저장소에 보관된 차트 배포

이미 만들어진 차트를 외부/내부 저장소에서 가져와 그대로 배포할 수도 있다. 대표 예로 Bitnami의 PostgreSQL 차트가 자주 등장한다. 이 차트는 StatefulSet, PVC, Secret, Service 등 데이터베이스 운영에 필요한 모든 쿠버네티스 리소스를 이미 패키징해둔다.

일반적인 사용 흐름:

# 저장소 등록
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo list

# 차트 검색
helm search repo postgresql
helm search repo postgresql -o json | jq

PostgreSQL 차트를 배포할 때는 DB 이름, 유저명, 패스워드 등 주요 값을 --set으로 전달한다. 예:

helm install my-db \
  --set postgresql.postgresqlUsername=my-default,\
postgresql.postgresqlPassword=postgres,\
postgresql.postgresqlDatabase=mydb,\
postgresql.persistence.enabled=false \
  bitnami/postgresql

상태 확인:

helm list
kubectl get sts,pod,svc,ep,secret

helm show values bitnami/postgresql로 차트의 기본값을 살펴보면 어떤 값들을 override할 수 있는지 명확하게 알 수 있다.

의견: 외부 차트를 바로 쓰는 건 빠르지만, 앞 절에서 언급한 것처럼 장기적으로는 리스크(업데이트 중단, 라이선스, 보안 책임)가 생긴다. 프로덕션이라면 사내에서 신뢰 가능한 사본(내부 레지스트리 기반 OCI 차트, 내부 Helm repo)을 관리하고, 그걸 기반으로 배포하는 체계가 필요하다.


5.6 의존성을 가진 차트 배포

Helm 차트는 다른 차트를 의존성(dependency)으로 선언할 수 있다. 예를 들어 어떤 애플리케이션이 PostgreSQL을 필요로 한다면, 그 애플리케이션 차트의 Chart.yaml에 PostgreSQL 차트를 의존성으로 추가할 수 있다.

예를 들어 music 서비스가 있고, 이 서비스는 PostgreSQL과 연결되어 동작한다고 하자.

Chart.yaml:

apiVersion: v2
name: music
description: A Helm chart for Music service
type: application
version: 0.1.0
appVersion: "1.0.0"
dependencies:
  - name: postgresql
    version: 18.0.17
    repository: "https://charts.bitnami.com/bitnami"

helm dependency update를 실행하면 charts/ 디렉터리에 의존 차트(PostgreSQL)가 패키지 형태로 내려온다.

helm dependency update
tree
# charts/postgresql-18.0.17.tgz 등 생성

values.yaml에는 애플리케이션 이미지 정보와 DB 접속 정보가 들어간다. 그 값은 templates/deployment.yaml에서 환경 변수(env)나 Secret 참조로 주입한다. 예:

env:
  - name: QUARKUS_DATASOURCE_JDBC_URL
    value: {{ .Values.postgresql.server | default (printf "%s-postgresql" ( .Release.Name )) | quote }}
  - name: QUARKUS_DATASOURCE_USERNAME
    value: {{ .Values.postgresql.postgresqlUsername | default (printf "postgres" ) | quote }}
  - name: QUARKUS_DATASOURCE_PASSWORD
    valueFrom:
      secretKeyRef:
        name: {{ .Values.postgresql.secretName | default (printf "%s-postgresql" ( .Release.Name )) | quote }}
        key: {{ .Values.postgresql.secretKey }}

실제 배포는 한 번으로 끝난다.

helm install music-db .
kubectl get sts,pod,svc,ep,secret,pv,pvc

의견: 이 패턴은 마이크로서비스 구조에서 특히 강력하다.

  • 애플리케이션 팀은 “우리 서비스” 차트만 관리한다.
  • DB나 캐시, 메시지 브로커 같은 인프라 구성 요소는 DevOps/플랫폼 팀이 관리하는 검증된 차트를 의존성으로 끌어다 쓴다.
  • 결국 인프라 스택을 표준 재사용 가능 단위로 만든다는 의미다.

5.7 자동 롤링 업데이트

쿠버네티스 기본 동작을 그대로 두면 ConfigMap이 바뀌어도 기존 Pod는 자동으로 재시작되지 않는다. 즉 설정을 변경했는데 애플리케이션이 여전히 이전 설정으로 동작할 수 있다.

Helm 차트 쪽에서 이 문제를 해결하는 표준 패턴이 있다.

핵심 아이디어:

  1. ConfigMap 템플릿 내용을 SHA-256으로 해싱한다.
  2. 그 해시 값을 Deployment의 Pod 템플릿 metadata.annotations에 주입한다.
  3. ConfigMap이 바뀌면 해시가 바뀌고, Deployment 템플릿도 바뀐 것으로 인식되어 쿠버네티스가 자동으로 롤링 업데이트를 수행한다.

예 (Deployment 템플릿 일부):

spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Chart.Name }}
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
    spec:
      containers:
      ...

여기서 include (print $.Template.BasePath "/configmap.yaml") . | sha256sum는 해당 차트의 configmap.yaml 템플릿 내용을 렌더링하고 그 결과를 sha256으로 계산한다. ConfigMap 내용이 조금이라도 바뀌면 checksum이 달라지고, 그에 따라 새로운 ReplicaSet이 생성되며 Pod가 순차적으로 교체된다.

의견: 이 패턴은 설정 변경이 즉각 서비스에 반영돼야 하는 API 서버류, 피처 플래그 기반 서비스, 외부 엔드포인트 주소가 자주 바뀌는 서비스에 특히 중요하다. 반대로 stateful 워크로드(캐시, 브로커 등)는 롤링 업데이트 그 자체가 리스크일 수도 있으므로, 모든 워크로드에 무조건 적용하지 말고 등급화할 필요가 있다.

추가로 stakater의 Reloader 같은 컨트롤러를 설치하면 ConfigMap/Secret 변경을 감지해 관련 Deployment/StatefulSet을 자동으로 재시작(롤아웃)해준다. 이 방식은 애플리케이션 템플릿을 직접 수정할 수 없는 서드파티 차트나 레거시 컴포넌트에도 적용 가능하다는 장점이 있다. stakater는 이 툴을 “ConfigMap/Secret 변경 시 자동 롤링” 솔루션으로 소개하고 있다.
의견: 조직 정책 차원에서 “이 서비스의 ConfigMap이 바뀌면 즉시 롤링”이라는 규칙을 선언할 수도 있고, “이 서비스는 수동 재배포 승인 필요”로 둘 수도 있다. 이 기준선을 명문화해두면 운영 중 혼란이 줄어든다.


5.8 맺으며

이 장에서 다룬 내용을 DevOps 엔지니어 관점에서 요약하면 다음과 같다.

  1. Helm 차트는 단순한 YAML 묶음이 아니라, 재현 가능한 배포 아티팩트다. 차트(version)와 앱(appVersion) 버전을 분리해두면 애플리케이션 팀과 플랫폼 팀의 책임이 분리된다.
  2. values 레이어를 통해 환경별 차이를 관리한다. 공통 차트 + 환경 전용 values 파일(prod, stage, dev 등) 조합은 운영 표준이 될 수 있다. 민감 값(비밀번호 등)은 Secret 관리 전략과 함께 설계해야 한다.
  3. _helpers.tpl은 조직 표준(라벨, 어노테이션, 보안 컨텍스트 등)을 강제하는 핵심 수단이다. 운영/보안/관측 요구사항을 여기서 일괄 관리할 수 있다.
  4. helm upgrade, helm rollback은 릴리스 안정성의 핵심이다. Helm은 릴리스 메타데이터를 Secret 형태로 클러스터에 저장하며, 그 이력을 통해 신속한 롤백이 가능하도록 설계되어 있다.
  5. Helm 차트 유통 방식은 전통적 repo(index.yaml + tgz)에서 OCI 기반 레지스트리 방식으로 넘어가고 있다. OCI 방식은 컨테이너 레지스트리의 인증·권한·감사 체계를 그대로 활용할 수 있고, CI/CD와 공급망 보안을 단일 경로에서 관리하기 좋다.
  6. Bitnami 등 서드파티 차트/이미지의 라이선스와 제공 정책이 바뀌고 있다. Broadcom은 ‘Bitnami Secure Images’로 하드닝된 유료 이미지를 강조하면서 기존 무료 카탈로그 일부를 legacy로 돌리고 있다. 이는 “외부 차트/이미지를 그냥 쓰면 된다” 시대가 끝나고, 내부 레지스트리 미러링·검증·버전 고정이 운영 표준이 되어야 한다는 신호다.
  7. ConfigMap 변경 자동 반영(자동 롤링 업데이트) 패턴은 실제 서비스 운영에서 매우 자주 쓰이는 기법이다. Helm 템플릿에 checksum 어노테이션을 넣는 방식과, Reloader 같은 컨트롤러 기반 방식 모두 존재한다. 서비스 특성별로 어느 쪽을 쓸지 표준화해둘 필요가 있다.

의견: 결국 Helm은 “쿠버네티스 리소스 선언 도구”가 아니라 “조직의 배포 계약서”다. 차트를 어떻게 설계하느냐가 곧 운영 성숙도다. 차트 구조, values 전략, 릴리스/롤백 프로세스, 의존성 관리, 이미지/차트 서플라이 체인, ConfigMap 반영 정책까지 포함해서 Helm 차트를 조직 표준으로 격상시키는 것이 DevOps 팀의 역할이다.



728x90

댓글