[CS300 #229] 헬름 — 쿠버네티스 매니페스트의 패키지 관리자
컴퓨터공학 300 주제 시리즈의 229번째 글이다. 전체 지도는 여기.
한 줄 요약
헬름은 Go 템플릿으로 쓴 매니페스트 묶음(차트)에 값(values)을 넣어 렌더링하고, 그 결과를 하나의 “릴리스”로 설치·업그레이드·롤백하는 쿠버네티스 패키지 관리자다.
왜 필요한가
웹 애플리케이션 하나를 쿠버네티스에 올리려면 디플로이먼트, 서비스, 인그레스, 컨피그맵, 시크릿, 서비스어카운트 등 매니페스트가 여러 개 필요하다. 이것을 개발·스테이징·운영 세 환경에 배포하면 대부분은 같고 이미지 태그, 레플리카 수, 도메인, 자원 한도만 다르다. 파일을 세 벌 복사하면 한쪽만 고치는 실수가 반드시 생긴다.
또 남이 만든 소프트웨어(데이터베이스, 모니터링 스택, 인그레스 컨트롤러)를 설치할 때 매니페스트 수십 개를 직접 관리하고 싶은 사람은 없다. 헬름은 이 둘을 해결한다. 공통 부분은 템플릿으로, 다른 부분은 값으로 분리하고, 설치 단위를 버전으로 관리한다.
핵심 개념
세 가지 용어
| 용어 | 뜻 |
|---|---|
| 차트(chart) | 쿠버네티스 리소스를 만드는 데 필요한 템플릿과 메타데이터의 묶음 |
| 저장소(repository) | 차트를 모아 두고 배포하는 곳. OCI 레지스트리에 차트를 올릴 수도 있다 |
| 릴리스(release) | 차트를 클러스터에 설치한 하나의 인스턴스. 같은 차트를 이름만 달리해 여러 번 설치할 수 있다 |
차트 구조
mychart/
Chart.yaml # 이름, 버전(version), 앱 버전(appVersion), 의존 차트
values.yaml # 기본값
values.schema.json # (선택) 값 검증 스키마
templates/ # Go 템플릿으로 쓴 매니페스트
deployment.yaml
service.yaml
_helpers.tpl # 재사용 템플릿 조각(밑줄로 시작하면 매니페스트로 렌더링 안 됨)
NOTES.txt # 설치 후 출력되는 안내문
charts/ # 의존 차트
Chart.yaml 의 version 은 차트 자체의 버전(SemVer 2)이고, appVersion 은 차트가 배포하는 애플리케이션의 버전이다. 둘은 독립적이다. 템플릿만 고쳐도 version 을 올린다.
템플릿과 값
템플릿은 Go 의 text/template 문법에 Sprig 함수 등을 더한 것이다.
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
# values.yaml
replicaCount: 2
image:
repository: nginx
tag: ""
resources: {}
자주 쓰는 내장 객체는 .Values(값), .Release(릴리스 이름·네임스페이스 등), .Chart(Chart.yaml 내용), .Capabilities(클러스터 버전·지원 API) 다.
공백 제어도 알아 둔다. {{- 는 앞쪽 공백·줄바꿈을, -}} 는 뒤쪽을 지운다. YAML 은 들여쓰기가 문법이므로, toYaml 결과를 nindent 로 정확한 깊이에 맞추는 일이 템플릿 작성의 상당 부분을 차지한다.
값의 우선순위
값은 여러 곳에서 들어와 병합된다. 뒤의 것이 앞의 것을 덮어쓴다.
- 차트의
values.yaml - 부모 차트의 values (의존 차트인 경우)
-f/--values로 준 파일(여러 개면 뒤 파일이 우선)--set,--set-string등으로 준 개별 값
맵은 깊게 병합되지만 리스트는 통째로 교체된다. 기본값에 컨테이너 포트 두 개가 있고 오버라이드 파일에 하나만 적으면 결과는 하나다.
릴리스 수명주기
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install web ./mychart -n web --create-namespace -f values-prod.yaml
helm upgrade web ./mychart -n web -f values-prod.yaml --set image.tag=1.2.3
helm history web -n web # 리비전 목록
helm rollback web 3 -n web # 리비전 3으로
helm uninstall web -n web
helm template web ./mychart -f values-prod.yaml # 클러스터 없이 렌더링만
헬름 3 은 릴리스 정보를 해당 네임스페이스의 시크릿으로 저장한다. 업그레이드할 때마다 리비전이 하나씩 늘고, 롤백도 새 리비전으로 기록된다. helm upgrade --install 은 릴리스가 없으면 설치, 있으면 업그레이드라서 CI 에서 많이 쓴다.
훅(hook)
helm.sh/hook 어노테이션을 단 리소스는 설치·업그레이드 전후 같은 특정 시점에 실행된다. DB 마이그레이션 잡을 pre-upgrade 훅으로 거는 것이 대표적이다. 훅 리소스는 일반 릴리스 리소스와 수명 관리가 다르므로 삭제 정책(helm.sh/hook-delete-policy)을 함께 정한다.
직접 해 보기
헬름의 값 병합 규칙(맵은 깊게, 리스트는 교체)과 간단한 렌더링을 파이썬으로 재현해 보자.
import copy, re
def deep_merge(base, override):
out = copy.deepcopy(base)
for k, v in override.items():
if isinstance(v, dict) and isinstance(out.get(k), dict):
out[k] = deep_merge(out[k], v)
else:
out[k] = copy.deepcopy(v) # 스칼라·리스트는 통째로 교체
return out
def set_flag(values, expr): # --set a.b.c=v
path, val = expr.split("=", 1)
keys, node = path.split("."), values
for k in keys[:-1]:
node = node.setdefault(k, {})
node[keys[-1]] = int(val) if val.isdigit() else val
return values
def render(tpl, values, release):
def lookup(m):
expr = m.group(1).strip()
if expr == ".Release.Name":
return release
node = values
for k in expr.removeprefix(".Values.").split("."):
node = node[k]
return str(node)
return re.sub(r"\{\{\s*(.*?)\s*\}\}", lookup, tpl)
chart_values = {"replicaCount": 1,
"image": {"repository": "nginx", "tag": "1.27"},
"ports": [80, 443]}
prod_file = {"replicaCount": 3, "image": {"tag": "1.27.2"}, "ports": [8080]}
values = deep_merge(chart_values, prod_file)
values = set_flag(values, "image.tag=1.27.3")
print(values)
tpl = """name: {{ .Release.Name }}-web
replicas: {{ .Values.replicaCount }}
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}"""
print(render(tpl, values, "shop"))
결과는 다음과 같다.
{'replicaCount': 3, 'image': {'repository': 'nginx', 'tag': '1.27.3'}, 'ports': [8080]}
name: shop-web
replicas: 3
image: nginx:1.27.3
image.repository 는 병합으로 살아남았고, ports 는 두 개에서 하나로 교체됐다. --set 이 파일보다 우선했다. 실제 헬름은 여기에 템플릿 함수, 조건문, 스키마 검증이 더해질 뿐 병합의 뼈대는 같다.
현업에서는
- 렌더링 결과를 먼저 본다.
helm template이나helm upgrade --dry-run으로 실제 매니페스트를 확인하고 적용한다. 들여쓰기 하나가 틀리면 필드가 엉뚱한 곳에 붙어 조용히 무시될 수 있다.helm diff플러그인을 쓰면 클러스터의 현재 상태와 차이를 볼 수 있다. - Jekyll·Jinja 와의 충돌. 헬름 템플릿을 문서나 다른 템플릿 엔진 안에 넣으면 이중 중괄호가 그쪽 엔진에 먼저 해석된다. 블로그(Jekyll)에서는 raw 블록으로 감싸야 하고, Ansible 에서 헬름 값 파일을 만들 때도 같은 문제가 생긴다.
- GitOps 와 함께. ArgoCD 같은 도구는 헬름 차트를 직접 렌더링해서 적용한다. 이때 헬름 릴리스 객체 대신 ArgoCD 가 상태를 관리하므로
helm list에 보이지 않을 수 있다(230번 주제). - 시크릿을 values 에 넣지 않는다. values 파일은 Git 에 올라간다. 비밀값은 외부 시크릿 관리 도구나 암호화된 시크릿(SOPS, Sealed Secrets 등)으로 분리한다.
- 차트 버전 고정. 외부 차트를 쓸 때는
--version으로 차트 버전을 고정한다. 고정하지 않으면 다음 설치 때 기본값이 바뀐 새 차트가 들어올 수 있다.
확인 문제
- 차트, 저장소, 릴리스를 한 문장씩 정의하라.
Chart.yaml의version과appVersion의 차이는?- 기본 values 에
ports: [80, 443],-f파일에ports: [8080]이 있다. 결과는? --set과-f로 같은 키를 주면 어느 쪽이 이기는가?- 헬름 3 은 릴리스 이력을 어디에 저장하는가?
풀이
- 차트: 리소스 템플릿과 메타데이터 묶음. 저장소: 차트를 모아 배포하는 곳. 릴리스: 차트를 클러스터에 설치한 인스턴스.
version은 차트 패키지의 버전,appVersion은 배포되는 애플리케이션의 버전이다.[8080]. 리스트는 병합되지 않고 교체된다.--set이 우선한다.- 릴리스가 설치된 네임스페이스의 시크릿(기본 저장 드라이버)에 리비전별로 저장한다.
더 읽을거리 (References)
- Helm Docs, Using Helm
- Helm Docs, Charts
- Helm Docs, Getting Started (Chart Template Guide)
- Helm Docs, Values Files