Helm Chart 생성하기

목차

Helm Chart란

Helm Chart는 Kubernetes 리소스를 정의하는 파일들의 모음입니다. Chart는 애플리케이션을 배포하는 데 필요한 모든 정보를 포함하며, 재사용 가능하고 버전 관리가 가능합니다.

Chart 구조

기본적인 Chart 디렉토리 구조는 다음과 같습니다:

mychart/
├── Chart.yaml # Chart에 대한 메타데이터
├── values.yaml # Chart의 기본 설정값
├── charts/ # 이 Chart가 의존하는 다른 Chart들
├── templates/ # Kubernetes 리소스 템플릿 파일들
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── _helpers.tpl # 템플릿 헬퍼 함수들
│ └── NOTES.txt # 설치 후 출력될 사용 안내문
├── .helmignore # 패키징 시 제외할 파일 패턴
└── README.md # Chart 설명 문서

Chart 생성하기

1. 새 Chart 생성

Helm CLI를 사용하여 기본 Chart 구조를 생성합니다:

helm create [mychart]

이 명령은 mychart 디렉토리를 생성하고 기본 템플릿 파일들을 자동으로 생성합니다.

2. Chart.yaml 작성

Chart.yaml은 Chart의 메타데이터를 정의하는 파일입니다:

apiVersion: v2
name: mychart
description: A Helm chart for my application
type: application

# Chart 버전 (Chart 자체의 버전)
version: 0.1.0

# 애플리케이션 버전 (배포할 애플리케이션의 버전)
appVersion: "1.0.0"

# 유지관리자 정보 (선택사항)
maintainers:
- name: Your Name
email: your.email@example.com

# 키워드 (선택사항)
keywords:
- web
- application

# 홈페이지 (선택사항)
home: https://example.com

# 소스 코드 저장소 (선택사항)
sources:
- https://github.com/yourusername/mychart

# 의존성 (선택사항)
dependencies:
- name: mysql
version: "9.3.4"
repository: https://charts.bitnami.com/bitnami
condition: mysql.enabled

3. values.yaml 작성

values.yaml은 Chart의 기본 설정값을 정의합니다:

# 복제본 개수
replicaCount: 1

# 이미지 설정
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.21.0"

# 이미지 Pull Secret (선택사항)
imagePullSecrets: []

# 서비스 어카운트
serviceAccount:
create: true
name: ""
annotations: {}

# Pod 어노테이션
podAnnotations: {}

# Pod 보안 컨텍스트
podSecurityContext: {}
# fsGroup: 2000

# 컨테이너 보안 컨텍스트
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000

# 서비스 설정
service:
type: ClusterIP
port: 80

# Ingress 설정
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: chart-example.local
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local

# 리소스 제한
resources: {}
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi

# 오토스케일링
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80

# 노드 선택
nodeSelector: {}

# Tolerations
tolerations: []

# Affinity
affinity: {}

# 환경 변수
env: []
# - name: ENV_NAME
# value: "value"

# ConfigMap
configMap:
enabled: false
data: {}

# Secret
secret:
enabled: false
data: {}

4. Deployment 템플릿 작성

templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "mychart.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "mychart.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.env }}
env:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

5. Service 템플릿 작성

templates/service.yaml:

apiVersion: v1
kind: Service
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "mychart.selectorLabels" . | nindent 4 }}

6. Ingress 템플릿 작성

templates/ingress.yaml:

{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "mychart.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

7. Helper 템플릿 작성

templates/_helpers.tpl:

{{/*
Chart의 전체 이름 생성
*/}}
{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $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 }}
{{- end }}

