Helm에서 Kustomize로 넘어갈 때 생기는 세 가지 빈자리 — 템플릿 함수·훅·테스트를 다시 채우는 법
Helm을 Kustomize로 바꾸자는 이야기를 처음 들었을 때 솔직히 반가웠습니다. {{ toYaml .Values.something | indent 8 }} 같은 구문을 디버깅하다 밤을 새운 적이 있거나, helm template을 돌려야만 실제로 어떤 YAML이 클러스터에 들어가는지 알 수 있다는 불편함을 겪어본 분이라면 같은 감정을 공유할 겁니다.
그런데 막상 전환을 시작하면 금방 벽에 부딪힙니다. Helm이 제공하던 것 중 Kustomize에는 아예 대응 개념이 없는 것들이 있거든요. 특히 세 가지가 뼈아픕니다. 동적 템플릿 함수, 릴리스 생명주기 훅, 그리고 helm test 기반 검증. 이 세 가지를 어떻게 대체하느냐가 전환의 성패를 가릅니다.
이 글은 그 공백을 각각 어떻게 채울 수 있는지, 어떤 도구와 패턴을 조합하면 되는지를 실제 사용 사례 중심으로 살펴봅니다. "Kustomize는 다 됩니다"도 아니고 "역시 Helm이 낫습니다"도 아닌, 2026년 현재 실무에서 통용되는 현실적인 선택지를 짚어보겠습니다.
왜 지금 이 전환 이야기가 나오나
Kustomize의 철학은 명확합니다. 템플릿 없음(template-free). 베이스 YAML은 그냥 Kubernetes 매니페스트고, 환경마다 오버레이를 겹쳐 최종 상태를 만드는 방식입니다. kubectl explain이 완전히 작동하고, IDE 자동완성이 죽지 않으며, kubectl kustomize overlays/prod만 실행하면 클러스터에 들어갈 최종 YAML을 바로 확인할 수 있습니다.
반면 Helm은 Go text/template 위에 Sprig 함수 라이브러리를 얹은 완전한 템플릿 시스템입니다. if/else, range, trim, toYaml, randAlphaNum까지 수백 가지 함수가 YAML 파이프라인 안에서 돌아가죠. 강력하지만, 그만큼 복잡성도 함께 옵니다.
2026년 현재 대규모 클러스터를 운영하는 팀 사이에서 지배적인 패턴은 이렇습니다. Helm은 외부 패키지 경계, Kustomize는 내부 애플리케이션 설정. Prometheus나 Cert-Manager 같은 서드파티 차트는 Helm으로 그대로 쓰고, 팀이 직접 소유한 배포 매니페스트에는 Kustomize를 적용하는 하이브리드 전략입니다.
그러나 팀 소유 배포 코드를 Kustomize로 옮길 때 빈자리가 생깁니다.
공백 1 — 템플릿 함수
실제로 무엇이 사라지나
Helm 차트에서 흔히 쓰는 템플릿 로직을 정리하면 크게 네 종류입니다.
| 유형 | Helm 예시 | Kustomize 지원 여부 |
|---|---|---|
| 단순 값 치환 | {{ .Values.image.tag }} |
제한적으로 가능 (replacements) |
| 조건부 리소스 포함 | {{- if .Values.ingress.enabled }} |
Components로 대응 |
| 동적 값 생성 | {{ randAlphaNum 32 }} |
완전 불가, 외부 도구 필요 |
| YAML 덩어리 삽입 | {{ toYaml .Values.resources | indent 10 }} |
패치로 대체 가능 |
패턴 A — helmCharts 필드로 선렌더링
Kustomize 4.1 이후부터 kustomization.yaml에 helmCharts 필드를 선언하면 Helm 렌더링을 Kustomize 파이프라인 첫 단계로 집어넣을 수 있습니다. Sprig 함수를 포함한 모든 템플릿이 먼저 처리되고, 그 순수 YAML 출력 위에 Kustomize 패치가 적용됩니다.
# kustomization.yaml
helmCharts:
- name: my-app
releaseName: my-app-prod
repo: https://charts.example.com
version: 1.2.3
valuesFile: values.yaml
patches:
- path: patch-resources.yaml여기서 주의할 점 하나. helmCharts는 기본적으로 비활성화된 기능이라 --enable-helm 플래그 없이는 아예 처리되지 않습니다. 실무 전환에서 가장 먼저 막히는 지점이니 기억해두면 좋습니다.
kustomize build --enable-helm overlays/prod
# 또는
kubectl kustomize --enable-helm overlays/prod그리고 releaseName을 명시하지 않으면 차트 name을 그대로 릴리스 이름으로 사용해 렌더링되므로, 실제 릴리스 이름과 어긋나면 라벨·셀렉터가 예상과 달라질 수 있습니다.
커맨드라인으로 비슷한 효과를 내려면 이렇게 됩니다.
helm template my-app-prod ./chart --values values.yaml | kubectl apply -f -저도 처음엔 이 방식이 "Kustomize의 패배 아닌가?" 싶었는데, 생각해보면 Helm의 패키징 능력과 Kustomize의 패치 능력을 각자의 역할대로 쓰는 거라 오히려 합리적입니다. 렌더링 결과를 Git에 커밋해두면 드리프트 감지도 명확해지고요.
패턴 B — replacements로 단순 값 치환
이미지 태그나 ConfigMap 값을 다른 리소스에 주입하는 단순 케이스는 Kustomize 네이티브 replacements로 처리할 수 있습니다.
# kustomization.yaml
replacements:
- source:
kind: ConfigMap
name: app-config
fieldPath: data.IMAGE_TAG
targets:
- select:
kind: Deployment
name: my-app
fieldPaths:
- spec.template.spec.containers.[name=app].image컨테이너를 지정할 때 숫자 인덱스(containers.0.image)로도 파싱은 되지만, 컨테이너 순서가 바뀌면 조용히 잘못된 대상에 값을 주입할 수 있어 위험합니다. 공식 문서는 [name=app]처럼 이름 기반 선택자를 권장합니다.
단, replacements는 "값을 다른 리소스에서 참조"하는 패턴이지 Go 템플릿처럼 임의 표현식을 실행하는 게 아닙니다. randAlphaNum 32 같은 동적 생성은 여기서도 불가능합니다.
패턴 C — Components로 조건부 리소스 표현
{{- if .Values.ingress.enabled }} 같은 조건부 포함 패턴은 Kustomize Components(4.1+)로 표현하는 것이 자연스럽습니다. 기능 단위를 독립된 kind: Component로 잘라두고, 필요한 오버레이에서만 참조하면 됩니다.
# components/ingress/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
- ingress.yaml
patches:
- path: enable-tls.yaml# overlays/prod/kustomization.yaml
components:
- ../../components/ingress
resources:
- ../../base오버레이 계층을 무리하게 늘리지 않고 "이 환경은 인그레스 기능을 켠다"는 의도를 그대로 코드에 드러낼 수 있습니다. 조건부 리소스 포함이 많은 차트일수록 Components 도입 효과가 큽니다.
동적 시크릿 생성 — Helm 시절부터의 숙제
randAlphaNum을 활용한 시크릿 자동 생성은 Kustomize의 한계라기보다는 Helm에서도 이미 안티패턴입니다. helm upgrade를 돌릴 때마다 값이 새로 생성돼 시크릿이 매번 교체되고, 참조하는 워크로드가 함께 재시작되는 문제가 잘 알려져 있습니다. lookup 함수로 회피하는 트릭이 있지만 GitOps 흐름과는 잘 맞지 않죠.
즉, 외부 시크릿 도구 도입은 "Kustomize 전환을 위해 치러야 할 대가"가 아니라 도구 선택과 무관하게 언젠가는 해야 할 개선입니다. 전환 계획을 그 정리 작업과 함께 묶어두면 오히려 명분이 생깁니다.
| 도구 | 특징 |
|---|---|
| External Secrets Operator | Vault, AWS Secrets Manager 등에서 Kubernetes Secret으로 동기화 |
| Sealed Secrets (Bitnami) | 암호화된 시크릿을 Git에 커밋, GitOps 친화적 |
| Vault Agent Injector | 사이드카 방식으로 Pod에 직접 주입 |
공백 2 — 릴리스 생명주기 훅
훅이 사라지면 무슨 일이 생기나
Helm 훅은 helm.sh/hook: pre-upgrade 같은 애노테이션으로 Job 실행 순서를 제어합니다. DB 마이그레이션을 애플리케이션 배포 전에 실행하거나, 배포 완료 후 캐시 워밍 Job을 돌리는 패턴이 대표적이죠.
Kustomize는 리소스 적용 순서를 보장하는 훅 메커니즘을 내장하지 않습니다. GitHub Issue #1580에서 오랫동안 논의됐지만 네이티브 지원은 없습니다.
ArgoCD를 쓴다면 — Sync Waves
ArgoCD는 Helm 훅 중 test와 rollback을 무시합니다. Sync 기반 배포 방식(기본값)에서는 사실상 Sync Waves 조합이 표준적인 선택이 됩니다. 다만 Helm 훅 자체를 살리고 싶다면 helm upgrade 방식으로 배포하도록 애플리케이션을 구성하는 우회 옵션도 있으니, 100% 강제라고 오해하진 마세요.
# DB 마이그레이션 Job
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/sync-wave: "1"
spec:
template:
spec:
containers:
- name: migrate
image: myapp:latest
command: ["./migrate"]
restartPolicy: Neverargocd.argoproj.io/hook: PreSync가 Helm의 pre-upgrade에 대응하고, sync-wave 숫자로 실행 순서를 제어합니다. PostSync는 post-install·post-upgrade에 대응합니다.
FluxCD를 쓴다면 — dependsOn
여기서 한 가지 짚고 넘어갈 것이 있습니다. Flux에서 HelmRelease를 그대로 두면 Helm SDK가 훅을 그대로 실행해줍니다. 하지만 이 글의 전제인 Kustomize로 대체를 선택한 순간, 즉 Kustomization 리소스로 옮긴 시점에는 Helm 훅이 더 이상 동작하지 않습니다. Helm 훅을 유지하고 싶다면 애초에 전환 대상에서 제외하고 HelmRelease로 남겨두는 편이 낫습니다.
Kustomize로 옮긴 경우 순서 제어가 필요하다면 Kustomization 객체 간 dependsOn을 활용합니다.
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: db-migrate
namespace: flux-system
spec:
dependsOn:
- name: database
path: ./jobs/migrateGitOps 없이 — 파이프라인 두 단계
GitOps 도구 없이 CI/CD 파이프라인에서 직접 처리하는 경우라면 단순하게 스크립트로 순서를 보장할 수 있습니다.
# Step 1: 마이그레이션 먼저
kubectl apply -k overlays/pre-install/
kubectl wait --for=condition=complete job/db-migrate --timeout=300s
# Step 2: 메인 애플리케이션
kubectl apply -k overlays/production/단순하지만 명시적입니다. 훅 메커니즘의 마법 대신 순서가 코드에 드러나니 오히려 추적하기 쉬울 때도 많습니다.
공백 3 — helm test 기반 검증
helm test가 하는 일
helm.sh/hook: test 애노테이션이 달린 Pod/Job을 릴리스 후 클러스터 안에서 직접 실행하고 결과를 수집하는 명령입니다. 서비스 엔드포인트가 실제로 응답하는지, DB 연결이 되는지 같은 것을 클러스터 내부에서 확인할 수 있어서 유용했습니다.
Kustomize에는 이에 상응하는 단일 명령이 없습니다.
전용 테스트 오버레이 구성
디렉터리 구조로 테스트 리소스를 분리하는 게 가장 직관적인 방식입니다.
k8s/
├── base/
│ └── deployment.yaml
├── overlays/
│ └── production/
└── tests/
├── kustomization.yaml
├── smoke-test-job.yaml
└── connectivity-test.yaml# tests/smoke-test-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: smoke-test
spec:
template:
spec:
restartPolicy: Never
containers:
- name: test
image: curlimages/curl
command: ["curl", "-f", "http://my-app/health"]# CI 파이프라인에서 실행
kubectl apply -k tests/
kubectl wait --for=condition=complete job/smoke-test --timeout=120s
kubectl logs job/smoke-test
kubectl delete -k tests/기존 Helm 테스트 Job YAML을 재활용하는 방법도 있습니다. helm.sh/hook 애노테이션만 제거하면 kubectl로 직접 실행 가능한 테스트 리소스가 됩니다. 작업량이 많지 않아 실용적입니다.
정적 검증 도구 조합
helm test는 런타임 검증이었지만, 배포 전 정적 검증은 오히려 더 풍부하게 구성할 수 있습니다.
| 도구 | 역할 | Helm 대응 |
|---|---|---|
| kubeconform | Kubernetes 스키마 검증 | helm lint 대체 |
| kube-score | 베스트 프랙티스 품질 검사 | 추가 검증 |
| conftest (OPA/Rego) | 정책 기반 매니페스트 테스트 | helm test 유효성 검증 역할 |
| Pluto | 지원 종료 API 버전 감지 | 추가 안전망 |
트레이드오프 — 정직하게 보기
| 항목 | Kustomize 전환 후 좋아지는 것 | 주의해야 할 것 |
|---|---|---|
| 가독성 | 순수 Kubernetes 매니페스트, IDE 자동완성 완전 작동 | - |
| 감사 용이성 | kubectl kustomize overlays/prod로 즉시 최종 상태 확인 |
- |
| GitOps 연동 | ArgoCD·FluxCD와 네이티브 통합이 단순 | Sync 기반 ArgoCD는 test·rollback 훅 무시 |
| 훅 관리 | - | 라이프사이클 관리를 GitOps 도구나 스크립트로 직접 구현 |
| 동적 값 | - | 동적 시크릿 생성에 외부 도구 필요 (Helm에서도 권장) |
| 패키징 | - | 단일 차트로 패키징해 배포하는 시나리오에는 근본적으로 부적합 |
| 조건부 리소스 | - | Components로 대응 가능하나 잘게 쪼갤수록 참조 관계가 늘어남 |
솔직히 말하면, 훅 부재는 "복잡성이 사라지는" 게 아니라 "복잡성이 이동하는" 겁니다. Helm 차트 안에 숨어 있던 실행 순서 로직이 GitOps 도구 설정이나 파이프라인 스크립트로 나오는 거죠. 어디가 더 명시적이냐의 문제입니다.
전환을 보류할 이유 — Helm 4의 변수
여기서 잠깐, 지금 시점에 함께 고려할 변수 하나. Atmosly 블로그의 Helm 4 정리에 따르면 Helm 4는 Sprig 실행 엔진과 훅·템플릿 렌더 스트림을 재정비했다고 소개하고 있습니다. 성능 수치는 해당 블로그의 자체 측정치인 만큼 프로덕션 전환의 근거로 삼기보다는 참고용으로 보는 편이 안전합니다. 공식 릴리스 노트와 자체 벤치마크로 재확인한 뒤 판단하시길 권합니다.
핵심은, Kustomize 전환을 이번 분기에 밀어붙일지 아니면 Helm 4 도입 이후로 재평가할지 판단이 필요하다는 겁니다. helmCharts 필드를 활용한 하이브리드 구성을 검토 중이라면 이 변화가 적잖은 영향을 줄 수 있습니다.
어디서부터 시작하면 될까
한 가지 패턴이 2026년 현재 가장 현실적인 접근으로 자리 잡았습니다. 팀이 소유한 애플리케이션부터 Kustomize로 전환하고, 서드파티 차트(모니터링, 인그레스 컨트롤러 등)는 Helm으로 유지하는 것입니다.
전환 순서를 제안하자면 이렇습니다.
-
시크릿 전략 먼저 — External Secrets Operator나 Sealed Secrets 중 하나를 결정합니다. 앞서 말했듯 이 정리는 Helm을 계속 쓰더라도 어차피 필요한 작업이라, 전환 계획과 함께 묶으면 명분이 커집니다.
-
훅 의존도 파악 — 현재 차트에서
pre-upgrade·post-install훅이 무엇을 하는지 목록화합니다. DB 마이그레이션인지 캐시 워밍인지에 따라 ArgoCD Sync Waves로 갈지, FluxdependsOn으로 갈지, 파이프라인 스크립트로 갈지 결정이 달라집니다. -
조건부 리소스는 Components로 재설계 —
if .Values.xxx.enabled블록을 세어보고, 기능 단위로 잘라 컴포넌트로 만듭니다. 이 단계에서 오버레이 계층이 지나치게 깊어지지 않도록 구조를 설계해두면 이후 유지보수가 편해집니다. -
테스트 오버레이 만들기 — 기존
helm testJob에서helm.sh/hook애노테이션만 제거하고tests/디렉터리로 옮깁니다. CI 파이프라인에 apply–wait–logs–delete 루프를 추가하는 건 어렵지 않습니다.
마치며
Kustomize 전환은 "간결함으로 가는 길"이라기보다는 "복잡성의 위치를 옮기는 결정"에 가깝습니다. 이 글에서 짚은 세 가지 공백 — 템플릿 함수, 훅, 테스트 — 은 어느 하나도 공짜로 채워지지 않습니다. 각각 외부 시크릿 도구, GitOps 도구 설정(또는 파이프라인 코드), 별도 테스트 오버레이라는 비용을 요구하죠.
이 비용을 감수할 가치가 있느냐는 팀의 상황에 달렸습니다. 다만 결정을 내리기 전에 세 공백을 각각 어떻게 채울지 종이 위에 먼저 그려보시길 권합니다. 그림이 안 그려지는 공백이 있다면, 그게 전환을 미뤄야 할 신호입니다.
참고 자료
- Helm vs Kustomize: We Manage 100+ Clusters - Here's What We Actually Use (2026) | Tasrie IT Services
- Helm vs Kustomize in 2025: Patterns, Pros, Cons, and How to Combine Them | justinpolidori.com
- pre-upgrade Hook · Issue #1580 · kubernetes-sigs/kustomize (GitHub)
- How to Handle Helm Chart Hooks vs ArgoCD Hooks Conflict
- Flux CD vs ArgoCD: Helm Support Comparison
- Helm Hooks and Chart Tests: Lifecycle Management Done Right | DevOpsil
- Replacing Helm and Kustomize with KRM Functions | Medium
- Power Up Helm Charts: Using Kustomize to Manage Kubernetes Deployments | JFrog Blog
- When and How to Use Helm and Kustomize Together | Thomas Stringer
- Helm 4 Migration Guide: What's New | Atmosly Blog
- Patch Any Helm Chart Template Using A Kustomize Post-Renderer | Austin Dewey
- Extending Kustomize | SIG CLI 공식 문서
- The Kustomization File | SIG CLI 공식 문서
- Kustomize Components | SIG CLI 공식 문서
- khelm — Helm chart templating CLI / Kustomize plugin (GitHub)
- template-transformer — Kustomize transformer plugin (GitHub)