[CI/CD Study 2주차] Helm Chart 생성 및 배포
CoudNet@ 팀의 가시다님께서 리딩하시는 CI/CD Study 2주차 스터디 내용 정리
이번 주차에는 Helm Chart 를 직접 생성해보고 Tekton CI/CD 도구를 활용해서 쿠버네티스에 배포하는 것까지 학습했습니다.
1. Helm
1.1. Helm 소개
쿠버네티스 환경에서 애플리케이션을 쉽고 일관되게 배포, 관리할 수 있도록 도와주는 패키지 관리자(package manager)
리눅스에서 apt나 yum이 패키지를 관리하듯이, Helm은 Kubernetes 리소스를 패키징하고 배포합니다.
헬름은 커스터마이즈와 유사하지만 템플릿 기반 솔루션이며 패키지 관리자처럼 동작하여 버전 관리, 공유, 배포 가능 아티팩트를 생성합니다.
커스터마이즈와 헬름의 차이점 하나는 Chart 입니다.
차트는 공유 가능한 쿠버네티스 패키지로, 다른 차트에 대한 의존성 등 다양한 요소를 포함하여 애플리케이션을 배포하는 데 필요한 모든 쿠버네티스 YAML 파일(deployment, service 등)과 설정 파일(values.yaml)을 묶어둔 것입니다.
Helm 구성 개념
1. Chart
- Helm 패키지 단위
2. Release
- Helm 이 설치된 인스턴스
3. Repository
- Helm Chart 를 저장하는 공간
4. Values
- Helm Chart 설정값을 정의하는 파일
Helm 역할
1. 템플릿화
- 쿠버네티스 매니페스트 파일들을 Go 템플릿 문법으로 만들어서 동적으로 값 주입
2. 릴리즈 관리
- Chart 설치, 업데이트, 롤백하는 과정을 버전 단위로 관리
Helm 기본 구조
mychart/
├── Chart.yaml # Chart 이름, 버전, 설명
├── values.yaml # 기본 설정값
├── templates/ # 실제 K8s 리소스 템플릿들
│ ├── deployment.yaml
│ ├── service.yaml
│ └── _helpers.tpl
└── charts/ # 하위 chart 의존성
1.2. Helm Chart 생성
간단한 Helm Chart 를 처음부터 생성해보려고 합니다.
Helm 배포를 위해서는 쿠버네티스 클러스터가 필요하며, 해당 쿠버네티스는 Kind 를 통해 Control Plane 1대만 배포 했습니다.
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
다음과 같은 Helm Chart 파일을 생성해보겠습니다.

# Helm 디렉토리 생성
mkdir -p pacman/templates
cd pacman
# Chart yaml 생성
cat << EOF > Chart.yaml
apiVersion: v2
name: pacman
description: A Helm chart for Pacman
type: application
version: 0.1.0 # 차트 버전, 차트 정의가 바뀌면 업데이트한다
appVersion: "1.0.0" # 애플리케이션 버전
EOF
# deployment 생성
cat << EOF > templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Chart.Name}} # Chart.yaml 파일에 설정된 이름을 가져와 설정
labels:
app.kubernetes.io/name: {{ .Chart.Name}}
{{- if .Chart.AppVersion }} # Chart.yaml 파일에 appVersion 여부에 따라 버전을 설정
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} # appVersion 값을 가져와 지정하고 따움표 처리
{{- end }}
spec:
replicas: {{ .Values.replicaCount }} # replicaCount 속성을 넣을 자리 placeholder
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}}" # 이미지 지정 placeholder, 이미지 태그가 있으면 넣고, 없으면 Chart.yaml에 값을 설정
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 14 }} # securityContext의 값을 YAML 객체로 지정하며 14칸 들여쓰기
name: {{ .Chart.Name}}
ports:
- containerPort: {{ .Values.image.containerPort }}
name: http
protocol: TCP
EOF
# service 생성
cat << EOF > 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 }}
EOF
# values 파일 생성
cat << EOF > values.yaml
image: # image 절 정의
repository: quay.io/gitops-cookbook/pacman-kikd
tag: "1.0.0"
pullPolicy: Always
containerPort: 8080
replicaCount: 1
securityContext: {} # securityContext 속성의 값을 비운다
EOF
# 최종 디렉터리 구조 확인
tree
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ └── service.yaml
└── values.yaml
2 directories, 4 files