{{/*
Chart 이름과 버전으로 chart 레이블 생성
*/}}
{{- define "mychart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
공통 레이블
*/}}
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
{{ include "mychart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector 레이블
*/}}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Chart 이름
*/}}
{{- define "mychart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
ServiceAccount 이름 생성
*/}}
{{- define "mychart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mychart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

8. NOTES.txt 작성

templates/NOTES.txt:

1. 다음 명령으로 애플리케이션 상태를 확인하세요:

kubectl get pods -n {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mychart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}"

2. 다음 명령으로 애플리케이션에 접근하세요:

{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mychart.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mychart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mychart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

Chart 검증 및 테스트

1. Chart 문법 검증

helm lint mychart

2. Chart 템플릿 렌더링 확인

# 기본 값으로 렌더링
helm template mychart

# 특정 릴리즈 이름으로 렌더링
helm template my-release mychart

# 커스텀 값으로 렌더링
helm template my-release mychart -f custom-values.yaml

# 특정 템플릿만 렌더링
helm template my-release mychart --show-only templates/deployment.yaml

3. Dry-run으로 설치 테스트

helm install my-release mychart --dry-run --debug

4. 실제 설치

# 기본 값으로 설치
helm install my-release mychart

# 커스텀 값으로 설치
helm install my-release mychart -f custom-values.yaml

# 명령줄에서 값 오버라이드
helm install my-release mychart --set replicaCount=3

Chart 패키징 및 배포

1. Chart 패키징

# Chart를 .tgz 파일로 패키징
helm package mychart

# 특정 디렉토리에 패키징
helm package mychart -d ./packages

2. Chart 의존성 관리

Chart가 다른 Chart에 의존하는 경우:

# 의존성 업데이트
helm dependency update mychart

# 의존성 빌드
helm dependency build mychart

# 의존성 목록 확인
helm dependency list mychart

3. Chart Repository에 업로드

# Chart Museum 사용 예시
curl --data-binary "@mychart-0.1.0.tgz" http://chartmuseum.example.com/api/charts

# Repository 인덱스 업데이트
helm repo index . --url http://charts.example.com

고급 기능

1. 조건부 리소스

{{- if .Values.configMap.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}
data:
{{- toYaml .Values.configMap.data | nindent 2 }}
{{- end }}

2. 반복문 사용

{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}

3. 기본값 설정

# values.yaml에 없는 경우 기본값 사용
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"

4. 파일 include

data:
config.yaml: |
{{- .Files.Get "configs/config.yaml" | nindent 4 }}

모범 사례

  1. 명확한 네이밍: Chart 이름과 리소스 이름을 명확하게 작성
  2. values.yaml 문서화: 각 설정값에 주석으로 설명 추가
  3. 기본값 제공: 모든 필수 값에 합리적인 기본값 설정
  4. 버전 관리: Chart 버전과 AppVersion을 명확히 구분
  5. 템플릿 검증: 배포 전 항상 helm linthelm template 실행
  6. 리소스 제한: 프로덕션 환경을 위한 리소스 제한값 설정
  7. 보안 고려: 민감한 정보는 Secret으로 관리
  8. 의존성 고정: 의존하는 Chart의 버전을 명시적으로 지정
  9. NOTES.txt 활용: 사용자를 위한 명확한 설치 후 안내 제공
  10. README 작성: Chart 사용법과 설정 옵션을 문서화

ConfigMap과 Secret 추가하기

ConfigMap 추가

1. templates/configmap.yaml 생성

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}-config
labels:
{{- include "mychart.labels" . | nindent 4 }}
data:
# 키-값 쌍으로 설정
app.properties: |
database.url={{ .Values.config.databaseUrl }}
log.level={{ .Values.config.logLevel }}

# 또는 단순 키-값
API_URL: {{ .Values.config.apiUrl }}
ENVIRONMENT: {{ .Values.config.environment }}

2. values.yaml에 ConfigMap 설정값 추가

config:
databaseUrl: "mongodb://localhost:27017/mydb"
logLevel: "info"
apiUrl: "https://api.example.com"
environment: "production"

3. Deployment에서 ConfigMap 사용

apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: {{ .Chart.Name }}
# 환경변수로 주입
envFrom:
- configMapRef:
name: {{ include "mychart.fullname" . }}-config

# 또는 개별 환경변수로
env:
- name: DATABASE_URL
valueFrom:
configMapKeyRef:
name: {{ include "mychart.fullname" . }}-config
key: database.url

# 볼륨으로 마운트
volumeMounts:
- name: config-volume
mountPath: /etc/config

volumes:
- name: config-volume
configMap:
name: {{ include "mychart.fullname" . }}-config

Secret 추가

1. templates/secret.yaml 생성

apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}-secret
labels:
{{- include "mychart.labels" . | nindent 4 }}
type: Opaque
data:
# base64로 인코딩된 값 (values.yaml에서 이미 인코딩된 값 사용)
{{- range $key, $value := .Values.secrets }}
{{ $key }}: {{ $value | b64enc | quote }}
{{- end }}

또는 stringData 사용 (자동 인코딩):

apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}-secret
labels:
{{- include "mychart.labels" . | nindent 4 }}
type: Opaque
stringData:
database-password: {{ .Values.secrets.databasePassword }}
api-key: {{ .Values.secrets.apiKey }}

2. values.yaml에 Secret 값 추가

secrets:
databasePassword: "mypassword"
apiKey: "my-secret-api-key"

중요: 실제 운영 환경에서는 values.yaml에 시크릿을 직접 저장하지 말고, 다음 방법 사용:

방법 1: 설치 시 값 오버라이드
helm install myapp ./mychart \
--set secrets.databasePassword="actual-password" \
--set secrets.apiKey="actual-api-key"
방법 2: 별도 values 파일 사용 (gitignore에 추가)
helm install myapp ./mychart -f secrets-values.yaml
방법 3: 외부 Secrets 참조
# 이미 존재하는 Secret 사용
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: myapp
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecret | default (include "mychart.fullname" .) }}
key: database-password

3. Deployment에서 Secret 사용

apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: {{ .Chart.Name }}
# 환경변수로 주입
envFrom:
- secretRef:
name: {{ include "mychart.fullname" . }}-secret

# 또는 개별 환경변수로
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "mychart.fullname" . }}-secret
key: database-password

# 볼륨으로 마운트
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true

volumes:
- name: secret-volume
secret:
secretName: {{ include "mychart.fullname" . }}-secret

조건부 생성

ConfigMap이나 Secret을 선택적으로 생성하려면:

{{- if .Values.config.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}-config
data:
# ...
{{- end }}

values.yaml:

config:
enabled: true
databaseUrl: "..."

보안 모범 사례

  1. External Secrets Operator 사용

    • AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager 등과 연동
    • Secret을 외부 시스템에서 동적으로 가져오기
  2. Sealed Secrets 사용

    • 암호화된 Secret을 Git에 안전하게 저장
    • 클러스터에서만 복호화 가능
  3. Helm Secrets Plugin 사용

    • SOPS(Secrets OPerationS)로 values 파일 암호화
    • Git에 암호화된 파일 안전하게 커밋
  4. 최소 권한 원칙

    • Secret에 접근할 수 있는 ServiceAccount 제한
    • RBAC를 통한 접근 제어

유용한 명령어 정리

# Chart 생성
helm create mychart

# Chart 검증
helm lint mychart

# 템플릿 렌더링
helm template my-release mychart

# Dry-run
helm install my-release mychart --dry-run --debug

# 설치
helm install my-release mychart

# 업그레이드
helm upgrade my-release mychart

# 패키징
helm package mychart

# Chart 값 확인
helm show values mychart

# Chart 정보 확인
helm show chart mychart

# 전체 정보 확인
helm show all mychart
Share