Llegamos al problema que abrió este módulo. El directorio k8s/ de Rutas Norte tiene más de 120 ficheros YAML. tienda-web-dev.yaml, tienda-web-pre.yaml y tienda-web-pro.yaml son idénticos salvo en el namespace, el número de réplicas, la etiqueta de la imagen y dos límites de recursos. Multiplica eso por seis componentes, cada uno con su Deployment, Service, HPA, ConfigMap, Ingress y NetworkPolicy, y tienes el desastre actual: cada cambio hay que replicarlo tres veces, y nadie está seguro de qué versión corre en producción.
Helm es el gestor de paquetes de Kubernetes, y es la primera de las dos respuestas a ese problema (la otra, Kustomize, es la lección siguiente). Ya lo hemos usado dos veces en el curso sin explicarlo: para instalar cert-manager en 04-05 y kube-prometheus-stack en 07-03. En esta lección vamos a entender qué estaba pasando realmente en esas órdenes, y después construiremos el chart de Rutas Norte desde cero.
Contenido
- El problema de Rutas Norte y cómo lo aborda Helm
- Conceptos: chart, release, repositorio y valores
- Consumir charts de terceros con criterio
- Estructura de un chart
- Plantillas: sintaxis, funciones y control de flujo
- Crear el chart de Rutas Norte
- Valores por entorno
- El ciclo de vida de una release
- Depuración: template, lint, dry-run y diff
- Dependencias y subcharts
- Hooks
- Empaquetar y publicar
- Qué hace mal Helm
- Errores comunes y consejos
- Ejercicios
- Conclusión
- El problema de Rutas Norte y cómo lo aborda Helm
El diferencial real entre dev y pro para api-reservas es este:
| Campo | dev | pre | pro |
|---|---|---|---|
metadata.namespace |
rutas-norte-dev |
rutas-norte-pre |
rutas-norte-pro |
spec.replicas |
1 | 2 | 4 |
image |
:dev-abc123 |
:rc-2.4.0 |
:2.4.0@sha256:... |
resources.limits.memory |
256Mi |
512Mi |
1Gi |
HPA maxReplicas |
3 | 5 | 20 |
Cinco valores. Y por cinco valores mantenemos 114 líneas duplicadas por componente.
Helm resuelve esto con una idea sencilla: convierte los manifiestos en plantillas y saca los valores variables a un fichero aparte.
flowchart LR
T["templates/<br/>deployment.yaml"] --> R{{"helm<br/>render"}}
V1["values-dev.yaml"] --> R
V2["values-pre.yaml"] --> R
V3["values-pro.yaml"] --> R
R --> M1["Manifiestos dev"]
R --> M2["Manifiestos pre"]
R --> M3["Manifiestos pro"]
M1 --> K[(Clúster)]
M2 --> K
M3 --> K
Pero Helm hace algo más que sustituir variables, y esto es lo que lo diferencia de un simple sed: gestiona el ciclo de vida completo de la instalación. Sabe qué objetos instaló, en qué versión, y puede desinstalarlos todos o volver atrás con una orden.
- Conceptos: chart, release, repositorio y valores
- Chart: el paquete. Un directorio (o
.tgz) con las plantillas, los valores por defecto y los metadatos. Es el equivalente a un.deb. No está instalado: es material inerte. - Release: una instalación concreta de un chart, con nombre. El mismo chart puede instalarse varias veces, y cada release tiene su historial independiente.
- Repositorio: un servidor HTTP (o un registro OCI) con charts empaquetados y un
index.yamlque los cataloga. - Valores: la configuración. Se resuelven por capas, y lo de más abajo gana:
values.yamldel chart → cada-f fichero.yamlen orden (el último gana) →--setde la línea de órdenes.
Helm es un gestor de paquetes con estado
Esta es la característica más malinterpretada. Cuando ejecutas helm install, Helm renderiza las plantillas y las aplica, pero además guarda una copia de todo en un Secret dentro del namespace de la release.
NAME TYPE DATA AGE
sh.helm.release.v1.rutas-norte.v1 helm.sh/release.v1 1 3d
sh.helm.release.v1.rutas-norte.v2 helm.sh/release.v1 1 2d
sh.helm.release.v1.rutas-norte.v3 helm.sh/release.v1 1 4hCada revisión es un Secret con el chart completo, los valores usados y los manifiestos renderizados, comprimidos y en base64.
| Consecuencia | Explicación |
|---|---|
helm rollback es posible |
Helm sabe exactamente qué había antes |
helm uninstall borra todo lo instalado |
Sabe qué objetos son suyos |
Si alguien hace kubectl edit, Helm no se entera |
El estado guardado y el real divergen |
| Si borras esos Secrets, Helm "pierde" la release | Aunque los objetos sigan en el clúster |
| El estado vive en el clúster, no en Git | La crítica principal a Helm, que resuelve GitOps (10-05) |
Helm 2 tenía un componente en el servidor llamado Tiller, con permisos de administrador, que fue un problema de seguridad notorio. Helm 3 lo eliminó: hoy helm es solo un binario en tu máquina que habla con la API usando tu kubeconfig y tu RBAC (08-01).
- Consumir charts de terceros con criterio
Volvamos sobre lo que hicimos en 04-05 y 07-03, ahora entendiendo cada paso.
helm repo add jetstack https://charts.jetstack.io
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# Descargar los índices. SIN ESTO, helm trabaja con una caché vieja.
helm repo update
# Ver TODAS las versiones disponibles
helm search repo jetstack/cert-manager --versions | head -4NAME CHART VERSION APP VERSION DESCRIPTION
jetstack/cert-manager v1.16.1 v1.16.1 A Helm chart for cert-manager
jetstack/cert-manager v1.15.3 v1.15.3 A Helm chart for cert-managerFíjate en las dos columnas de versión. CHART VERSION es la del empaquetado (cambia cuando el autor modifica las plantillas); APP VERSION es la del software instalado. Avanzan de forma independiente, y --version fija la del chart.
El paso que casi nadie da y que separa a un profesional de alguien que copia órdenes de un blog: leer los valores disponibles antes de instalar.
helm show values jetstack/cert-manager --version v1.16.1 > /tmp/defectos.yaml
wc -l /tmp/defectos.yaml # 1204 líneas de opciones documentadasAhí está todo lo configurable: réplicas, recursos, tolerations, PDB, integración con Prometheus, crds.enabled. Si instalas sin mirarlo, aceptas a ciegas los defectos del autor. Complementan helm show chart, helm show readme y helm show all.
cert-manager, ahora con criterio
En 04-05 ejecutamos algo parecido a helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --version v1.16.1 --set crds.enabled=true. Ahora sabemos leerlo: cert-manager es el nombre de la release (muchos objetos lo heredan); --create-namespace es necesario porque Helm no crea namespaces si no se lo pides; y --version es imprescindible, porque sin él instalas la última versión de ese momento y reinstalar seis meses después instala otra distinta. Es el mismo principio que las etiquetas inmutables de imagen (08-05).
La versión profesional usa un fichero de valores versionado en el repositorio:
# plataforma/cert-manager/valores-pro.yaml
crds:
enabled: true
keep: true # no borrar las CRDs al desinstalar: se llevarían
# por delante todos los Certificate del clúster
replicaCount: 2 # alta disponibilidad (09-05)
podDisruptionBudget:
enabled: true
minAvailable: 1
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { memory: 128Mi }
prometheus:
servicemonitor:
enabled: true # que kube-prometheus-stack lo recoja (07-03)
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app.kubernetes.io/instance: cert-manager }helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--version v1.16.1 -f plataforma/cert-manager/valores-pro.yaml \
--atomic --timeout 5mY lo mismo para Prometheus, donde hay un valor que merece atención especial:
# plataforma/kube-prometheus-stack/valores-pro.yaml
prometheus:
prometheusSpec:
retention: 30d
# Que descubra ServiceMonitors de TODOS los namespaces. Sin esto,
# los ServiceMonitor de rutas-norte-pro se ignoran EN SILENCIO.
serviceMonitorSelectorNilUsesHelmValues: false
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: rutas-norte-ssd
resources: { requests: { storage: 100Gi } }
grafana:
ingress:
enabled: true
ingressClassName: nginx
hosts: ["grafana.rutasnorte.example"]Lista de verificación antes de instalar un chart ajeno
| Comprobación | Cómo |
|---|---|
| ¿Quién lo mantiene? | helm show chart y el repositorio de origen |
| ¿Qué valores acepto por defecto? | helm show values > fichero.yaml y leerlo |
| ¿Qué va a crear exactamente? | helm template ... | less |
| ¿Pide permisos de cluster-admin? | Buscar ClusterRole en la salida de helm template |
| ¿Instala CRDs? ¿Qué pasa al desinstalar? | Buscar el directorio crds/ en el chart |
| ¿Qué imágenes descarga y de dónde? | helm template ... | grep image: |
| ¿He fijado la versión y guardado los valores en Git? | --version y -f, siempre |
- Estructura de un chart
chart-rutas-norte/
├── Chart.yaml # metadatos del chart
├── values.yaml # valores por defecto
├── charts/ # subcharts descargados
├── crds/ # CRDs, instaladas antes que todo lo demás
├── .helmignore # qué excluir al empaquetar
└── templates/
├── NOTES.txt # mensaje que se imprime tras instalar
├── _helpers.tpl # fragmentos reutilizables (no genera manifiestos)
├── deployment.yaml # ... y el resto de plantillas
└── tests/# Chart.yaml
apiVersion: v2 # v2 = Helm 3. No uses v1.
name: chart-rutas-norte
description: Plataforma de venta de billetes de Rutas Norte S.L.
version: 1.4.0 # versión del CHART, semántica
appVersion: "2.4.0" # versión de la APLICACIÓN que despliega
type: application
kubeVersion: ">=1.28.0-0" # si el clúster no cumple, install falla claro
maintainers:
- { name: Equipo de plataforma, email: [email protected] }
dependencies:
- name: redis
version: "20.1.0"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabledLa distinción entre version y appVersion confunde a todo el mundo. Regla: si cambias una plantilla, sube version. Si cambias la versión del software desplegado, sube appVersion (y también version, porque el chart ha cambiado).
Sobre los directorios especiales:
templates/: todo pasa por el motor de plantillas. Los ficheros que empiezan por_(como_helpers.tpl) se renderizan pero no generan manifiestos: sirven para definir fragmentos.NOTES.txtse imprime en pantalla tras instalar.charts/: aquí aterrizan los subcharts conhelm dependency update.crds/: YAML plano, sin plantillas. Helm las instala antes que el resto y nunca las actualiza ni las borra. Es una limitación deliberada y una fuente constante de fricción (apartado 13)..helmignore: igual que.dockerignore, qué no se incluye al empaquetar.
- Plantillas: sintaxis, funciones y control de flujo
Helm usa el motor de plantillas de Go con las funciones de la biblioteca Sprig. Todo va entre {{ }}.
Los objetos integrados
| Objeto | Contenido | Ejemplo |
|---|---|---|
.Values |
Los valores resueltos | .Values.replicaCount |
.Release |
Datos de la release | .Release.Name, .Release.Namespace, .Release.Revision, .Release.IsUpgrade |
.Chart |
Contenido de Chart.yaml |
.Chart.Name, .Chart.Version, .Chart.AppVersion |
.Capabilities |
Qué sabe hacer el clúster | .Capabilities.APIVersions.Has "..." |
.Files |
Ficheros del chart fuera de templates/ |
.Files.Get "config/app.conf" |
Funciones y tuberías
La tubería | pasa el resultado de la izquierda como último argumento de la función de la derecha.
# 'quote' añade comillas. Imprescindible con números que deben ser texto.
version: {{ .Chart.AppVersion | quote }} # -> "2.4.0"
# 'default' aporta un valor si el de la izquierda está vacío
replicas: {{ .Values.replicaCount | default 1 }}
# 'trunc 63' porque las etiquetas de Kubernetes tienen ese límite
name: {{ .Release.Name | trunc 63 | trimSuffix "-" }}
# 'required' ABORTA con tu mensaje si el valor falta. Mucho mejor
# que desplegar algo roto.
image: {{ required "Debes indicar image.repository" .Values.image.repository }}
# 'toYaml' convierte una estructura de values.yaml en YAML;
# 'nindent N' añade salto de línea + N espacios a cada línea
resources:
{{- toYaml .Values.resources | nindent 2 }}
# 'sha256sum' calcula un hash: lo usaremos para el reinicio automático
checksum/config: {{ .Values.config | toYaml | sha256sum }}indent frente a nindent causa más errores de sintaxis YAML que ninguna otra cosa: indent N añade N espacios a cada línea; nindent N hace lo mismo más un salto de línea al principio. Regla práctica: usa siempre {{- ... | nindent N }}, es el patrón que funciona.
Los guiones {{- y -}} eliminan el espacio en blanco anterior y posterior. Sin ellos, cada {{ if }} deja una línea en blanco en la salida y el YAML resultante es ilegible aunque sea válido.
Control de flujo
Se consideran falso: false, 0, la cadena vacía, nil, y las listas o mapas vacíos. Los operadores (eq, ne, lt, gt, and, or, not, empty) van delante, en notación prefija: {{- if and .Values.hpa.enabled (gt (int .Values.hpa.maxReplicas) 1) }}.
# 'with' comprueba que el valor no esté vacío Y cambia el '.' a ese valor
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 2 }}
{{- end }}Es el patrón idiomático para campos opcionales. Trampa: dentro de un with, el . ya no es la raíz; para el contexto global se usa $ (por ejemplo, {{ $.Release.Name }}).
# 'range' sobre una lista, o sobre un mapa capturando clave y valor
env:
{{- range $clave, $valor := .Values.env }}
- name: {{ $clave }}
value: {{ $valor | quote }}
{{- end }}define e include: fragmentos reutilizables
Aquí está la clave para no repetir las etiquetas de Rutas Norte en veinte plantillas.
{{/* templates/_helpers.tpl */}}
{{- define "rutas-norte.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "rutas-norte.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/* Etiquetas comunes de TODOS los objetos, con las convenciones de 02-07 */}}
{{- define "rutas-norte.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 }}
{{ include "rutas-norte.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: rutas-norte
entorno: {{ .Values.entorno | quote }}
{{- end }}
{{/* Etiquetas de SELECTOR. Son INMUTABLES en un Deployment (02-03):
si cambian, el upgrade falla. Por eso van aparte y contienen
lo MÍNIMO imprescindible, y no se tocan nunca más. */}}
{{- define "rutas-norte.selectorLabels" -}}
app.kubernetes.io/name: {{ include "rutas-norte.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}Se usan así: {{- include "rutas-norte.labels" . | nindent 4 }}.
include frente a template: template inserta el resultado pero no se puede encadenar con tuberías; include sí. Como casi siempre necesitas | nindent, usa include siempre.
- Crear el chart de Rutas Norte
values.yaml (defectos)
entorno: dev # dev | pre | pro
imageRegistry: registry.rutasnorte.example
apiReservas:
enabled: true
replicaCount: 1
image:
repository: api-reservas
tag: "" # vacío -> se usa .Chart.AppVersion
digest: "" # si se indica, tiene prioridad (08-05)
pullPolicy: IfNotPresent
service: { type: ClusterIP, port: 80, targetPort: 8080 }
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 256Mi }
# Configuración no sensible -> acaba en un ConfigMap (03-01)
config:
LOG_LEVEL: info
RESERVA_TTL_MINUTOS: "15"
PASARELA_URL: https://pagos.proveedorexterno.example/v2
# Secretos: SOLO el nombre de un Secret que ya existe.
# NUNCA se ponen valores sensibles en values.yaml (03-02).
existingSecret: api-reservas-credenciales
hpa: { enabled: false, minReplicas: 1, maxReplicas: 3, targetCPUUtilizationPercentage: 70 }
probes:
liveness: { path: /salud/vivo, initialDelaySeconds: 15, periodSeconds: 20 }
readiness: { path: /salud/listo, initialDelaySeconds: 5, periodSeconds: 5 }
nodeSelector: {}
tolerations: []
topologySpreadConstraints: []
# Contexto de seguridad común (08-02)
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile: { type: RuntimeDefault }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }El ConfigMap
{{- if .Values.apiReservas.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "rutas-norte.fullname" . }}-api-config
labels:
{{- include "rutas-norte.labels" . | nindent 4 }}
data:
{{- range $clave, $valor := .Values.apiReservas.config }}
{{ $clave }}: {{ $valor | quote }}
{{- end }}
{{- end }}Sencillo pero potente: añadir una variable nueva en values.yaml no toca la plantilla.
El Deployment
{{- if .Values.apiReservas.enabled }}
{{- $c := .Values.apiReservas }} {{/* variable local: ahorra el camino largo */}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "rutas-norte.fullname" . }}-api
labels:
{{- include "rutas-norte.labels" . | nindent 4 }}
app: api-reservas
spec:
{{- if not $c.hpa.enabled }}
# IMPORTANTE: si el HPA está activo (09-01) NO declaramos replicas.
# Si lo hiciéramos, cada 'helm upgrade' devolvería el Deployment al
# valor del chart, deshaciendo el escalado automático.
replicas: {{ $c.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "rutas-norte.selectorLabels" . | nindent 6 }}
app: api-reservas
strategy:
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
template:
metadata:
labels:
{{- include "rutas-norte.labels" . | nindent 8 }}
app: api-reservas
annotations:
# Hash de la configuración: si el ConfigMap cambia, cambia este
# hash, cambia la plantilla del pod y el Deployment hace rollout
# solo. Es el mecanismo de 03-03, ahora calculado automáticamente.
checksum/config: {{ include (print $.Template.BasePath "/api-reservas/configmap.yaml") . | sha256sum }}
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
spec:
serviceAccountName: {{ include "rutas-norte.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: api
image: {{ printf "%s/%s" .Values.imageRegistry (include "rutas-norte.imagenApi" .) }}
imagePullPolicy: {{ $c.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
ports:
- { name: http, containerPort: {{ $c.service.targetPort }} }
envFrom:
- configMapRef: { name: {{ include "rutas-norte.fullname" . }}-api-config }
{{- if $c.existingSecret }}
- secretRef: { name: {{ $c.existingSecret }} }
{{- end }}
livenessProbe:
httpGet: { path: {{ $c.probes.liveness.path }}, port: http }
initialDelaySeconds: {{ $c.probes.liveness.initialDelaySeconds }}
readinessProbe:
httpGet: { path: {{ $c.probes.readiness.path }}, port: http }
initialDelaySeconds: {{ $c.probes.readiness.initialDelaySeconds }}
resources:
{{- toYaml $c.resources | nindent 12 }}
volumeMounts:
# readOnlyRootFilesystem exige volúmenes para lo escribible
- { name: tmp, mountPath: /tmp }
volumes:
- { name: tmp, emptyDir: {} }
{{- with $c.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $c.topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}Cuatro puntos merecen atención:
replicascondicionado al HPA resuelve, dentro de Helm, media parte del conflicto anunciado en 09-01. La otra media (que Argo CD no marque deriva por ese campo) la veremos en 10-05.checksum/configrenderiza la plantilla del ConfigMap y le calcula el hash: automatiza exactamente lo que en 03-03 hacíamos a mano.withen los campos opcionales evita que salganodeSelector: {}o, peor,nodeSelector: null.- El Service y el HPA siguen el mismo patrón:
{{- if and .Values.apiReservas.enabled .Values.apiReservas.hpa.enabled }}envuelve el HPA, cuyoscaleTargetRef.nameusa el mismoinclude "rutas-norte.fullname"para no desincronizarse nunca del Deployment.
Y NOTES.txt cierra la instalación con instrucciones contextuales:
La plataforma Rutas Norte se ha desplegado en el entorno {{ .Values.entorno }}.
Release: {{ .Release.Name }} | Namespace: {{ .Release.Namespace }} | Revisión: {{ .Release.Revision }}
kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/part-of=rutas-norte
{{- if eq .Values.entorno "pro" }}
*** ENTORNO DE PRODUCCIÓN *** Verifica el rollout antes de cerrar la ventana:
kubectl rollout status deploy/{{ include "rutas-norte.fullname" . }}-api -n {{ .Release.Namespace }}
{{- end }}
- Valores por entorno
Aquí es donde desaparecen los 120 ficheros.
# entornos/values-dev.yaml
entorno: dev
apiReservas:
replicaCount: 1
image: { tag: dev-abc123f, pullPolicy: Always }
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { memory: 256Mi }
config:
LOG_LEVEL: debug
PASARELA_URL: https://pagos-sandbox.proveedorexterno.example/v2
hpa: { enabled: false }# entornos/values-pro.yaml
entorno: pro
apiReservas:
replicaCount: 4
image:
tag: "2.4.0"
# Digest inmutable: es lo que realmente se despliega (08-05)
digest: "sha256:9c1e4a7b3d2f8e6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a"
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { memory: 1Gi }
config:
LOG_LEVEL: warn
PASARELA_URL: https://pagos.proveedorexterno.example/v2
hpa: { enabled: true, minReplicas: 4, maxReplicas: 20, targetCPUUtilizationPercentage: 60 }
podDisruptionBudget: { enabled: true, minAvailable: 2 }
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels: { app: api-reservas }values-pre.yaml queda en medio: 2 réplicas, imagen rc-2.4.0, límite de 512Mi, HPA de 2 a 5 y la pasarela de pruebas. Tres ficheros de valores de 30 líneas han sustituido a 120 manifiestos duplicados.
--set frente a -f
| Aspecto | -f fichero.yaml |
--set clave=valor |
|---|---|---|
| Versionable en Git y revisable en una PR | Sí | No |
| Reproducible | Sí | Solo si alguien recuerda la orden exacta |
| Estructuras complejas | Natural | Sintaxis con corchetes, dolorosa |
| Tipos | Explícitos | Se infieren, con sorpresas (--set-string para forzar texto) |
Regla del equipo de Rutas Norte: -f para todo lo que define un entorno; --set solo para la etiqueta de imagen que inyecta la canalización ci-rutasnorte, que es lo único que legítimamente cambia en cada despliegue. Existe además --set-file clave=./fichero para volcar el contenido de un fichero como valor.
- El ciclo de vida de una release
# LA ORDEN QUE DEBES USAR SIEMPRE: instala si no existe, actualiza si sí.
# Idempotente, y por tanto apta para automatización.
helm upgrade --install rutas-norte ./chart-rutas-norte \
-n rutas-norte-pro --create-namespace \
-f entornos/values-pro.yaml \
--atomic --timeout 10mhelm install a secas falla si la release existe; helm upgrade a secas falla si no existe. upgrade --install cubre ambos casos.
| Bandera | Qué hace | Por qué importa |
|---|---|---|
--wait |
Espera a que los pods estén Ready y los Services tengan endpoints |
Sin esto, upgrade devuelve éxito en cuanto la API acepta los objetos, aunque los pods estén en CrashLoopBackOff |
--timeout 10m |
Cuánto espera (por defecto 5m) | Un StatefulSet grande o una migración lenta necesitan más |
--atomic |
Implica --wait, y si falla o expira hace rollback automático |
La red de seguridad de producción: o funciona, o el clúster queda como estaba |
--cleanup-on-fail |
Borra los objetos nuevos de un upgrade fallido | Evita dejar basura |
--force |
Borra y recrea los objetos en vez de parchearlos | Peligroso: provoca corte de servicio |
--reuse-values |
Reutiliza los anteriores y solo aplica los nuevos | Cómodo y traicionero: oculta qué está realmente aplicado |
En producción, --atomic --timeout no son opcionales. Sin ellos, un despliegue con la imagen mal escrita deja rutas-norte-pro con pods en ImagePullBackOff y Helm diciendo "deployed".
Consultar el estado
helm list -n rutas-norte-pro # releases del namespace
helm list -A --all # todas, incluidas las fallidas
helm history rutas-norte -n rutas-norte-proREVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
5 2026-08-01 16:45 CEST superseded chart-rutas-norte-1.4.0 2.4.0 Upgrade complete
6 2026-08-04 10:11 CEST failed chart-rutas-norte-1.4.0 2.4.1 Upgrade failed
7 2026-08-05 09:14 CEST deployed chart-rutas-norte-1.4.0 2.4.0 Rollback to 5Esa tabla cuenta una historia completa: la revisión 6 falló y la 7 fue un rollback a la 5.
helm get values rutas-norte -n rutas-norte-pro # los valores que TÚ diste
helm get values rutas-norte -n rutas-norte-pro --all # incluyendo los defectos
helm get manifest rutas-norte -n rutas-norte-pro # lo que está aplicado AHORA
helm get manifest rutas-norte -n rutas-norte-pro --revision 5helm get manifest es la orden que responde a "¿qué hay realmente desplegado en producción?", la pregunta que nadie sabía contestar al principio del módulo.
Rollback y desinstalación
helm rollback rutas-norte -n rutas-norte-pro # a la anterior
helm rollback rutas-norte 5 -n rutas-norte-pro --wait # a una concretaEl rollback crea una revisión nueva (la 8 sería una copia de la 5): el historial nunca retrocede, solo avanza.
Limitación crítica: el rollback afecta a los objetos de Kubernetes, no a los datos. Si la revisión 6 ejecutó una migración que añadió una columna a postgres-reservas, el rollback devuelve el código pero no deshace la migración. Las migraciones de esquema tienen que ser compatibles hacia atrás.
helm uninstall rutas-norte -n rutas-norte-dev # borra objetos e historial
helm uninstall rutas-norte -n rutas-norte-dev --keep-history # conserva el historial
helm list -n rutas-norte-dev --uninstalled # verlas
helm rollback rutas-norte 7 -n rutas-norte-dev # y resucitarla--keep-history es la opción prudente cuando desinstalas algo en un entorno importante y no estás del todo seguro. Ojo con los PVC: Helm no borra los creados por los volumeClaimTemplates de un StatefulSet (06-01), así que postgres-reservas conserva sus datos. Es bueno (protege contra el borrado accidental) y confuso (reinstalar reutiliza los datos viejos).
- Depuración: template, lint, dry-run y diff
# Renderizar en local, sin tocar el clúster ni necesitar conexión
helm template rutas-norte ./chart-rutas-norte -f entornos/values-pro.yaml
# Solo una plantilla
helm template rutas-norte ./chart-rutas-norte -f entornos/values-pro.yaml \
-s templates/api-reservas/deployment.yaml
# Comparar dos entornos: responde con exactitud a "¿en qué se diferencian?"
diff <(helm template rn ./chart-rutas-norte -f entornos/values-pre.yaml) \
<(helm template rn ./chart-rutas-norte -f entornos/values-pro.yaml)
# Encadenar con la validación real del servidor (01-06): esquema + webhooks
# de admisión (Kyverno, de 08-03), sin aplicar nada
helm template rutas-norte ./chart-rutas-norte -f entornos/values-pro.yaml | \
kubectl apply --dry-run=server -f - -n rutas-norte-proA diferencia de helm template, la variante --dry-run --debug sí contacta con el clúster, así que .Capabilities es real, valida contra la API y muestra los valores calculados. Es el paso previo obligatorio a cualquier despliegue en producción.
helm lint comprueba la estructura, que Chart.yaml esté completo y que las plantillas rendericen. Ponlo en ci-rutasnorte como comprobación obligatoria de cualquier cambio al chart.
helm diff: la red de seguridad
Con diferencia, la herramienta más valiosa de este apartado. Es un plugin, no viene incluido:
helm plugin install https://github.com/databus23/helm-diff
helm diff upgrade rutas-norte ./chart-rutas-norte -n rutas-norte-pro -f entornos/values-pro.yamlrutas-norte-pro, rutas-norte-api, Deployment (apps) has changed:
metadata:
annotations:
- checksum/config: 8f2a91bc4d7e...
+ checksum/config: c3e7d92a1b8f...
containers:
- name: api
- image: registry.rutasnorte.example/api-reservas:2.4.0
+ image: registry.rutasnorte.example/api-reservas:2.4.1
resources:
limits:
- memory: 1Gi
+ memory: 2GiCompara lo que hay en el clúster con lo que vas a aplicar, línea a línea. Es lo que impide desplegar un cambio de recursos que no querías, o darte cuenta de que actualizar un chart de terceros cambia treinta cosas inesperadas.
Norma para producción: nadie ejecuta helm upgrade contra rutas-norte-pro sin haber pegado antes la salida de helm diff en la petición de cambio. Es el equivalente de kubectl diff (01-06) para Helm.
Cuando la plantilla no compila, las causas más frecuentes son, por orden: indent donde tocaba nindent, un valor nil que no existe en values.yaml, una cadena con : sin comillas, y un {{- if }} sin su {{- end }}. Aísla la plantilla sospechosa con -s y usa --debug.
- Dependencias y subcharts
Rutas Norte necesita redis-cache. En vez de mantener su StatefulSet, podemos usar un chart existente, declarado en Chart.yaml (apartado 4) con condition: redis.enabled, tags para activar grupos y alias para renombrarlo.
helm dependency update ./chart-rutas-norte # resuelve, descarga y REESCRIBE Chart.lock
helm dependency build ./chart-rutas-norte # descarga EXACTAMENTE lo que dice Chart.lockChart.lock va a Git; charts/*.tgz no. Es exactamente la lógica de package-lock.json. En la canalización, siempre build; en tu máquina cuando quieres subir de versión, update.
Pasar valores a un subchart
Los valores del subchart se anidan bajo su nombre (o su alias), y global es visible desde el chart padre y desde todos los subcharts:
global:
imageRegistry: registry.rutasnorte.example
storageClass: rutas-norte-ssd
redis: # <- nombre del subchart
enabled: true
architecture: replication
auth: { enabled: true, existingSecret: redis-cache-credenciales }
master:
persistence: { storageClass: rutas-norte-ssd, size: 8Gi }
replica: { replicaCount: 2 }La mayoría de charts serios (Bitnami, entre otros) respetan global.imageRegistry y global.storageClass.
La trampa de sobreescribir valores de un subchart
Tres reglas que hay que interiorizar:
Regla 1: el chart padre gana, pero solo si acierta con la ruta. Si el subchart espera master.persistence.size y tú escribes redis.persistence.size, Helm no avisa: puedes escribir redis.tamañoDelDisco: 500Gi y no pasa absolutamente nada. Verifica siempre con helm template -s charts/redis/templates/... que el cambio surtió efecto.
Regla 2: -f sobreescribe, no fusiona listas. Si values.yaml tiene extraFlags: ["--appendonly yes", "--maxmemory 512mb"] y values-pro.yaml pone extraFlags: ["--maxmemory 2gb"], el resultado es solo ["--maxmemory 2gb"]. Se perdió --appendonly yes. Los mapas se fusionan clave a clave; las listas se sustituyen enteras. Fuente inagotable de sorpresas.
Regla 3: un subchart no puede leer los valores del padre salvo a través de global.
- Hooks
Un hook es un objeto de Kubernetes que Helm crea en un momento concreto del ciclo de vida, fuera del flujo normal: pre-install, post-install, pre-upgrade, post-upgrade, pre-rollback, post-rollback, pre-delete, post-delete y test.
El caso real: migración del esquema de la base de datos
api-reservas 2.4.0 necesita una columna nueva en postgres-reservas. Si desplegamos el código antes de migrar, los pods fallan. La solución es un pre-upgrade que bloquea el despliegue hasta que la migración termine.
apiVersion: batch/v1
kind: Job
metadata:
# El nombre incluye la revisión: cada upgrade crea un Job distinto
name: {{ include "rutas-norte.fullname" . }}-migracion-{{ .Release.Revision }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
# Los pesos menores van primero: permite encadenar hooks
"helm.sh/hook-weight": "-5"
# before-hook-creation: borra el anterior antes de crear el nuevo
"helm.sh/hook-delete-policy": before-hook-creation
spec:
backoffLimit: 2
activeDeadlineSeconds: 600
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
containers:
- name: migrador
image: {{ .Values.imageRegistry }}/api-reservas-migraciones:{{ .Chart.AppVersion }}
command: ["/app/migrar", "--hasta", "{{ .Chart.AppVersion }}"]
env:
- name: BD_PASSWORD
valueFrom:
secretKeyRef: { name: {{ .Values.apiReservas.existingSecret }}, key: bd-password }sequenceDiagram
participant U as Operador
participant H as Helm
participant K as Clúster
U->>H: helm upgrade --install --atomic
H->>K: crea el Job pre-upgrade (migración)
alt Migración correcta
K-->>H: Job Complete
H->>K: aplica Deployment, Service, HPA, ConfigMap
H->>K: espera a que los pods estén Ready (--wait)
H-->>U: release desplegada (revisión N+1)
else Migración fallida
K-->>H: Job Failed
H->>H: aborta: NO toca los objetos de la aplicación
H-->>U: error; el clúster sigue en la revisión N
end
Lo importante: si la migración falla, los pods de api-reservas ni se enteran. Siguen sirviendo la versión anterior. Sin el hook, habríamos desplegado código nuevo contra un esquema viejo.
Políticas de borrado
before-hook-creation (por defecto) borra el recurso anterior justo antes de crear el nuevo; hook-succeeded lo borra si termina bien; hook-failed lo borra si falla. Se combinan con comas.
Consejo firme: no uses hook-failed. Cuando una migración falle a las tres de la mañana, lo único que querrás es kubectl logs del Job que falló. Si la política lo borró, te quedas sin nada.
Hooks de prueba
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "rutas-norte.fullname" . }}-prueba-api"
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": before-hook-creation
spec:
restartPolicy: Never
containers:
- name: curl
image: curlimages/curl:8.10.1
args: ["-sS", "--fail", "--max-time", "10",
"http://{{ include "rutas-norte.fullname" . }}-api/salud/listo"]Es una prueba de humo estupenda para ejecutar automáticamente tras desplegar en rutas-norte-pre.
- Empaquetar y publicar
helm package ./chart-rutas-norte # -> chart-rutas-norte-1.4.0.tgz
# Firmarlo con GPG, complementando la firma de imágenes de 08-05
helm package ./chart-rutas-norte --sign --key '[email protected]'
helm verify chart-rutas-norte-1.4.0.tgzDesde Helm 3.8, un registro de contenedores puede alojar charts directamente. Rutas Norte ya tiene registry.rutasnorte.example; no necesita otro servidor.
helm registry login registry.rutasnorte.example -u ci-rutasnorte
helm push chart-rutas-norte-1.4.0.tgz oci://registry.rutasnorte.example/charts
# Consumirlo: fíjate en que NO hace falta 'helm repo add'
helm upgrade --install rutas-norte \
oci://registry.rutasnorte.example/charts/chart-rutas-norte \
--version 1.4.0 -n rutas-norte-pro -f entornos/values-pro.yaml --atomicVentajas del OCI frente al repositorio HTTP clásico (helm repo index sobre un directorio servido estáticamente): mismas credenciales y mismo control de acceso que las imágenes, sin index.yaml que mantener, versionado por digest, y el mismo escaneo de seguridad que ya tienes montado (08-06).
- Qué hace mal Helm
Helm resuelve un problema real, pero tiene defectos honestos que conviene conocer.
1. Es un motor de texto, no entiende YAML. No manipula estructuras: concatena cadenas y al final intenta interpretar el resultado. De ahí vienen nindent, los {{-, y que un espacio de más rompa el despliegue. Además, las plantillas no son YAML válido: no puedes abrirlas y que el editor valide el esquema de Kubernetes, ni aplicarlas con kubectl apply -f. Pierdes todo el instrumental que existe alrededor del YAML.
2. Las plantillas complejas se vuelven ilegibles. Charts de terceros grandes tienen plantillas de 300 líneas con seis niveles de condicionales. Depurarlas es doloroso.
3. Las CRDs son un problema. Helm instala lo que hay en crds/ pero nunca lo actualiza ni lo borra (razón declarada: borrar una CRD elimina todos sus recursos, lo que sería catastrófico). Consecuencia: actualizar cert-manager de 1.15 a 1.16 puede requerir kubectl apply -f cert-manager.crds.yaml a mano antes del helm upgrade.
4. El estado vive en el clúster, no en Git. Si alguien hace kubectl scale a mano, Helm no lo detecta hasta el siguiente upgrade, y entonces lo pisa sin avisar. No hay reconciliación continua: solo actúa cuando alguien ejecuta una orden. Esa es exactamente la carencia que resuelve GitOps (10-05).
5. Depender de charts ajenos es depender de decisiones ajenas. Si necesitas cambiar algo que el autor no parametrizó, tus opciones son abrir una petición al proyecto, bifurcar el chart o post-procesar la salida. Ninguna es cómoda.
Por qué mucha gente prefiere Kustomize
| Crítica a Helm | Respuesta de Kustomize |
|---|---|
| Las plantillas no son YAML válido | Los manifiestos base son YAML normal, aplicable con kubectl |
nindent, {{-, errores de espaciado |
No hay motor de plantillas |
| Estado en el clúster | Sin estado: solo genera YAML |
| Hay que aprender Go templates y Sprig | Solo un fichero, kustomization.yaml |
| Herramienta externa | Viene dentro de kubectl |
Y donde Helm sigue ganando claramente: el ecosistema (miles de charts listos), el empaquetado y la distribución (un .tgz versionado y publicable), la gestión del ciclo de vida (rollback, history, uninstall que sabe qué borrar), los hooks y la lógica condicional real con bucles.
La conclusión práctica del sector, y la que adoptaremos en Rutas Norte: Helm para consumir software de terceros, Kustomize para las aplicaciones propias. cert-manager y Prometheus por Helm; tienda-web, api-reservas y compañía por Kustomize.
Errores Comunes y Consejos
1. No fijar --version. Instalas cert-manager hoy y obtienes 1.16.1; reinstalas en marzo y obtienes 1.19, con cambios incompatibles. Fija la versión siempre y anótala en el repositorio.
2. Poner secretos en values.yaml. El fichero va a Git. Usa existingSecret apuntando a un Secret que gestiona otro sistema (Sealed Secrets, SOPS, External Secrets Operator: 03-02 y 10-05).
3. Confundir indent con nindent. El error de sintaxis más frecuente. Usa {{- toYaml . | nindent N }} como patrón por defecto.
4. Declarar replicas fijas con un HPA activo. Cada helm upgrade devuelve el Deployment a las réplicas del chart, cancelando el escalado automático hasta que el HPA reacciona. Condiciona el campo, como en el apartado 6.
5. helm upgrade sin --atomic en producción. Un despliegue fallido deja el clúster a medias y la release en failed. Con --atomic, vuelve solo al estado anterior.
6. Abusar de --reuse-values. Es cómodo hasta que nadie sabe qué valores están realmente aplicados. Usa siempre -f fichero.yaml completo y versionado.
7. Escribir valores para un subchart con la ruta equivocada. Helm los ignora sin decir nada. Comprueba con helm template -s que el cambio llegó.
8. Creer que las listas se fusionan. No: se sustituyen enteras. Si redefines extraFlags en values-pro.yaml, pierdes los del values.yaml base.
9. Cambiar las etiquetas de selector entre versiones del chart. spec.selector de un Deployment es inmutable (02-03). Si tu helper selectorLabels cambia, el upgrade falla con "field is immutable" y hay que borrar y recrear el objeto, con corte de servicio. Mantén selectorLabels mínimo y estable para siempre.
10. Actualizar un chart de terceros sin helm diff. El salto de una versión menor puede cambiar treinta campos. Míralos antes.
11. hook-delete-policy: hook-failed en las migraciones. Te borra justo el Job cuyos logs necesitas.
12. Olvidar helm dependency update tras editar Chart.yaml. El despliegue usa el charts/ viejo y no entiendes por qué no cambia nada.
13. Desinstalar sin --keep-history en un entorno importante. Sin historial no hay rollback. Y recuerda que los PVC sobreviven, lo que puede sorprenderte al reinstalar.
Ejercicios
Ejercicio 1: plantillar worker-notificaciones
Añade al chart el componente worker-notificaciones: un Deployment sin Service (no recibe tráfico), con enabled condicional, recursos y réplicas parametrizados, y las etiquetas comunes vía include. Debe tener un ScaledObject de KEDA (09-04) solo si worker.keda.enabled es cierto, y en ese caso el Deployment no debe declarar replicas. Escribe también los tres bloques de valores por entorno.
Ejercicio 2: subchart, valores globales y la trampa de las listas
Añade redis como dependencia con condition, configúralo para que use la StorageClass rutas-norte-ssd mediante un valor global, y demuestra con helm template que el valor llegó. Después, define master.extraFlags en values.yaml con dos banderas y sobreescríbelo en values-pro.yaml con una sola; comprueba qué sale y explica el resultado.
Ejercicio 3: hook de migración con verificación previa
Escribe dos hooks encadenados por peso: uno pre-upgrade con peso -10 que verifique que postgres-reservas acepta conexiones, y otro con peso -5 que ejecute la migración. Si el primero falla, el segundo no debe ejecutarse y el despliegue debe abortar sin tocar la aplicación. Explica cómo verificarías todo esto sin romper rutas-norte-pro.
Soluciones
Solución 1
# templates/worker/deployment.yaml
{{- if .Values.worker.enabled }}
{{- $w := .Values.worker }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "rutas-norte.fullname" . }}-worker
labels: {{- include "rutas-norte.labels" . | nindent 4 }}
spec:
{{- if not $w.keda.enabled }}
replicas: {{ $w.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "rutas-norte.selectorLabels" . | nindent 6 }}
app: worker-notificaciones
template:
metadata:
labels:
{{- include "rutas-norte.labels" . | nindent 8 }}
app: worker-notificaciones
spec:
securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: worker
image: {{ .Values.imageRegistry }}/worker-notificaciones:{{ $w.image.tag | default .Chart.AppVersion }}
envFrom: [{ secretRef: { name: {{ $w.existingSecret }} } }]
resources: {{- toYaml $w.resources | nindent 12 }}
{{- end }}# templates/worker/scaledobject.yaml
{{- if and .Values.worker.enabled .Values.worker.keda.enabled }}
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: {{ include "rutas-norte.fullname" . }}-worker
spec:
scaleTargetRef: { name: {{ include "rutas-norte.fullname" . }}-worker }
minReplicaCount: {{ .Values.worker.keda.minReplicas }}
maxReplicaCount: {{ .Values.worker.keda.maxReplicas }}
triggers:
- type: redis
metadata:
address: {{ .Values.worker.keda.redisAddress }}
listName: cola-correos
listLength: {{ .Values.worker.keda.listLength | quote }}
{{- end }}# values.yaml (defectos)
worker:
enabled: true
replicaCount: 1
existingSecret: worker-credenciales
image: { tag: "" }
resources: { requests: {cpu: 50m, memory: 64Mi}, limits: {memory: 256Mi} }
keda: { enabled: false, minReplicas: 0, maxReplicas: 10,
listLength: 20, redisAddress: "redis-cache:6379" }
# dev: worker.keda.enabled: false
# pre: worker.keda: { enabled: true, minReplicas: 0, maxReplicas: 5 }
# pro: worker.keda: { enabled: true, minReplicas: 1, maxReplicas: 30 }
# worker.resources.requests: { cpu: 200m, memory: 256Mi }Solución 2
# Chart.yaml
dependencies:
- { name: redis, version: "20.1.0",
repository: "https://charts.bitnami.com/bitnami", condition: redis.enabled }# values.yaml
global: { storageClass: rutas-norte-ssd }
redis:
enabled: true
master: { extraFlags: ["--appendonly yes", "--maxmemory 512mb"] }
---
# values-pro.yaml
redis:
master: { extraFlags: ["--maxmemory 4gb"] }helm dependency update ./chart-rutas-norte
helm template rn ./chart-rutas-norte -f entornos/values-pro.yaml \
-s charts/redis/templates/master/statefulset.yaml | grep -E 'storageClassName|maxmemory|appendonly'Explicación: global.storageClass llegó porque el chart de Bitnami lo respeta explícitamente. --appendonly yes desapareció: las listas no se fusionan, se sustituyen enteras. Para conservar ambas hay que repetir las dos en values-pro.yaml.
Solución 3
# templates/hooks/00-verificar-bd.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "rutas-norte.fullname" . }}-verif-bd-{{ .Release.Revision }}
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
backoffLimit: 3
activeDeadlineSeconds: 120
template:
spec:
restartPolicy: Never
containers:
- name: verificar
image: postgres:16.4
command: ["sh","-c","pg_isready -h postgres-reservas -p 5432 -t 10"]El segundo hook es el Job de migración del apartado 11, con "helm.sh/hook-weight": "-5".
Cómo funciona: Helm ordena los hooks del mismo evento por peso ascendente y espera a que cada uno termine antes de lanzar el siguiente. Si el de peso -10 falla, el de -5 nunca se crea y el upgrade aborta sin tocar Deployments ni Services.
Verificación sin romper producción:
# 1. Ver que se generan y en qué orden
helm template rn ./chart-rutas-norte -f entornos/values-pro.yaml | grep -B2 hook-weight
# 2. Probar el flujo completo en pre
helm upgrade --install rutas-norte ./chart-rutas-norte \
-n rutas-norte-pre -f entornos/values-pre.yaml --atomic --timeout 10m
# 3. Provocar el fallo a propósito: apuntar a un host inexistente
helm upgrade rutas-norte ./chart-rutas-norte -n rutas-norte-pre \
-f entornos/values-pre.yaml --set bd.host=no-existe --atomic
kubectl get jobs -n rutas-norte-pre # solo debe existir verif-bd, NO migracion
helm history rutas-norte -n rutas-norte-pre # la revisión aparece como failed
kubectl get deploy -n rutas-norte-pre -o wide # la imagen NO ha cambiadoConclusión
Helm ha convertido los 120 ficheros de Rutas Norte en un chart con veinte plantillas y tres ficheros de valores. Lo esencial:
- Un chart es el paquete, una release es su instalación con nombre, y Helm guarda estado en el clúster en forma de Secrets de release: eso es lo que hace posibles
history,rollbackyuninstall. - Consumir charts de terceros con criterio significa fijar
--version, leerhelm show valuesantes de instalar, y guardar tus valores en un fichero versionado. Es lo que ahora sabemos que estábamos haciendo con cert-manager ykube-prometheus-stack. - Las plantillas combinan
.Values,.Release,.Charty.Capabilitiescon funciones (default,quote,toYaml,nindent,required,sha256sum) y control de flujo (if,with,range);define/includeen_helpers.tplcentralizan las etiquetas del proyecto. - El ciclo de vida profesional es
helm upgrade --install ... --atomic --timeout, con-fen vez de--set, y conhelm diffcomo paso obligatorio antes de tocarrutas-norte-pro. - Los hooks ordenan las migraciones de base de datos respecto al despliegue, con el enorme beneficio de que un fallo en la migración no llega a tocar la aplicación.
- Y Helm tiene defectos reales: es un motor de texto, las plantillas dejan de ser YAML válido, las CRDs no se actualizan, y el estado vive en el clúster y no en Git.
Ese último punto abre las dos lecciones que siguen. La siguiente, Kustomize, ataca la crítica de las plantillas desde el extremo opuesto: sin motor de plantillas, sin estado, con manifiestos que siguen siendo YAML válido y que se personalizan mediante superposiciones. Veremos su modelo de base y overlays, sus generadores de ConfigMaps con hash automático —que resuelve limpiamente el reinicio ante cambios de configuración de 03-03—, y migraremos el k8s/ de Rutas Norte completo. Al final compararemos las dos herramientas de forma honesta y veremos cómo combinarlas.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
