쿠버네티스 패키지 도구 Helm 에 대해 정리한 글을 공유합니다.
Helm 구성과 lgtm stack 배포를 통해 구성 원리를 확인하겠습니다.
Helm
Helm은 쿠버네티스 패키지 관리자입니다. 쿠버네티스용으로 개발된 소프트웨어를 제공, 공유 및 관리할 수 있도록 지원합니다.
Google의 쿠버네티스용 배포 관리자와 통합되어 현재 Helm의 주요 프로젝트 저장소로 잡았고, 깃허브의 3만개의 이상의 Star, 200만 건 이상의 다운로드를 기록하고 있습니다.
CNCF에서 졸업(Graduation) 단계인 프로젝트로 쿠버네티스 패키지를 손쉽게 배포하고 관리하는 표준으로 자리매김하고 있습니다.
Helm Chart
기본 헬름 차트 구조는 다음과 같이 구성됩니다.
mychart/
Chart.yaml # 차트 설명 및 메타데이터 포함
values.yaml # 차트 설정 값
charts/ # 하위 차트
templates/ # 템플릿
...
예제 차트를 구성하겠습니다.
# chart 차트 구성
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
# templates 템플릿 구성
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.yaml 파일에서 템플릿화 : 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
# 차트 값 Value 구성
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
- toYaml 는 Value에서 설정한 YAML 값를 그대로 가져와서 Deployment 파일의 해당 위치(nindent)에 적절히 들여쓰기하여 출력해주는 역할임
로컬 차트 랜더링( Value → Template, Chart에 구성)
helm template .
---
# Source: pacman/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: pacman
name: pacman
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app.kubernetes.io/name: pacman
---
# Source: pacman/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: pacman # Chart.yaml 파일에 설정된 이름을 가져와 설정
labels:
app.kubernetes.io/name: pacman # Chart.yaml 파일에 appVersion 여부에 따라 버전을 설정
app.kubernetes.io/version: "1.0.0" # appVersion 값을 가져와 지정하고 따움표 처리
spec:
replicas: 1 # replicaCount 속성을 넣을 자리 placeholder
selector:
matchLabels:
app.kubernetes.io/name: pacman
template:
metadata:
labels:
app.kubernetes.io/name: pacman
spec:
containers:
- image: "quay.io/gitops-cookbook/pacman-kikd:1.0.0" # 이미지 지정 placeholder, 이미지 태그가 있으면 넣고, 없으면 Chart.yaml에 값을 설정
imagePullPolicy: Always
securityContext:
{} # securityContext의 값을 YAML 객체로 지정하며 14칸 들여쓰기
name: pacman
ports:
- containerPort: 8080
name: http
protocol: TCP
배포 및 관리
# 헬름 배포
helm install pacman .
# 차트 리소스 확인
helm list -A
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
pacman default 1 2025-10-24 23:16:49.558416 +0900 KST deployed pacman-0.1.0 1.0.0
# 배포 리소스 확인
kubectl get all
NAME READY STATUS RESTARTS AGE
pod/pacman-576769bb86-xp7xh 1/1 Running 0 4m5s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 6d5h
service/pacman ClusterIP 10.96.204.156 <none> 8080/TCP 4m5s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/pacman 1/1 1 1 4m5s
NAME DESIRED CURRENT READY AGE
replicaset.apps/pacman-576769bb86 1 1 1 4m5s
# 헬름 히스토리 확인
helm history pacman
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Fri Oct 24 23:16:49 2025 deployed pacman-0.1.0 1.0.0 Install complete
# 헬름 메타데이터 관리를 위한 Secret 리소스 관리
## Helm이 차트의 상태를 복구하거나 rollback 할 때 데이터 사용
kubectl get secret
NAME TYPE DATA AGE
sh.helm.release.v1.pacman.v1 helm.sh/release.v1 1 9m47s
차트 업그레이드 및 메타데이터 확인
# 헬름 차트
helm upgrade pacman --reuse-values --set replicaCount=2 .
---
Release "pacman" has been upgraded. Happy Helming!
NAME: pacman
LAST DEPLOYED: Fri Oct 24 23:36:15 2025
NAMESPACE: default
STATUS: deployed
REVISION: 3
TEST SUITE: None
# 메타데이터 관리 확인
kubectl get secret
NAME TYPE DATA AGE
sh.helm.release.v1.pacman.v1 helm.sh/release.v1 1 19m
sh.helm.release.v1.pacman.v2 helm.sh/release.v1 1 5m23s
sh.helm.release.v1.pacman.v3 helm.sh/release.v1 1 21s
# 헬름 차트 정보 확인
helm get manifest pacman
---
...
helm get values pacman
---
USER-SUPPLIED VALUES:
replicaCount: 2
# 차트 삭제
helm uninstall pacman
kubectl get secret
release "pacman" uninstalled
No resources found in default namespace.
_helpers.tpl 를 통한 재사용 가능한 코드 블록 정의
pacman 차트에서 동일한 필드가 3곳 존재함 → 수정시 똑같이 업데이트 필요
# deployment.yaml, service.yaml 에 selector 필드가 동일
## deployment.yaml
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ .Chart.Name}}
template:
metadata:
labels:
app.kubernetes.io/name: {{ .Chart.Name}}
## service.yaml
selector:
app.kubernetes.io/name: {{ .Chart.Name }}
_helpers.tpl 이용
- define 값(pacman.selectorLabels)을 통해 매핑
# _helpers.tpl 파일 작성
cat << EOF > templates/_helpers.tpl
{{- define "pacman.selectorLabels" -}} # stetement 이름을 정의
app.kubernetes.io/name: {{ .Chart.Name}} # 해당 stetement 가 하는 일을 정의
{{- end }}
EOF
# 기존 yaml 리팩토링
## deployment.yaml 수정
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "pacman.selectorLabels" . | nindent 6 }} # pacman.selectorLabels를 호출한 결과를 6만큼 들여쓰기하여 주입
template:
metadata:
labels:
{{- include "pacman.selectorLabels" . | nindent 8 }} # pacman.selectorLabels를 호출한 결과를 8만큼 들여쓰기하여 주입
## service.yaml 수정
selector:
{{- include "pacman.selectorLabels" . | nindent 6 }}
# 랜더링 확인
helm template .