Helm Chart 를 클러스터에 배포하기 전 helm template 명령어를 통해 쿠버네티스 YAML 로 렌더링할 수 있습니다.
렌더링된 값은 Helm 문법 등이 모두 적용된 쿠버네티스 배포용 매니페스트입니다.
helm template .

Helm 배포 시에는 아래와 같이 우선 순위가 적용됩니다.
1. CLI 에서 --set 명령 파라미터를 통한 직접 설정 값 지정
2. CLI 에서 -f 을 통해 지정된 여러 사용자 지정 values.yaml 파일 값 중 마지막으로 지정된 values.yaml 값
3. CLI 에서 -f 을 통해 지정된 사용자 지정 values.yaml 파일 값
4. 기본 values.yaml 설정 값
helm template --set replicaCount=3 .

Helm 배포
# Helm 설치
helm install pacman .
# Helm 설치 정보 확인
helm list
# helm 히스토리 확인
helm history pacman

Helm 버전 관리
Helm 은 Secret 에 배포 릴리스 메타데이터를 저장하고 상태를 확인합니다.
Helm 버전을 롤백하거나 업그레이드할 때 해당 Secret 정보를 참조하여 새로 생성하게 됩니다.
# 기존 Secret 정보 확인
kubectl get secret
# Helm 값 업그레이드 (Replica 변경 1 -> 2)
helm upgrade pacman --reuse-values --set replicaCount=2 .
# Secret 정보 확인
kubectl get secret
# Helm 삭제 후 Secret 확인
helm uninstall pacman
kubectl get secret

1.3. Helm 템플릿화 적용
현재 생성된 Helm Chart 의 Deployment 나 Service Yaml 파일을 보면,
Label 의 app.kubernetes.io/name: {{ .Chart.Name}} 값이 중복으로 적용된 것을 알 수 있습니다.
이렇게 된다면 Label 값을 추가하거나 수정할 필요가 있을 때, 적용된 모든 값을 찾아서 하나씩 업데이트 해줘야 되는 번거로움이 존재합니다.
이를 해결하기 위해 템플릿 파일을 사용해서 반복해서 중복으로 사용하는 코드를 선언할 수 있습니다.


