Templating con Helm: empaquetar manifiestos Kubernetes
Construí un Helm chart listo para producción desde cero: values, helpers, condicionales, rangos, hooks, subcharts, empaquetado y registros OCI.
Overview
Mantener un manifiesto de Kubernetes por entorno no escala. Copiás deployment.yaml, ajustás réplicas y tags de imagen para staging, los volvés a ajustar para producción, y después de unos releases las copias divergen — nadie recuerda cuál es la versión canónica. Helm trata los manifiestos como plantillas: un chart guarda la estructura, values.yaml guarda los valores por defecto y cada entorno recibe un archivo de overrides. Cuando corrés helm install, Helm renderiza las plantillas con tus valores, aplica el resultado al cluster y registra el release para que upgrades y rollbacks sean reproducibles.
Esta receta construye un chart completo — values, helpers, condicionales, rangos, hooks, subcharts y library charts — y después lo empaqueta y distribuye. Para instalar y administrar releases en CI/CD en lugar de crear charts, mirá Desplegar aplicaciones en Kubernetes con Helm Charts.
When to Use
- Desplegar la misma app a varios entornos (dev, staging, production) donde solo cambia la configuración
- Compartir un deployment reusable entre equipos — los consumidores sobrescriben values en lugar de editar YAML
- Administrar el ciclo de vida de una aplicación (install, upgrade, rollback) con seguimiento de releases
- Parametrizar manifiestos: réplicas, imágenes, límites de recursos, feature toggles por entorno
- Incorporar herramientas de terceros (Prometheus, Grafana, ingress controllers) a través de sus charts publicados
When NOT to Use
- Un solo entorno con manifiestos estáticos — un simple
kubectl apply -ftiene menos partes móviles - Clusters gestionados con GitOps donde ArgoCD o Flux es dueño del ciclo de aplicación (aunque ambos pueden consumir Helm charts)
- Manifiestos que cambian raramente y no necesitan parametrización
- Equipos que ya estandarizaron con overlays de Kustomize o jsonnet y no quieren Go templates
Solution
Estructura del chart
my-chart/
├── Chart.yaml # Metadata del chart y dependencias
├── .helmignore # Archivos excluidos del chart empaquetado
├── values.yaml # Values por defecto
├── values/
│ ├── staging.yaml # Overrides por entorno
│ └── production.yaml
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ ├── ingress.yaml
│ ├── _helpers.tpl # Named templates
│ └── NOTES.txt # Notas post-install
└── charts/ # Dependencias de subcharts descargadas
Chart.yaml
apiVersion: v2
name: my-app
description: A Helm chart for my application
type: application
version: 1.0.0 # Versión del chart
appVersion: "2.5.0" # Versión de la app
keywords:
- web
- api
maintainers:
- name: Mathias Paulenko
email: mathias@example.com
dependencies:
- name: postgresql
version: 12.x.x
repository: oci://registry-1.docker.io/bitnamicharts
condition: postgresql.enabled
values.yaml
replicaCount: 2
image:
repository: my-app
tag: "2.5.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
ingress:
enabled: false
className: nginx
hosts:
- host: my-app.example.com
paths:
- path: /
pathType: Prefix
config:
LOG_LEVEL: info
DATABASE_URL: ""
postgresql:
enabled: false
Template de Deployment
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
- name: LOG_LEVEL
value: {{ .Values.config.LOG_LEVEL | quote }}
- name: DATABASE_URL
value: {{ .Values.config.DATABASE_URL | quote }}
Template de Service
# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
selector:
{{- include "my-app.selectorLabels" . | nindent 4 }}
Helper templates
# templates/_helpers.tpl
{{- define "my-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "my-app.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 }}
{{- define "my-app.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
{{ include "my-app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
Bloques condicionales
# templates/ingress.yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "my-app.fullname" . }}
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: {{ .Values.ingress.className }}
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "my-app.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
Range sobre values
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "my-app.fullname" . }}
data:
{{- range $key, $value := .Values.config }}
{{ $key }}: {{ $value | quote }}
{{- end }}
Values específicos por entorno
# values/production.yaml
replicaCount: 5
image:
tag: "2.5.0-prod"
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
ingress:
enabled: true
className: nginx
hosts:
- host: my-app.production.com
paths:
- path: /
pathType: Prefix
config:
LOG_LEVEL: warn
DATABASE_URL: "postgres://prod-db:5432/myapp"
Helm hooks
# templates/post-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-app.fullname" . }}-post-install
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate", "up"]
env:
- name: DATABASE_URL
value: {{ .Values.config.DATABASE_URL | quote }}
El chart completo — Chart.yaml, values.yaml, overrides por entorno y todos los templates de abajo — es ejecutable tal cual desde el repositorio complementario.
Instalar y actualizar
# Instalar con values por defecto
helm install my-app ./my-chart
# Instalar con values personalizados
helm install my-app ./my-chart -f values/production.yaml
# Instalar con overrides inline
helm install my-app ./my-chart --set replicaCount=5 --set image.tag=2.5.0
# Upgrade
helm upgrade my-app ./my-chart -f values/production.yaml
# Rollback
helm rollback my-app 1
# Uninstall
helm uninstall my-app
# Template (dry-run, renderiza los manifiestos)
helm template my-app ./my-chart -f values/production.yaml > manifests.yaml
# Lint
helm lint ./my-chart
How It Works
Cuando corrés helm install, el CLI ejecuta cada archivo bajo templates/ como un Go template contra los values fusionados, y después envía los manifiestos renderizados a la API de Kubernetes. Si el apply tiene éxito, Helm guarda el release como un Secret en el cluster — ese registro es lo que permite que helm upgrade, helm rollback y helm history funcionen.
Objetos de contexto del template
Además de .Values, los templates pueden leer varios objetos integrados:
| Objeto | Qué expone |
|---|---|
.Values | Values fusionados de values.yaml, archivos -f y flags --set |
.Release | Nombre del release, namespace, revisión y estado de install/upgrade |
.Chart | Campos de Chart.yaml como nombre, versión y appVersion |
.Capabilities | Info del cluster — versión de Kubernetes y grupos de API disponibles |
.Files | Archivos del chart que no son templates, para embeber datos crudos |
Precedencia de values
Cuando varias fuentes definen la misma clave, Helm las aplica en orden y gana la última escritura:
values.yamldentro del chart provee los valores por defecto- Los archivos
-fse aplican de izquierda a derecha, así que los últimos sobrescriben a los primeros - Los flags
--set,--set-stringy--set-filetienen la precedencia más alta
Una clave que nunca se define renderiza como nil. Protegé los bloques opcionales con {{ if .Values.x }}, dales un fallback con {{ .Values.x | default "y" }}, o fallá rápido con {{ required "database.host es requerido" .Values.database.host }} para que el consumidor reciba un error claro en lugar de un manifiesto roto.
Inspeccionar el output renderizado
Renderizá siempre antes de instalar — helm template muestra exactamente lo que va a llegar a la API, y helm show values ./my-chart imprime los defaults fusionados que un consumidor sobrescribiría:
helm template my-app ./my-chart -f values/production.yaml
# Output renderizado (extracto)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
labels:
helm.sh/chart: my-app-1.0.0
app.kubernetes.io/name: my-app
app.kubernetes.io/instance: my-app
app.kubernetes.io/version: "2.5.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 5
Empaquetar y distribuir un chart
helm package empaqueta el directorio del chart en un archivo .tgz versionado. Corré primero helm dependency update — resuelve los subcharts en charts/ y escribe Chart.lock, que conviene commitear para que cada build fije las mismas versiones de dependencias. .helmignore funciona como .gitignore para el archivo empaquetado.
helm dependency update ./my-chart # escribe Chart.lock, descarga subcharts
helm package ./my-chart # produce my-chart-1.0.0.tgz
# Registros OCI (Helm 3.8+): se sube como una imagen de contenedor
helm push my-chart-1.0.0.tgz oci://registry.example.com/charts
# Repositorio de charts clásico: generar el índice
helm repo index ./repo --url https://charts.example.com
Depurar errores de renderizado
Dos errores concentran la mayoría de los templates rotos:
nil pointer evaluating .Values.database.host— el template referenció una clave que no existe. Agregá un default o marcá la clave como required para que Helm falle rápido.error converting YAML to JSON: yaml: line 12— el template renderizó YAML inválido. Corréhelm templatee inspeccioná el archivo generado — unquoteque faltó o unnindentmal ubicado suele ser la causa.
Para un release ya instalado, helm get manifest my-app imprime los manifiestos renderizados que Helm aplicó por última vez — la forma más rápida de comparar lo que está vivo en el cluster contra lo que produce el chart en local.
Variants
Usar subcharts
# Chart.yaml
dependencies:
- name: redis
version: 17.x.x
repository: oci://registry-1.docker.io/bitnamicharts
condition: redis.enabled
# values.yaml — configurar el subchart
redis:
enabled: true
auth:
password: "my-redis-password"
architecture: standalone
Library chart (bloques reusables)
# Chart.yaml
apiVersion: v2
name: common
type: library # Library chart — no desplegable
version: 1.0.0
# templates/_container.tpl
{{- define "common.container" -}}
- name: {{ .name }}
image: {{ .image }}
ports:
- containerPort: {{ .port }}
resources:
{{- toYaml .resources | nindent 4 }}
{{- end }}
# En el template de otro chart
{{- include "common.container" (dict "name" "api" "image" "my-api:1.0" "port" 8080 "resources" .Values.resources) | nindent 8 }}
Helm con sealed secrets
# templates/sealedsecret.yaml
{{- if .Values.sealedSecret.enabled }}
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: {{ include "my-app.fullname" . }}
spec:
encryptedData:
database-password: {{ .Values.sealedSecret.encryptedPassword | quote }}
template:
metadata:
name: {{ include "my-app.fullname" . }}
type: Opaque
{{- end }}
Best Practices
-
Para una guía más profunda, mirá Helm Charts: estructura, templating, dependencias y registry.
-
Preferí
includesobretemplate— soloincludedevuelve un string que podés encadenar connindent, y lo necesitás en casi todas partes. -
Usá siempre
nindenten lugar deindentpara YAML anidado — antepone el salto de línea, así el bloque queda en su propia línea. -
Definí resource requests y limits en
values.yamlpara que cada entorno pueda ajustarlos sin editar templates. -
Entrecomillá los valores de texto con
{{ .Values.x | quote }}— YAML leeyes/no/on/offy números con pinta de versión como booleanos o floats. -
Mantené
values.yamlcomo el default seguro y poné los overrides por entorno en archivos separados, no en cadenas largas de--setdentro de scripts de CI. -
Corré
helm lintyhelm templateen CI antes de cualquierhelm install— atrapan errores de sintaxis y de renderizado sin tocar el cluster. -
Fijá las versiones de subcharts en
Chart.yamly commiteáChart.lock, o un release upstream va a cambiar tus manifiestos renderizados sin avisar. -
Antes de mergear cambios de charts en un repo compartido, pasá por la checklist de revisión de Helm charts.
Common Mistakes
- Usar
indentdonde hace faltanindent:indentindenta las líneas pero no agrega un salto de línea inicial, así que la primera línea queda pegada a la clave padre y el manifiesto sale malformado. Usánindentpor defecto. - No entrecomillar values:
version: {{ .Values.version }}conversion: "2.0"renderiza comoversion: 2.0, que YAML parsea como float — yon/off/yes/nose vuelven booleanos. Pasá los valores de texto porquotepara que queden literales. - Hardcodear values en los templates: si un valor varía entre entornos o consumidores, movélo a
values.yaml; si no, el chart no se puede reusar y se pierde todo el sentido del templating. - Omitir
helm lintyhelm templateen CI: los errores de template aparecen al instalar, muchas veces con el pipeline reteniendo un lock de deployment. Renderizá y linteá primero. - Sobrescribir todo con
--set: las cadenas largas de--setson difíciles de revisar y filtran valores a los logs de CI. Los archivos pasados con-fquedan versionados, son comparables y más fáciles de auditar. - Editar archivos bajo
charts/: los subcharts descargados se sobrescriben en el próximohelm dependency update. Modificá el comportamiento a través de values, o forkeá el chart upstream si necesitás cambios reales.
See Also
- Chart Template Guide — recorrido oficial por el lenguaje de templates de Helm
- Template Function List — todas las funciones Sprig y Helm disponibles en templates
- Chart Best Practices — convenciones oficiales para autores de charts
- Registros basados en OCI — distribución de charts a través de registros de contenedores
Preguntas frecuentes
¿Qué es un Helm chart?
Un Helm chart es un paquete de manifiestos de Kubernetes basados en plantillas más un archivo values.yaml. Sobrescribís los values por entorno, instalás con helm install y Helm registra cada despliegue como un release que podés actualizar o revertir.
¿En qué se diferencia Helm de Kustomize?
Helm parametriza manifiestos con Go templates, así que los values pueden venir de flags, archivos o consumidores de un chart compartido. Kustomize evita el templating por completo — apila patches sobre una base. Helm conviene más cuando distribuís charts; Kustomize encaja mejor para overlays de entornos internos del equipo.
¿Qué es un Helm hook?
Un hook es un manifiesto anotado con helm.sh/hook que Helm ejecuta en un punto específico del ciclo de vida del release — pre-install, post-upgrade, pre-rollback, etc. Las migraciones de base de datos y los jobs de backup son el caso de uso clásico.
¿Puedo usar Helm con ArgoCD o Flux?
Sí — ambas herramientas GitOps despliegan Helm charts. ArgoCD los renderiza de forma nativa; Flux usa un CRD HelmRelease. En ese esquema Helm sigue haciendo el templating, pero el controlador GitOps decide cuándo se aplican los cambios.
¿Cómo depuro un template?
Renderizá en local con helm template my-app ./my-chart -f values.yaml, o usá helm install --dry-run --debug para ver los manifiestos y los values calculados sin tocar el cluster. Para un release instalado, helm get manifest muestra lo que Helm aplicó realmente.
¿Dónde publico un Helm chart?
Desde Helm 3.8 la opción recomendada es un registry OCI — subís charts con helm push al mismo registry de contenedores que ya usás. El enfoque anterior también sigue funcionando: hospedá los .tgz y un index.yaml generado detrás de cualquier servidor de archivos estáticos como un repositorio de charts clásico.
Recursos Relacionados
Helm Charts: Estructura, Templating, Dependencias, Registry
Dominá Helm charts para Kubernetes: estructura de charts, templating, values, dependencias, hooks, library charts, registry management y patrones de producción.
RecipeDespliegue de Aplicaciones en Kubernetes con Helm Charts
Empaqueta, versiona y despliega aplicaciones Kubernetes usando Helm charts con value overrides, funciones de template y release management para infraestructura reproducible
DocChecklist de Revisión de Helm Charts
Un checklist para revisar Helm charts cubriendo values, templates, security, resource limits, probes, RBAC y best practices.
RecipeMontar Configs y Secrets en Pods de Kubernetes
Cómo montar ConfigMaps y Secrets en pods de Kubernetes usando env vars, volumes, projected volumes y secret management con external secrets.