Skip to content
ioob.dev
Go back

Kubernetes 입문 12편 — Helm과 패키지 관리

· 9분 읽기
Kubernetes 시리즈 (12/12)
  1. Kubernetes 입문 1편 — Kubernetes란
  2. Kubernetes 입문 2편 — 클러스터 구조
  3. Kubernetes 입문 3편 — Pod
  4. Kubernetes 입문 4편 — 컨트롤러
  5. Kubernetes 입문 5편 — 서비스와 네트워킹
  6. Kubernetes 입문 6편 — Ingress와 Gateway API
  7. Kubernetes 입문 7편 — ConfigMap과 Secret
  8. Kubernetes 입문 8편 — 스토리지: PV, PVC, StorageClass
  9. Kubernetes 입문 9편 — 리소스 관리와 오토스케일링
  10. Kubernetes 입문 10편 — RBAC과 보안: 최소 권한의 원칙
  11. Kubernetes 입문 11편 — 관측성: 로그, 메트릭, 추적
  12. Kubernetes 입문 12편 — Helm과 패키지 관리
Table of contents

Table of contents

YAML이 너무 많다

여기까지 오면서 우리는 수많은 YAML을 만들었다. Deployment, Service, ConfigMap, Secret, Ingress, PVC, HPA, NetworkPolicy. 서비스 하나 띄우는 데 파일 열 개가 우습지 않다. 네임스페이스마다, 환경(dev/stage/prod)마다 이걸 복제해가며 값을 조금씩 바꾸는 건 지속 가능하지 않다.

예전에는 sed로 치환하거나 Kustomize로 패치하거나 각자 방식을 찾았다. 시간이 지나면서 쿠버네티스 생태계에서 사실상 표준이 된 패키징 도구가 Helm이다. 하나의 애플리케이션을 구성하는 여러 쿠버네티스 리소스를 Chart라는 단위로 묶고, values.yaml로 환경별 설정을 분리하는 구조다.

flowchart LR
    A[Chart<br/>템플릿 + 기본값] --> C[helm install/upgrade]
    B[values.yaml<br/>환경별 설정] --> C
    C --> D[렌더링된<br/>YAML]
    D --> E[쿠버네티스<br/>클러스터]

한 문장으로 요약하면 Helm은 쿠버네티스의 apt, yum, brew다. 파라미터화된 템플릿에 값을 주입해서 실제 매니페스트를 만들어내고, 그 상태를 “Release”로 관리한다.

설치와 첫 릴리즈

먼저 Helm CLI를 설치한다.

# macOS
brew install helm

# Linux
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

공식 저장소를 추가하고 간단한 차트를 설치해보자. 가장 많이 쓰이는 bitnami/nginx 차트로 첫 릴리즈를 만든다.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# 릴리즈 이름 "my-nginx"로 설치
helm install my-nginx bitnami/nginx --namespace web --create-namespace

# 설치된 릴리즈 확인
helm list -n web
# NAME       NAMESPACE  REVISION  STATUS    CHART        APP VERSION
# my-nginx   web        1         deployed  nginx-18.x.x 1.27.x

이제 쿠버네티스에 Nginx 관련 리소스가 올라갔다. Deployment, Service, ServiceAccount, ConfigMap 등 차트가 정의한 모든 리소스가 한 번에 생성된다.

# 어떤 리소스가 만들어졌는지
kubectl get all -n web

# 렌더링된 실제 매니페스트 (디버깅용)
helm get manifest my-nginx -n web

값을 바꿔서 업그레이드해보자.

helm upgrade my-nginx bitnami/nginx \
  --namespace web \
  --set replicaCount=3 \
  --set service.type=LoadBalancer

# 릴리즈 이력
helm history my-nginx -n web
# REVISION  UPDATED       STATUS      CHART         DESCRIPTION
# 1         Apr 20 01:22  superseded  nginx-18.x.x  Install complete
# 2         Apr 20 01:24  deployed    nginx-18.x.x  Upgrade complete

문제가 생기면 이전 버전으로 돌릴 수 있다.

helm rollback my-nginx 1 -n web

Helm은 각 릴리즈의 상태를 쿠버네티스 Secret으로 저장해둔다. 그래서 helm history가 말해주는 이력은 실제로 클러스터 안에 보관되어 있는 기록이다.

Chart의 내부 구조

helm create 명령으로 빈 차트를 만들어보면 구조가 한눈에 들어온다.

helm create myapp
tree myapp
myapp/
├── Chart.yaml          # 차트 메타데이터
├── values.yaml         # 기본값
├── charts/             # 의존성 서브차트들이 들어가는 곳
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── serviceaccount.yaml
│   ├── hpa.yaml
│   ├── _helpers.tpl    # 템플릿 함수/헬퍼
│   ├── NOTES.txt       # 설치 후 출력될 메시지
│   └── tests/          # helm test용
└── .helmignore

