Table of contents
- YAML이 너무 많다
- 설치와 첫 릴리즈
- Chart의 내부 구조
- templates와 Go 템플릿 문법
- values.yaml — 환경별 설정의 허브
- 렌더링 미리 보기와 디버깅
- 릴리즈의 생명주기
- 차트를 만들어 공유하기
- 공식 차트 저장소 활용
- GitOps와 Helm
- Helm의 대안들
- 시리즈를 마치며
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 }}
문법이 처음엔 낯설지만 몇 가지 패턴만 알면 된다.
{{ .Values.x }}:values.yaml의 값 참조{{ .Chart.Name }}:Chart.yaml의 필드 참조{{- }}: 앞의 공백/줄바꿈 제거 (YAML 들여쓰기 맞추기용){{ include "..." . }}: 재사용 가능한 템플릿 호출 (_helpers.tpl에 정의){{- if .x }} ... {{- end }}: 조건 블록{{- range .x }} ... {{- end }}: 반복| default "foo": 값이 없으면 기본값| toYaml | nindent N: 객체를 YAML로 직렬화하면서 N만큼 들여쓰기
그리고 _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 생성
생성된 .tgz와 index.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 |
| DB | bitnami/postgresql, bitnami/redis, bitnami/mongodb |
| 메시지 큐 | bitnami/kafka, bitnami/rabbitmq |
| 시크릿 | external-secrets/external-secrets |
| GitOps | argo/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만 있는 건 아니다. 각자 장단점이 있어서 상황에 따라 고른다.
- Kustomize: 템플릿 없이 YAML을 오버레이로 패치.
kubectl내장이라 설치 불필요, 하지만 복잡한 로직은 표현이 어렵다 - Jsonnet / Kapitan: 프로그래밍 언어처럼 YAML 생성. 유연하지만 학습 곡선이 가파르다
- CUE / Timoni: 타입이 있는 구성 언어. 검증력이 강하지만 생태계가 작다
- Pulumi / CDK8s: 일반 프로그래밍 언어(Go, TS, Python)로 쿠버네티스 리소스 선언
초심자라면 Helm + Kustomize 조합이 가장 흔하고 자료도 많다. 공식 차트를 Helm으로 쓰고, 내부 매니페스트는 Kustomize로 관리하는 식이 실무에서 자주 보인다.
시리즈를 마치며
12편에 걸쳐 쿠버네티스의 주요 개념을 훑었다. 돌이켜보면 이런 여정이었다.
- Pod와 컨테이너 — 가장 작은 배포 단위
- Deployment와 ReplicaSet — 선언적 롤아웃
- Service와 네트워킹 — Pod를 엔드포인트로 묶기
- Ingress — 외부 트래픽의 게이트웨이
- 네임스페이스와 라벨 — 리소스를 조직하는 축
- StatefulSet과 DaemonSet — 특수한 워크로드
- ConfigMap과 Secret — 설정과 민감 정보 분리
- PV/PVC와 스토리지 — 영속 데이터
- 리소스 관리와 오토스케일링 — requests/limits/HPA
- RBAC과 보안 — 최소 권한 원칙
- 관측성 — 로그/메트릭/추적
- Helm과 패키지 관리 — 모든 걸 하나로 묶기
처음 쿠버네티스를 배울 때는 개념이 많아서 어지럽게 느껴진다. 그런데 실제로 몇 개의 서비스를 배포하고, 장애를 겪고, 스케일 아웃을 경험하다 보면 왜 각 리소스가 필요한지가 몸으로 이해된다.
다음 단계로 추천하는 것들을 몇 가지 적어둔다.
- Operator 패턴: CRD와 컨트롤러로 도메인 개념을 쿠버네티스에 넣는 방법. Kubebuilder, Operator SDK
- 서비스 메시: Istio, Linkerd. 트래픽 제어, mTLS, 세밀한 관측성
- 정책 엔진: OPA/Gatekeeper, Kyverno. Admission 단계의 정책 강제
- 클러스터 운영: kubeadm, kOps로 직접 클러스터 구축. 매니지드만 쓰다가 이걸 한 번 해보면 내부가 선명해진다
- Chaos Engineering: Chaos Mesh, LitmusChaos. 의도적으로 실패를 주입해서 복원력 검증
쿠버네티스는 넓고 깊은 생태계다. 이 시리즈는 그 중 입구까지만 다뤘다. 여기서부터는 각자의 서비스 특성에 맞는 깊이로 확장해 나가면 된다.




Loading comments...