자주 쓰는 명령어
헬름을 통해 패키지를 배포하였으나, 차트 파일이 없는 상태일 경우
# 차트 리스트 확인
helm list -A
---
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
pacman default 1 2025-10-25 00:07:08.44483 +0900 KST deployed pacman-0.1.0 1.0.0
# 차트 값, 매니페스트 재구성
helm pull pacman --version 0.1.0 --untar # 로컬이라 안됨 -> URL을 통해 가져올 수 있음
helm get values pacman > values.yaml
# Values 값 수정 후 업데이트
vi values.yaml
replicaCount: 2 # 1->2 수정
# 업그레이드
helm upgrade pacman . --reuse-values -f values.yaml
---
Release "pacman" has been upgraded. Happy Helming!
NAME: pacman
LAST DEPLOYED: Sat Oct 25 00:20:21 2025
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None
- -f 은 values 파일 설정 옵션
- 덮어쓰기가 가능하므로 맨 뒤에 파일이 최종으로 업데이트됨
Helm 원리 이해하기
배포 단계에서 설정 값에 따라 구성 옵션을 자유롭게 변경할 수 있습니다.
이때 내부 차트는 어떻게 구성되어 있는 지, LGTM Stack 을 통해 확인해보겠습니다.
LGTM Stack ?
https://github.com/daviaraujocc/lgtm-stack?tab=readme-ov-file
The LGTM stack, by Grafana Labs, combines best-in-class open-source tools to provide
- Loki: Log aggregation system https://grafana.com/oss/loki/
- Grafana: Interface & Dashboards https://grafana.com/oss/grafana/
- Tempo: Distributed tracing storage and management https://grafana.com/oss/tempo/
- Mimir: Long-term metrics storage for Prometheus https://grafana.com/oss/mimir/

helm repo add grafana https://grafana.github.io/helm-charts
# 패키지 설치
helm install my-lgtm-distributed grafana/lgtm-distributed --version 2.1.0
# 로컬 템플릿 확인
helm pull grafana/lgtm-distributed --version 2.1.0 --untar
# 패스워드 확인
kubectl get secret my-lgtm-distributed-grafana -n lgtm -o json | jq -r '.data | to_entries[] | "\(.key): \(.value|@base64d)"'
연동 확인