가장 먼저 볼 파일이 Chart.yaml이다. 차트의 정체성을 담는다.

apiVersion: v2
name: myapp
description: My application
type: application
version: 0.1.0         # 차트 자체 버전 (SemVer)
appVersion: "1.0.0"    # 앱 버전 (문자열, 참고용)

dependencies:
  - name: postgresql
    version: "15.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled

version은 차트 구조가 바뀔 때 올리고, appVersion은 포함된 애플리케이션의 버전이다. 둘을 분리한 이유가 있다. 같은 앱 버전이어도 차트 구조(예: 템플릿 변경)는 바뀔 수 있기 때문이다.

dependencies로 다른 차트를 서브차트로 묶을 수 있다. condition을 걸면 값으로 on/off도 가능하다.

templates와 Go 템플릿 문법

templates/ 안의 파일이 Helm의 핵심이다. 평범한 YAML처럼 보이지만, Go 템플릿 문법이 섞여 있어서 값을 주입하고 조건/반복을 만들 수 있다.

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.service.port }}
          {{- if .Values.resources }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          {{- end }}
          {{- with .Values.env }}
          env:
            {{- range . }}
            - name: {{ .name }}
              value: {{ .value | quote }}
            {{- end }}
          {{- end }}

문법이 처음엔 낯설지만 몇 가지 패턴만 알면 된다.

그리고 _helpers.tpl에는 자주 쓰는 표현을 함수처럼 정의한다.

# _helpers.tpl
{{- define "myapp.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{- define "myapp.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

helm create가 만들어주는 기본 헬퍼가 이미 꽤 쓸만해서, 처음 차트를 만들 때는 이걸 그대로 쓰면서 필요한 리소스만 추가하는 식으로 시작하면 된다.

values.yaml — 환경별 설정의 허브

values.yaml은 템플릿이 참조하는 기본값이다. 사용자는 이 값을 재정의해서 환경마다 다른 설정을 적용한다.

# values.yaml
replicaCount: 1

image:
  repository: myapp
  tag: ""
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 8080

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

env:
  - name: LOG_LEVEL
    value: info

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 10
  targetCPU: 70

ingress:
  enabled: false
  className: nginx
  hosts:
    - host: myapp.local
      paths:
        - path: /
          pathType: Prefix

환경별로 다른 값을 쓰고 싶으면 별도 파일로 관리한다.

# values-prod.yaml
replicaCount: 5

image:
  tag: "1.2.3"

resources:
  requests:
    cpu: 500m
    memory: 1Gi
  limits:
    cpu: 2
    memory: 4Gi

env:
  - name: LOG_LEVEL
    value: warn

autoscaling:
  enabled: true
  minReplicas: 5
  maxReplicas: 50

ingress:
  enabled: true
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix

설치할 때 -f로 파일을 지정하거나 --set으로 개별 값을 덮어쓴다.

# 프로덕션 배포
helm upgrade --install myapp ./myapp \
  -n production --create-namespace \
  -f values-prod.yaml

# 특정 값만 오버라이드
helm upgrade --install myapp ./myapp \
  -n production \
  -f values-prod.yaml \
  --set image.tag=1.2.4

여러 -f를 주면 뒤에 오는 파일이 앞 파일을 덮어쓴다. --set은 그보다도 더 우선순위가 높다. 이 우선순위를 이용해서 공통 → 환경별 → 배포별 순으로 값을 쌓아올리는 패턴을 많이 쓴다.

렌더링 미리 보기와 디버깅

값을 적용한 결과를 실제 설치하기 전에 미리 볼 수 있다.

# 최종 렌더링 결과를 stdout으로
helm template myapp ./myapp -f values-prod.yaml

# 설치하지 않고 드라이런
helm install myapp ./myapp -f values-prod.yaml --dry-run --debug

# 문법 검증
helm lint ./myapp

템플릿에 실수가 있으면 --debug가 자세한 에러 위치를 보여준다. 새 차트를 개발할 때는 helm template으로 출력을 반복해서 확인하면서 고치는 게 효율적이다.

릴리즈의 생명주기

Helm이 “Release”라 부르는 건 차트를 실제 클러스터에 설치한 결과 인스턴스다. 같은 차트를 다른 이름으로 여러 번 설치해서 각각 독립적으로 관리할 수 있다.

# 같은 차트를 두 번 설치
helm install myapp-a ./myapp -f values-a.yaml -n tenant-a
helm install myapp-b ./myapp -f values-b.yaml -n tenant-b

# 모든 릴리즈
helm list -A

# 특정 릴리즈 삭제
helm uninstall myapp-a -n tenant-a

# 릴리즈 이력 전부 지우지 않고 보관하려면
helm uninstall myapp-a -n tenant-a --keep-history

Helm은 릴리즈 상태를 쿠버네티스 Secret(sh.helm.release.v1.<name>.<rev>)에 저장한다. 그래서 등록된 상태와 클러스터 상태가 꼬일 수 있는데, helm get all <release>로 Helm이 인식하는 상태를 확인할 수 있다.

차트를 만들어 공유하기

만든 차트를 팀이나 외부와 공유하려면 패키징하고 저장소에 올린다.

# 차트를 tar.gz으로 패키징
helm package ./myapp
# myapp-0.1.0.tgz 생성

# 저장소 인덱스 만들기
helm repo index . --url https://charts.example.com
# index.yaml 생성

생성된 .tgzindex.yaml을 HTTP로 접근 가능한 곳(S3, GitHub Pages, 내부 웹서버)에 올리면 사설 저장소가 된다. 요즘은 OCI 레지스트리(ECR, GHCR 등)에 직접 푸시하는 방식도 표준화되어 널리 쓰인다.

# OCI 레지스트리에 푸시
helm push myapp-0.1.0.tgz oci://registry.example.com/charts

# OCI 레지스트리에서 설치
helm install myapp oci://registry.example.com/charts/myapp --version 0.1.0

저장소에 올려두면 helm upgrade --install을 CI에서 돌려 배포 파이프라인에 쉽게 녹일 수 있다.

공식 차트 저장소 활용

실무에서 자주 쓰이는 애플리케이션은 대부분 잘 관리되는 공식 차트가 있다. 바닥부터 만들지 말고 이걸 활용하자.

영역대표 차트
관측성prometheus-community/kube-prometheus-stack, grafana/loki, grafana/tempo
인그레스ingress-nginx/ingress-nginx, traefik/traefik
인증서jetstack/cert-manager
DBbitnami/postgresql, bitnami/redis, bitnami/mongodb
메시지 큐bitnami/kafka, bitnami/rabbitmq
시크릿external-secrets/external-secrets
GitOpsargo/argo-cd

이런 차트들은 수많은 운영 사례가 녹아 있어서, 옵션을 적절히 맞춰 쓰면 프로덕션 레디 상태로 빠르게 갈 수 있다. 단, 블랙박스로 믿고 쓰지 말고 helm template으로 어떤 리소스가 만들어지는지 한 번은 훑어봐야 한다. 기본값이 내 환경에 안 맞는 경우가 의외로 많다.

GitOps와 Helm

ArgoCD 시리즈에서 다룬 것처럼, GitOps 환경에서는 helm install을 직접 돌리지 않는다. Application CR에 차트 레포와 values를 참조하도록 해두고, ArgoCD가 알아서 렌더링하고 싱크한다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://charts.example.com
    chart: myapp
    targetRevision: 0.1.0
    helm:
      valueFiles:
        - values-prod.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

이 패턴이 되면 helm CLI를 직접 칠 일이 줄어든다. Helm은 템플릿 엔진과 패키지 포맷으로 역할이 옮겨가고, 실제 배포는 GitOps 컨트롤러가 담당한다.

Helm의 대안들

Helm만 있는 건 아니다. 각자 장단점이 있어서 상황에 따라 고른다.

초심자라면 Helm + Kustomize 조합이 가장 흔하고 자료도 많다. 공식 차트를 Helm으로 쓰고, 내부 매니페스트는 Kustomize로 관리하는 식이 실무에서 자주 보인다.

시리즈를 마치며

12편에 걸쳐 쿠버네티스의 주요 개념을 훑었다. 돌이켜보면 이런 여정이었다.

  1. Pod와 컨테이너 — 가장 작은 배포 단위
  2. Deployment와 ReplicaSet — 선언적 롤아웃
  3. Service와 네트워킹 — Pod를 엔드포인트로 묶기
  4. Ingress — 외부 트래픽의 게이트웨이
  5. 네임스페이스와 라벨 — 리소스를 조직하는 축
  6. StatefulSet과 DaemonSet — 특수한 워크로드
  7. ConfigMap과 Secret — 설정과 민감 정보 분리
  8. PV/PVC와 스토리지 — 영속 데이터
  9. 리소스 관리와 오토스케일링 — requests/limits/HPA
  10. RBAC과 보안 — 최소 권한 원칙
  11. 관측성 — 로그/메트릭/추적
  12. Helm과 패키지 관리 — 모든 걸 하나로 묶기

처음 쿠버네티스를 배울 때는 개념이 많아서 어지럽게 느껴진다. 그런데 실제로 몇 개의 서비스를 배포하고, 장애를 겪고, 스케일 아웃을 경험하다 보면 왜 각 리소스가 필요한지가 몸으로 이해된다.

다음 단계로 추천하는 것들을 몇 가지 적어둔다.

쿠버네티스는 넓고 깊은 생태계다. 이 시리즈는 그 중 입구까지만 다뤘다. 여기서부터는 각자의 서비스 특성에 맞는 깊이로 확장해 나가면 된다.


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Kubernetes 입문 11편 — 관측성: 로그, 메트릭, 추적
Next Post
Terraform 1편 — Terraform이란