# _helpers.tpl 파일 작성
cat << EOF > templates/_helpers.tpl
{{- define "pacman.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name}}
app.kubernetes.io/version: {{ .Chart.AppVersion}}
{{- end }}
EOF
## deployment.yaml 수정
# labels 부분을 찾아서 아래와 같이 수정
# 기존: app.kubernetes.io/name: {{ .Chart.Name }}
# 변경:
# matchLabels: {{- include "pacman.selectorLabels" . | nindent 6 }}
# metadata.labels: {{- include "pacman.selectorLabels" . | nindent 8 }}
## service.yaml 수정
# labels 부분을 찾아서 아래와 같이 수정
# 기존: app.kubernetes.io/name: {{ .Chart.Name }}
# 변경: {{- include "pacman.selectorLabels" . | nindent 6 }}
# Helm 렌더링 값 확인
helm template .

1.4. Helm 컨테이너 이미지 업데이트
배포된 파일에서 컨테이너 이미지를 변경하고 업그레이드를 하면 어떻게 되는 지 확인해보겠습니다.
# Values 파일에서 이미지 태그 업데이트
cat << EOF > values.yaml
image:
repository: quay.io/gitops-cookbook/pacman-kikd
tag: "1.1.0"
pullPolicy: Always
containerPort: 8080
replicaCount: 1
securityContext: {}
EOF
# Chart 파일에서 앱버전 업데이트
cat << EOF > Chart.yaml
apiVersion: v2
name: pacman
description: A Helm chart for Pacman
type: application
version: 0.1.0
appVersion: "1.1.0"
EOF
# Helm 업그레이드
helm upgrade pacman .

이전 버전으로 롤백
Helm Chart 를 잘못 배포했을 경우, 아래와 같은 명령어로 손쉽게 롤백할 수 있습니다.
helm rollback pacman 1 && kubectl get pod -w

1.5. Helm 패키징
Helm Chart 를 패키징하면 공개 사이트에 내가 만든 Helm 을 업로드할 수 있고 다른 사용자가 Helm 을 배포할 수 있습니다.
# 저장소 디렉터리 예시
repo
|—— index.html
|—— pacman-0.1.0.tgz
Helm 패키징
패키징한 결과물을 Helm Chart 저장소에 게시하면 됩니다.
Helm Chart 저장소는 차트 및 .tgz 차트에 대한 메타데이터 정보를 담은 index.html 파일이 있는 HTTP 서버이기에,
차트를 저장소에 게시하려면 index.html 파일을 새 메타데이터 정보로 업데이트하고 아티팩트를 업로드 해야합니다.
# pacman 차트를 .tgz 파일로 패키징
helm package .
## index.html 파일 생성
helm repo index .

1.6. Helm Repository 활용
이번에는 원격 Helm Chart 저장소에서 Helm 을 배포해보겠습니다.
원격 저장소를 사용하기 위해 helm repo add 명령어를 사용해서 원격 저장소를 추가해야합니다.
# 원격 레포지토리를 사용해서 postgresql 설치
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo list
helm search repo postgresql
# 배포
helm install my-db \
--set postgresql.postgresqlUsername=my-default,postgresql.postgresqlPassword=postgres,postgresql.postgresqlDatabase=mydb,postgresql.persistence.enabled=false \
bitnami/postgresql
# 확인
helm list
# 확인 후 삭제
helm uninstall my-db

1.7. 의존성 Helm Chart
의존성 Helm Chart 는 어떤 차트가 다른 차트에 의존한다는 사실을 선언할 때 사용하며,
Chart.yaml 파일의 dependencies 섹션을 사용합니다.

Song Service Helm 차트가 PostgreSQL 차트에 의존하고 있는 관계를 만들어 보겠습니다.
# Helm 디렉터리 생성
mkdir -p music/templates
cd music
# Deployment 생성
cat << EOF > 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 }}
name: {{ .Chart.Name}}
ports:
- containerPort: {{ .Values.image.containerPort }}
name: http
protocol: TCP
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 }}
EOF
# Service 생성
cat << EOF > 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 }}
EOF
# Chart 생성
cat << EOF > 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"
EOF
# Values 생성
cat << EOF > values.yaml
image:
repository: quay.io/gitops-cookbook/music
tag: "1.0.0"
pullPolicy: Always
containerPort: 8080
replicaCount: 1
postgresql:
server: jdbc:postgresql://music-db-postgresql:5432/mydb
postgresqlUsername: my-default
postgresqlPassword: postgres
postgresqlDatabase: mydb
secretName: music-db-postgresql
secretKey: postgresql-password
EOF
# 생성된 파일 구조 확인
tree
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ └── service.yaml
└── values.yaml
2 directories, 4 files

그 후, 의존성 차트를 다운해서 차트 디렉터리에 저장합니다.
# 의존성 차트 업데이트
helm dependency update
tree
├── Chart.lock
├── Chart.yaml
├── charts
│ └── postgresql-18.0.17.tgz
├── templates
│ ├── deployment.yaml
│ └── service.yaml
└── values.yaml
3 directories, 6 files

Helm 배포
helm install music-db .
# 확인
kubectl get sts,pod,svc,ep,secret,pv,pvc

이번 주차 학습을 통해 Helm Chart 를 동작 방식에 대해 자세히 알 수 있었습니다.
이전에도 Helm Chart 를 직접 만들어서 애플리케이션을 배포한 적은 있었지만 내부 동작 방식을 완전히 이해한 것은 아니었는데,
이번 기회를 통해 그 궁금증을 해결할 수 있어서 뜻깊었습니다. :)