차트 구성
LGTM Stack 들이 각 하위 차트로 구성되어 있습니다.
tree -L 2 .
.
├── Chart.lock
├── Chart.yaml
├── README.md
├── charts
│ ├── grafana
│ ├── loki-distributed
│ ├── mimir-distributed
│ ├── oncall
│ └── tempo-distributed
├── templates
│ ├── NOTES.txt
│ └── _helpers.tpl
└── values.yaml
Loki 저장소 확장 구성 확인
Loki 로그 저장소를 AWS S3를 통해 구성하고 헬름 차트를 확인하겠습니다.
S3를 로그 저장소로 구성하면 데이터 보관, 확장성과 백업 구성이 용이해집니다.
https://grafana.com/docs/loki/latest/setup/install/helm/deployment-guides/aws/
1. IAM 역할 정의
AWS S3 접근 제어를 위해 IAM 역할 및 정책을 정의하겠습니다.
자리표시자(<, >) 값을 실제 값으로 수정해야 합니다.
vi loki-s3-policy.json
---
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LokiStorage",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::< CHUNK BUCKET NAME >",
"arn:aws:s3:::< CHUNK BUCKET NAME >/*",
"arn:aws:s3:::< RULER BUCKET NAME >",
"arn:aws:s3:::< RULER BUCKET NAME >/*"
]
}
]
}
aws iam create-policy --policy-name LokiS3AccessPolicy --policy-document file://loki-s3-policy.json
2. EKS 로키 파드에서 AWS S3를 사용하기 위해 신뢰 정책을 정의합니다.
vi trust-policy.json
---
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::< ACCOUNT ID >:oidc-provider/oidc.eks.<INSERT REGION>.amazonaws.com/id/< OIDC ID >"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.<INSERT REGION>.amazonaws.com/id/< OIDC ID >:sub": "system:serviceaccount:loki:loki",
"oidc.eks.<INSERT REGION>.amazonaws.com/id/< OIDC ID >:aud": "sts.amazonaws.com"
}
}
}
]
}
# IAM 역할 생성
aws iam create-role --role-name LokiServiceAccountRole --assume-role-policy-document file://trust-policy.json
# 정책 Attach
aws iam attach-role-policy --role-name LokiServiceAccountRole --policy-arn arn:aws:iam::<AccountID>:policy/LokiS3AccessPolicy
3. 헬름 초기 버전 배포
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
kubectl create namespace lgtm
# Helm 설치
helm install my-lgtm-distributed grafana/lgtm-distributed \
--version 2.1.0 \
--namespace lgtm
4. Loki 패스워드 설정
# htpasswd -c .htpasswd <username>
htpasswd -c .htpasswd admin
kubectl create secret generic loki-basic-auth --from-file=.htpasswd -n lgtm
kubectl create secret generic canary-basic-auth \
--from-literal=username=admin \
--from-literal=password=admin \
-n lgtm
5. Loki 차트 구성
Loki 차트는 LGTM 하위 차트로 구성되어 있습니다.
중첩 구조를 통해 Loki 스토리지 설정을 다음과 같이 설정합니다.
# lgtm-values.yaml
loki:
enabled: true
# loki-distributed 차트 설정은 여기에
loki:
schemaConfig:
configs:
- from: 2020-07-01
store: boltdb-shipper
object_store: aws
schema: v11
index:
prefix: index_
period: 24h
storageConfig:
boltdb_shipper:
active_index_directory: /loki/boltdb-shipper-active
cache_location: /loki/boltdb-shipper-cache
cache_ttl: 24h # Can be increased for faster performance over longer query periods, uses more disk space
shared_store: s3
aws:
s3: s3://ap-northeast-2
bucketnames: <bucket-name>
s3forcepathstyle: false
ingester:
chunk_encoding: snappy
limits_config:
allow_structured_metadata: true
volume_enabled: true
retention_period: 672h
compactor:
retention_enabled: true
delete_request_store: s3
shared_store: s3
working_directory: /var/loki/compactor
ruler:
enabled: false
querier:
max_concurrent: 4
# 컴포넌트 replicas 설정
ingester:
replicas: 1
persistence:
enabled: false
querier:
replicas: 1
queryFrontend:
replicas: 1
queryScheduler:
enabled: true
replicas: 1
distributor:
replicas: 1
compactor:
enabled: true
replicas: 1
persistence:
enabled: false
indexGateway:
enabled: true
replicas: 1
persistence:
enabled: false
ruler:
enabled: true
replicas: 1
maxUnavailable: 1
gateway:
enabled: true
service:
type: LoadBalancer
basicAuth:
enabled: true
existingSecret: loki-basic-auth
# ServiceAccount (IRSA)
serviceAccount:
create: true
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::<Account-ID>:role/LokiServiceAccountRole"
grafana:
enabled: true
datasources:
datasources.yaml:
apiVersion: 1
datasources:
- name: Loki
uid: loki
type: loki
url: http://my-lgtm-distributed-loki-gateway
access: proxy
isDefault: false
basicAuth: true
basicAuthUser: ${LOKI_BASIC_AUTH_USER}
jsonData:
httpHeaderName1: X-Scope-OrgID
secureJsonData:
basicAuthPassword: ${LOKI_BASIC_AUTH_PASSWORD}
httpHeaderValue1: "1"
- name: Mimir
uid: prom
type: prometheus
url: http://my-lgtm-distributed-mimir-nginx/prometheus
isDefault: true
- name: Tempo
uid: tempo
type: tempo
url: http://my-lgtm-distributed-tempo-query-frontend:3100
isDefault: false
jsonData:
tracesToLogsV2:
datasourceUid: loki
lokiSearch:
datasourceUid: loki
tracesToMetrics:
datasourceUid: prom
serviceMap:
datasourceUid: prom
# 🔐 환경변수로 BasicAuth 주입(운영은 Secret 권장)
env:
LOKI_BASIC_AUTH_USER: "admin"
LOKI_BASIC_AUTH_PASSWORD: "admin"
mimir:
enabled: true
# ... 기존 mimir 설정 유지
tempo:
enabled: true
# ... 기존 tempo 설정 유지
helm upgrade my-lgtm-distributed grafana/lgtm-distributed \
--version 2.1.0 \
-f lgtm-values.yaml \
--namespace lgtm
6. 차트 구성 확인
헬름 차트에서 스토리지 연동 부분은 다음과 같습니다. Loki 2.9.6 버전 configmap을 통해 연동을 하며 옵션 확인은 차트가 아닌 Doc를 통해 확인해야 합니다.
- 예제로 선택한 Lgtm Stack이 작년 이후 업데이트 되지 않은 상태라 Loki 버전이 낮습니다. deprecated 된 boltdb를 기준으로 설명합니다.
# lgtm-vlaues.yaml
..
storageConfig:
aws:
region: ap-northeast-2
bucketnames: hshan92
s3forcepathstyle: false
tsdb_shipper:
active_index_directory: /var/loki/index
cache_location: /var/loki/cache
shared_store: s3
..
{{- toYaml .Values.loki.storageConfig | nindent 2}}
# values.yaml
...
storage_config:
{{- if .Values.indexGateway.enabled}}
{{- $indexGatewayClient := dict "server_address" (printf "dns:///%s:9095" (include "loki.indexGatewayFullname" .)) }}
{{- $_ := set .Values.loki.storageConfig.boltdb_shipper "index_gateway_client" $indexGatewayClient }}
{{- end}}
{{- toYaml .Values.loki.storageConfig | nindent 2}} # 스토리지 입력
{{- if .Values.memcachedIndexQueries.enabled }}
index_queries_cache_config:
memcached_client:
addresses: dnssrv+_memcached-client._tcp.{{ include "loki.memcachedIndexQueriesFullname" . }}.{{ .Release.Namespace }}.svc.{{ .Values.global.clusterDomain }}
consistent_hash: true
...
# 랜더링 확인
helm template my-lgtm-distributed grafana/lgtm-distributed \
-n lgtm \
-f lgtm-values.yaml \
| grep -A 20 "storage_config:"\
storage_config:
aws:
bucketnames: hshan92
s3: s3://ap-northeast-2
s3forcepathstyle: false
boltdb_shipper:
active_index_directory: /loki/boltdb-shipper-active
cache_location: /loki/boltdb-shipper-cache
cache_ttl: 24h
index_gateway_client:
server_address: dns:///my-lgtm-distributed-loki-index-gateway:9095
shared_store: s3
filesystem:
directory: /var/loki/chunks
table_manager:
retention_deletes_enabled: false
retention_period: 0s
7. 테스트 로그 호출
ELB=a08db421ff2f34f11a25629d1e1ed624-2032163681.ap-northeast-2.elb.amazonaws.com
# (Gateway에 basicAuth 켜둔 상태라면) 사용자/비번 포함
curl -u 'admin:admin' \
-X POST http://$ELB/loki/api/v1/push \
-H "Content-Type: application/json" \
-d '{
"streams": [{
"stream": { "app": "test", "level": "info" },
"values": [[ "'$(date +%s)000000000'", "Test log message" ]]
}]
}'
kubectl port-forward svc/my-lgtm-distributed-grafana 3000:80 -n lgtm

AWS S3 연동 확인
# 업로딩 로그 확인
kubectl logs my-lgtm-distributed-loki-ingester-0 -n lgtm
---
...
level=info ts=2025-10-25T07:14:47.019507465Z caller=index_set.go:86 msg="uploading table loki_index_20386"
level=info ts=2025-10-25T07:14:47.019515227Z caller=index_set.go:107 msg="finished uploading table loki_index_20386"
level=info ts=2025-10-25T07:14:47.019521961Z caller=index_set.go:185 msg="cleaning up unwanted indexes from table loki_index_20386"
...

참고
https://grafana.com/docs/loki/latest/setup/install/helm/deployment-guides/aws/
'Cloud' 카테고리의 다른 글
| ArgoCD 정리 (1) (0) | 2025.11.08 |
|---|---|
| Gitops CI & CD 구성(Jenkins & ArgoCD) (0) | 2025.11.02 |
| 쿠버네티스 GItOps (0) | 2025.10.19 |
| Amazon VPC Lattice for Amazon EKS (0) | 2025.04.27 |
| EKS 파드로 노드 관리하기 (0) | 2025.04.12 |