Misión Helm: industrializar un despliegue multi-entorno
15 min
Proyecto 13 — Helm sobre Kubernetes · Nivel intermedio → avanzado · Duración estimada: 4 a 6 h
Partes de una aplicación compuesta de dos services Python (un portail visual y una api backend) y vas a desplegarla tres veces lado a lado — en DEV (azul), STAGING (naranja), PROD (verde) — con un solo Chart Helm y tres archivos de valores. Al final harás un helm upgrade y luego un helm rollback, y repararás tres templates llenos de bugs reales vistos en empresa.
Trabajas en un equipo donde cada nueva versión de una aplicación debe pasar por tres entornos:
DEV — caja de arena de los desarrolladores, datos desechables, una sola réplica.
STAGING — preproducción, datos de prueba, dos réplicas para validar la escalabilidad.
PROD — producción, datos reales, tres réplicas mínimo, ningún downtime tolerado.
Hoy, el equipo copia y pega los mismos manifiestos YAML para cada entorno cambiando a mano los valores distintos. Resultado: los archivos divergen, un parche aplicado en dev no llega a prod, y un despliegue pide medio día.
Tu misión: industrializar todo eso con Helm. Un solo Chart, tres archivos de valores, un comando por entorno. Demostrarás que funciona mostrando tres tableros lado a lado en el navegador — cada uno con su propio color, su propio número de Pods y su propio mensaje.
Conceptos esenciales antes de empezar
Este documento es autosuficiente. No necesitas ninguna referencia externa para terminarlo.
1. El papel de Helm en una frase
Helm genera manifiestos Kubernetes a partir de templates y de variables. Donde kubectl apply toma un YAML estático, Helm toma un template YAML y un archivo de valores, produce el YAML final y lo aplica como una unidad versionada que se llama una release.
Una release es una instalación de un chart. Un mismo chart puede instalarse varias veces, cada una con un nombre de release distinto (hedge-dev, hedge-staging, hedge-prod) — es el fundamento del multi-entorno.
{{ .Release.Name }} cambia en cada install, {{ .Chart.Name }} se queda idéntico. Retén este contraste: es central.
6. La regla de oro del selector inmutable
El campo spec.selector.matchLabels de un Deployment queda fijado de una vez por todas en la creación. Si tu template pone en ese campo un valor que puede cambiar (como una versión, un entorno, una fecha), el primer helm install funcionará, pero el primer helm upgrade fallará con:
spec.selector: Invalid value: ...: field is immutable
Regla para grabar en mármol: en matchLabels, pon solo cosas que no cambiarán NUNCA para esta instancia — típicamente name, instance, component.
La arquitectura objetivo: DEV / STAGING / PROD
Vas a desplegar el mismo chart en tres namespaces distintos, cada uno con sus parámetros:
Parámetro
DEV
STAGING
PROD
Namespace
hedge-dev
hedge-staging
hedge-prod
Release name
hedge-dev
hedge-staging
hedge-prod
Color de la franja
azul#2563eb
naranja#ea580c
verde#16a34a
Mensaje
«Entorno de desarrollo…»
«Preproducción — datos de prueba únicamente»
«Producción — cada acción tiene un impacto real»
Réplicas portail
1
2
3
Réplicas api
1
2
3
Puerto expuesto (NodePort)
30130
30131
30132
URL de prueba
http://localhost:30130
http://localhost:30131
http://localhost:30132
Al final del TP, abres tres pestañas lado a lado y ves tres dashboards de colores distintos, cada uno mostrando su entorno, su versión, sus Pods y el estado de su backend.
Disposición de los archivos
Partes del árbol siguiente — el ANEXO da el contenido exacto de cada archivo:
projet13-kubernetes-helm-tp/├── 00-ENONCE.md <- este documento│├── apps/ <- EL CÓDIGO (ANEXO A) — NO MODIFICAR│ ├── portail/│ │ ├── app.py│ │ ├── requirements.txt│ │ └── Dockerfile│ └── api/│ ├── app.py│ ├── requirements.txt│ └── Dockerfile│├── chart/ <- EL CHART A COMPLETAR│ ├── Chart.yaml <- esqueleto (ANEXO B)│ ├── values.yaml <- valores por defecto (ANEXO B)│ ││ ├── environments/ <- TE TOCA A TI│ │ ├── values-dev.yaml <- esqueleto TODO (ANEXO B)│ │ ├── values-staging.yaml <- esqueleto TODO (ANEXO B)│ │ └── values-prod.yaml <- esqueleto TODO (ANEXO B)│ ││ ├── templates/ <- TE TOCA A TI│ │ ├── _helpers.tpl <- esqueleto TODO (ANEXO B)│ │ ├── portail-deployment.yaml <- esqueleto TODO (ANEXO B)│ │ ├── portail-service.yaml <- esqueleto TODO (ANEXO B)│ │ ├── api-deployment.yaml <- esqueleto TODO (ANEXO B)│ │ └── api-service.yaml <- esqueleto TODO (ANEXO B)│ ││ └── casses/ <- SUMINISTRADOS pero DEFECTUOSOS (ANEXO C)│ ├── casse-1-configmap.yaml│ ├── casse-2-worker-deployment.yaml│ └── casse-3-cache-deployment.yaml│├── outils/│ └── valider.ps1 <- SUMINISTRADO (ANEXO D)│└── RAPPORT.md <- A REDACTAR por ti
Punto crucial: los archivos de chart/casses/no están en chart/templates/. Helm por tanto no los carga automáticamente. La misión 6 te pedirá copiarlos uno a uno en templates/ para observar el bug, y luego repararlos antes de conservarlos.
Las reglas del juego
Prohibición absoluta de modificar la carpeta apps/. El código de la aplicación ya está escrito — tú eres el DevOps, no el desarrollador.
Solo modificas los archivos de la carpeta chart/.
Ninguna configuración de entorno codificada a pelo en un template: replicas, nodePort, color, mensaje, entorno — todo debe venir de un .Values.*.
Los 3 archivos values-<env>.yaml deben diferir únicamente por los valores que distinguen DEV, STAGING y PROD. Un archivo values-prod.yaml que redefine inútilmente image.repository o service.targetPort es un error — esas cosas vienen de values.yaml.
Trabajas sobre el Kubernetes integrado en Docker Desktop.
Preparación
Requisitos previos — a verificar una sola vez
Docker Desktop está arrancado y Kubernetes está activado (Settings → Kubernetes → Enable Kubernetes).
Docker Desktop dispone de al menos 4 GB de RAM asignados (Settings → Resources → Memory ≥ 4 GB). Este TP hace correr 12 Pods a la vez (1+1 + 2+2 + 3+3).
Helm está instalado:
powershell
helm version --short # debe mostrar v3.x o v4.x
Si no: winget install Helm.Helm (o choco install kubernetes-helm).
Estás en el clúster correcto:
powershell
kubectl config use-context docker-desktopkubectl get nodes # docker-desktop Ready
Construcción de las imágenes
El chart referencia dos imágenes locales que debes construir una sola vez:
portail-deployment.yaml — un Deployment que usa .Values.portail.replicas, .Values.portail.image.*, e inyecta las variables de entorno ENVIRONMENT, APP_VERSION, THEME_COLOR, BANNIERE_MESSAGE, BACKEND_URL, REPLICAS_INFO.
portail-service.yaml — un Service NodePort que apunta a los Pods del portal.
api-deployment.yaml — un Deployment para la api (variables ENVIRONMENT, APP_VERSION).
api-service.yaml — un Service ClusterIP.
Punto clave: la variable BACKEND_URL del portal debe contener el nombre del Service api construido con {{ .Release.Name }} (por ejemplo http://hedge-dev-api), no un nombre a pelo.
Validación:
powershell
helm template check .\chart -f .\chart\environments\values-dev.yaml# debe mostrar 2 Deployments + 2 Services, todos prefijados por "check-"
Misión 3 — Escribir helpers limpios (15 puntos)
Completa chart/templates/_helpers.tpl con tres helpers:
kubectl get pods -n hedge-dev -l app.kubernetes.io/component=portail
Consulta el historial:
powershell
helm history hedge-dev -n hedge-dev
Ves al menos 2 revisiones.
Anula el upgrade volviendo a la revisión 1:
powershell
helm rollback hedge-dev 1 -n hedge-dev
Comprueba que se ha vuelto a 1 solo Pod portail, y que el historial muestra una nueva revisión de tipo Rollback:
powershell
kubectl get pods -n hedge-dev -l app.kubernetes.io/component=portailhelm history hedge-dev -n hedge-dev
Pregunta a tratar en el informe: ¿cuál es la diferencia fundamental entre helm upgrade --set portail.replicas=5 y kubectl scale deploy/hedge-dev-portail --replicas=5? ¿Por qué Helm prefiere que se pase por él?
La carpeta chart/casses/ contiene tres templates ya escritos que compilan pero introducen cada uno un bug real encontrado en empresa. Debes, para cada uno:
copiarlo a chart/templates/;
reproducir el síntoma descrito al inicio del archivo;
diagnosticar la causa leyendo el mensaje de error;
reparar (modificando el template en chart/templates/, no el original en casses/);
probar que la avería ha desaparecido.
Archivo
Componente añadido
Naturaleza del bug
casse-1-configmap.yaml
Un ConfigMap global
Colisión de nombre entre releases
casse-2-worker-deployment.yaml
Un Deploymentworker
Selector inmutable violado en el primer helm upgrade
casse-3-cache-deployment.yaml
Un Deploymentcache
Path de valor erróneo (typo silencioso)
Truco de investigación:
powershell
# render SECO de un solo template (no instala nada)helm template hedge-dev .\chart -f .\chart\environments\values-dev.yaml ` --show-only templates/casse-3-cache-deployment.yaml --debug
Este comando imprime exactamente lo que Helm enviaría a Kubernetes. Es tu primera herramienta de diagnóstico — úsala sin moderación.
Misión 7 — Bonus: el refinamiento (5 puntos)
A elegir, uno solo basta:
a) Añade un hook pre-install (Job) que muestre Bienvenue dans <environnement> en los logs de Helm. La release debe esperar al final del Job antes de continuar.
b) Haz que el número de réplicas sea dinámico con un valor de values.yaml que tenga una estructura anidada (por ejemplo portail.autoscaling.enabled, portail.autoscaling.min, portail.autoscaling.max) y genera condicionalmente un HorizontalPodAutoscaler según .enabled.
c) Añade un NOTES.txt en templates/ que muestre, después de cada helm install, la URL exacta para abrir el dashboard (con el nodePort correcto según las Values).
Validación automática
Un script te da tu puntuación en cualquier momento:
La carpeta chart/ completa, en estado de marcha (helm lint limpio).
Un RAPPORT.md que contenga:
la salida de helm list -A que muestre tus 3 releases;
una captura por entorno (3 dashboards de colores);
el historial completo de hedge-dev (con upgrade + rollback);
para cada avería de la misión 6: comando de diagnóstico, causa, correctivo, prueba;
tus respuestas a las preguntas de reflexión.
La salida final de .\outils\valider.ps1.
Preguntas de reflexión
¿Por qué el campo spec.selector.matchLabels es inmutable en Kubernetes? ¿Qué problema resuelve esta restricción?
Hoy tienes 3 entornos. Mañana el equipo DevSecOps pide un 4.º («pre-prod»). ¿Qué archivos creas y cuáles no tocas?
¿Cuál es la diferencia entre helm upgrade --set replicas=5 y kubectl scale, desde el punto de vista de la trazabilidad y del rollback?
El portal muestra «backend OK» — ¿por qué esta información es más fiable que un simple kubectl get svc api?
¿Qué ocurre si eliminas un Pod con kubectl delete pod, habiendo sido creado por un Deployment vía Helm? ¿Helm se entera de la «pérdida»?
Un compañero te propone poner app.kubernetes.io/version: {{ .Chart.AppVersion }} en el matchLabels de un Deployment. ¿Qué le respondes?
Baremo
Elemento
Puntos
Misión 1 — Chart válido y lint limpio
10
Misión 2 — Templating portail + api
20
Misión 3 — Helpers y labels reutilizables
15
Misión 4 — Tres entornos lado a lado
20
Misión 5 — Upgrade + rollback rastreados
10
Misión 6 — Diagnóstico + reparación de las 3 averías
20
Calidad del informe y justificación de las elecciones
5
Bonus — Misión 7
+5
Total
100 (+5)
Penalizaciones:
−10 por valor de entorno codificado a pelo en un template (replicas: 3 literal en vez de .Values....).
−5 por redefinición inútil en un values-<env>.yaml (un valor que no tiene razón de diferir entre entornos).
−10 por modificación de un archivo de apps/.
Caja de herramientas Helm
powershell
# ANÁLISIS (ningún despliegue)helm lint .\chart # sintaxis + buenas prácticashelm template <release> .\chart -f <values.yaml> # render completohelm template <release> .\chart -f <values.yaml> --show-only templates/<fichier> # render puntualhelm template <release> .\chart -f <values.yaml> --debug # con trazashelm show values .\chart # valores por defecto# DESPLIEGUEhelm install <release> .\chart -f <values.yaml> -n <ns> --create-namespacehelm upgrade <release> .\chart -f <values.yaml> -n <ns>helm upgrade <release> .\chart -f <values.yaml> --set portail.replicas=5 -n <ns>helm rollback <release> <revision> -n <ns>helm uninstall <release> -n <ns># OBSERVACIÓNhelm list -A # todas las releaseshelm status <release> -n <ns>helm history <release> -n <ns>helm get values <release> -n <ns> # los valores activoshelm get manifest <release> -n <ns> # los manifiestos aplicados
Los 3 reflejos en caso de bug:
Empieza siempre por helm template — es un render SECO, sin riesgo, que muestra exactamente lo que se va a enviar a Kubernetes.
Lee la ruta en el mensaje de error — Helm siempre da el archivo + la línea + el path .Values.* fallido.
helm get manifest te muestra lo que está actualmente en el clúster (útil para comparar con lo que genera tu nuevo template).
ANEXO A — Las aplicaciones
No modifiques ninguno de estos archivos. Cópialos tal cual en las rutas indicadas.
A.1 — El portal (tablero multi-entorno)
Todos los valores mostrados vienen de variables de entorno inyectadas por Helm. La misma imagen se comporta de forma distinta según los env: del Deployment.
Archivo: apps/portail/app.py
python
"""Portail — tablero multi-entorno.Este Pod muestra el entorno en el que corre (DEV / STAGING / PROD),la versión de la aplicación, el número de réplicas y el estado del backend.Todos los valores mostrados vienen de VARIABLES DE ENTORNO inyectadaspor Helm desde values-<env>.yaml. El mismo código se adapta a cadaentorno sin ninguna modificación."""import osimport socketimport timeimport urllib.errorimport urllib.requestfrom flask import Flask, jsonify, requestapp = Flask(__name__)DEMARRAGE = time.time()def cfg(): return { "env": os.environ.get("ENVIRONMENT", "inconnu"), "version": os.environ.get("APP_VERSION", "0.0.0"), "theme": os.environ.get("THEME_COLOR", "#64748b"), "message": os.environ.get("BANNIERE_MESSAGE", "Deploye avec Helm"), "backend_url": os.environ.get("BACKEND_URL", "http://api"), "replicas_info": os.environ.get("REPLICAS_INFO", "?"), "pod": socket.gethostname(), "uptime": int(time.time() - DEMARRAGE), }def tester_backend(url): try: with urllib.request.urlopen(url + "/ping", timeout=1.5) as reponse: corps = reponse.read(200).decode("utf-8", "ignore") return "ok", corps.strip() except urllib.error.HTTPError as err: return "http", "HTTP %s" % err.code except Exception as err: return "ko", type(err).__name__@app.route("/health")def health(): return "OK", 200@app.route("/api-json")def api_json(): """Ruta útil para la validación automática.""" c = cfg() etat, detail = tester_backend(c["backend_url"]) return jsonify(pod=c["pod"], env=c["env"], version=c["version"], backend=etat, backend_detail=detail, uptime=c["uptime"])@app.route("/")def accueil(): c = cfg() etat, detail = tester_backend(c["backend_url"]) # ... (plantilla HTML completa en el archivo — no se repite aquí para que siga legible)
El archivo completo se suministra en apps/portail/app.py.
Copia estos archivos y luego sustituye cada TODO por el valor correcto.
Las líneas precedidas de # ? son preguntas a resolver: te toca decidir cómo completar el código.
Archivo: chart/Chart.yaml
yaml
# ? Rellena los campos obligatorios de un Chart Helm.# ? apiVersion debe valer v2 (la v1 está deprecada desde Helm 3).# ? type es "application" (por oposición a "library").apiVersion: TODOname: hedgedescription: TODOtype: TODOversion: 0.1.0appVersion: "1.0.0"
Archivo: chart/values.yaml
yaml
# values.yaml — valores por defecto del chart hedge.# Cada entorno aporta un archivo values-<env>.yaml que SOBRESCRIBE# estos valores por encima. Mantén este archivo NEUTRO (ningún valor específico# de un entorno).environment: defaultbanniere: message: "Chart Helm — application multi-environnement" couleur: "#64748b"portail: image: repository: hedge-portail tag: "1.0" pullPolicy: IfNotPresent replicas: 1 service: type: NodePort port: 80 targetPort: 5000 nodePort: 30130api: image: repository: hedge-api tag: "1.0" pullPolicy: IfNotPresent replicas: 1 service: type: ClusterIP port: 80 targetPort: 8000
Archivo: chart/templates/_helpers.tpl
yaml
{{/*Nombre completo de un recurso: "<release>-<composant>".Uso: {{ include "hedge.fullname" (dict "root" . "composant" "portail") }}*/}}{{- define "hedge.fullname" -}}{{- printf "TODO" .root.Release.Name .composant | trunc 63 | trimSuffix "-" -}}{{- end -}}{{/*Labels comunes a todos los recursos.Uso: {{ include "hedge.labels" (dict "root" . "composant" "portail") | nindent 4 }}*/}}{{- define "hedge.labels" -}}# ? rellena los 7 labels pedidos en la Misión 3app.kubernetes.io/name: TODOapp.kubernetes.io/instance: TODO# ... continúa ...{{- end -}}{{/*Selector labels: subconjunto ESTABLE de los labels.Pon aquí SOLO labels que NUNCA cambiarán para una instancia.*/}}{{- define "hedge.selectorLabels" -}}# ? únicamente los 3 labels ESTRICTAMENTE inmutables{{- end -}}
Archivo: chart/templates/portail-deployment.yaml
yaml
# ? Deployment del portail. Usa:# - .Values.portail.replicas# - .Values.portail.image.{repository,tag,pullPolicy}# - .Values.portail.service.targetPort# - .Values.environment# - .Values.banniere.{couleur,message}# - .Chart.AppVersion (para APP_VERSION)# - El NOMBRE del Service api construido con .Release.Name (para BACKEND_URL)apiVersion: apps/v1kind: Deploymentmetadata: name: TODO labels: TODOspec: replicas: TODO selector: matchLabels: TODO template: metadata: labels: TODO spec: containers: - name: portail image: TODO imagePullPolicy: TODO ports: - containerPort: TODO env: - name: ENVIRONMENT value: TODO # ? añade APP_VERSION, THEME_COLOR, BANNIERE_MESSAGE, # BACKEND_URL, REPLICAS_INFO readinessProbe: httpGet: path: /health port: TODO initialDelaySeconds: 3 periodSeconds: 5
Archivo: chart/templates/portail-service.yaml
yaml
# ? Service para el portail. Tipo NodePort. Usa la condición# {{- if eq .Values.portail.service.type "NodePort" }} ... {{- end }}# para incluir "nodePort" SOLO si es realmente un NodePort.apiVersion: v1kind: Servicemetadata: name: TODO labels: TODOspec: type: TODO selector: TODO ports: - port: TODO targetPort: TODO # ? nodePort únicamente si type == NodePort
Archivo: chart/templates/api-deployment.yaml
yaml
# ? Misma estructura que portail-deployment.yaml, pero:# - composant = "api"# - variables de entorno: ENVIRONMENT y APP_VERSION solamente# - puerto del contenedor = .Values.api.service.targetPort (8000)apiVersion: apps/v1kind: Deploymentmetadata: name: TODOspec: # ... (estructura similar a la del portail) ...
Archivo: chart/templates/api-service.yaml
yaml
# ? Service ClusterIP para la api. Un solo puerto. Sin nodePort.apiVersion: v1kind: Servicemetadata: name: TODOspec: type: TODO selector: TODO ports: - port: TODO targetPort: TODO
Archivo: chart/environments/values-dev.yaml
yaml
# ? Entorno DEV: 1 réplica, franja azul #2563eb, NodePort 30130.environment: TODObanniere: message: TODO couleur: TODOportail: replicas: TODO service: nodePort: TODOapi: replicas: TODO
Archivo: chart/environments/values-staging.yaml
yaml
# ? Entorno STAGING: 2 réplicas, franja naranja #ea580c, NodePort 30131.environment: TODO# ... completa siguiendo el modelo de values-dev.yaml ...
El archivo outils/valider.ps1 se suministra tal cual. No da ninguna solución — solo una puntuación y el primer punto a corregir. Ejecútalo en cualquier momento:
powershell
.\outils\valider.ps1
Lo que verifica el script:
Misión
Criterios automáticos
1
helm lint pasa, Chart.yaml tiene apiVersion: v2, type: application
2
helm template produce bien 2 Deployments y 2 Services, nombres prefijados por la release
3
_helpers.tpl define los 3 helpers, labels estándar presentes
4
Los 3 archivos values-<env>.yaml existen con los valores correctos (env, replicas, puerto, color), las 3 releases están desplegadas
5
La release hedge-dev tiene ≥ 2 revisiones y un rollback en el historial
6
Ningún hedge-config a pelo, ninguna label variable en matchLabels, ningún .Values.portal (con typo)
El script no ejecuta él mismo ningún comando helm install: te toca a ti desplegar antes de validar.
Curso creado por el Dr. Haythem REHOUMA — Desarrollo y despliegue de soluciones de datos