DevOps desde Cero: Helm Charts

2026-05-24 | Gabriel Garrido | 23 min de lectura
Share:

Apoya este blog

Si te resulta util este contenido, considera apoyar el blog.

Introduccion

Bienvenido al articulo doce de la serie DevOps desde Cero. En los articulos anteriores aprendimos como deployear containers a Kubernetes usando manifiestos YAML crudos. Eso funciona bien cuando tenes un solo servicio con un punado de archivos, pero a medida que tus aplicaciones crecen y empezas a gestionar multiples entornos, los manifiestos crudos se vuelven dolorosos de mantener.


Imaginate que tenes un deployment, un service, un ingress, un configmap y un HPA. Ahora multiplica eso por tres entornos (dev, staging, produccion) donde solo cambian algunos valores: el tag de la imagen, la cantidad de replicas, el nombre de dominio. De repente estas copiando y pegando archivos YAML, buscando y reemplazando valores a mano, y rezando para no haberte olvidado de algo. Este es exactamente el problema que Helm resuelve.


Helm es el gestor de paquetes para Kubernetes. Te permite definir tu aplicacion como un template reutilizable, parametrizar las partes que cambian, versionar todo, e instalar o actualizar con un solo comando. Si alguna vez usaste apt, brew o npm, Helm cumple el mismo rol para Kubernetes.


En este articulo vamos a cubrir que es Helm y por que existe, crear un chart desde cero, meternos en la sintaxis de templates y helpers, empaquetar una API TypeScript con todos los recursos que necesita, gestionar releases con install, upgrade y rollback, pushear charts a registries OCI, y testear nuestro trabajo. Si queres ver como se usaba Helm hace anos, mira Getting started with Helm y Deploying my apps with Helm, pero tene en cuenta que esos articulos son del 2018 y cubren Helm 2 que ya esta deprecado.


Vamos a ello.


Que es Helm?

Helm se autodefine como el gestor de paquetes para Kubernetes, y es una buena descripcion. Hay cuatro conceptos clave que necesitas entender:


  • Chart: Una coleccion de archivos que describen un conjunto relacionado de recursos Kubernetes. Pensalo como un paquete. Un chart contiene templates, valores por defecto, metadata, y opcionalmente sub-charts para dependencias.
  • Release: Una instancia especifica de un chart corriendo en tu cluster. Podes instalar el mismo chart multiples veces con diferentes configuraciones, y cada instalacion es un release separado con su propio nombre e historial.
  • Repository: Un lugar donde se almacenan y comparten charts. Puede ser un servidor HTTP tradicional, un repositorio Helm, o un registry de containers compatible con OCI (el enfoque moderno).
  • Values: Los parametros de configuracion que personalizan un chart para un deployment especifico. Provees values para sobreescribir los defaults del chart, y Helm los usa para renderizar los templates en manifiestos Kubernetes validos.

Asi es como encajan estas piezas:


Chart (definicion del paquete)
  + Values (tu configuracion)
  = Release (instancia corriendo en tu cluster)
    ├── Deployment (renderizado desde template)
    ├── Service (renderizado desde template)
    ├── Ingress (renderizado desde template)
    └── ConfigMap (renderizado desde template)

Por que Helm en vez de manifiestos crudos

Tal vez te preguntes si realmente necesitas otra herramienta. Esto es lo que Helm te da que el YAML crudo no:


  • Templating: Escribi tus manifiestos una vez con placeholders, y renderizalos con diferentes valores para cada entorno. Se acabo el copiar y pegar archivos YAML.
  • Versionado: Cada chart tiene una version. Cada release trackea que version se instalo y que values se usaron. Siempre sabes que esta corriendo.
  • Rollback: Hiciste un deployment malo? helm rollback te lleva al estado anterior en segundos. Helm mantiene un historial de cada revision del release.
  • Gestion de dependencias: Tu aplicacion depende de Redis? Agregalo como dependencia del chart y Helm instala ambos juntos.
  • Compartir: Empaqueta tu chart y pushealo a un registry. Cualquiera de tu equipo (o del mundo) puede instalarlo con un solo comando.
  • Lifecycle hooks: Correr jobs antes o despues de install, upgrade o delete. Genial para migraciones de base de datos, cache warming o health checks.

La alternativa es gestionar YAML crudo con Kustomize o scripts hechos a mano. Kustomize viene integrado en kubectl y funciona bien para escenarios simples de overlay, pero no te da versionado, rollback ni historial de releases. Para la mayoria de los equipos, Helm es la mejor opcion una vez que vas mas alla de deployments triviales.


Helm 3 vs Helm 2: breve historia

Si viste tutoriales viejos de Helm, mencionan algo llamado Tiller. Era un componente server-side que Helm 2 requeria para correr dentro de tu cluster. Tiller tenia permisos de cluster-admin y era una preocupacion de seguridad significativa.


Helm 3 (lanzado en noviembre de 2019) elimino Tiller por completo. Esto es lo que cambio:


  • Sin Tiller: Helm ahora habla directamente con la API de Kubernetes usando tus credenciales del kubeconfig. No mas deployear un pod privilegiado en tu cluster.
  • Three-way strategic merge: Helm 3 compara el manifiesto viejo, el nuevo y el estado actual en el cluster. Esto significa que los cambios manuales a recursos se detectan y manejan correctamente durante los upgrades.
  • Release namespaces: Los releases se guardan como secrets de Kubernetes en el namespace donde se deployean, no en un namespace central de Tiller.
  • Validacion con JSON Schema: Los charts pueden incluir un archivo values.schema.json para validar los values provistos por el usuario antes de renderizar.
  • Soporte de registries OCI: Los charts se pueden almacenar en registries de containers como Docker Hub, GHCR o ECR, igual que las imagenes de containers.

Si empezas hoy, solo vas a usar Helm 3. El binario helm que instalas del sitio oficial es Helm 3. Helm 2 llego a su end of life en noviembre de 2020, asi que no hay razon para usarlo en proyectos nuevos.


Instalando Helm

Instalar Helm es sencillo. Elegi el metodo que corresponda a tu sistema:


# macOS con Homebrew
brew install helm

# Linux con el script oficial de instalacion
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Arch Linux
pacman -S helm

# Verificar la instalacion
helm version

Deberias ver una salida como:


version.BuildInfo{Version:"v3.17.x", GitCommit:"...", GitTreeState:"clean", GoVersion:"go1.23.x"}

Creando un chart desde cero

Vamos a crear nuestro primer chart. Helm provee un comando de scaffolding:


helm create task-api

Esto crea una estructura de directorios con todo lo que necesitas:


task-api/
├── Chart.yaml          # Metadata: nombre, version, descripcion
├── values.yaml         # Valores de configuracion por defecto
├── charts/             # Sub-charts (dependencias)
├── templates/          # Templates de manifiestos Kubernetes
│   ├── _helpers.tpl    # Helpers de templates con nombre
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   ├── serviceaccount.yaml
│   ├── NOTES.txt       # Instrucciones post-instalacion mostradas al usuario
│   └── tests/
│       └── test-connection.yaml
└── .helmignore         # Archivos a excluir al empaquetar

Vamos a repasar los archivos clave uno por uno.


Chart.yaml: la metadata del chart

Este archivo define quien es tu chart. Pensalo como un package.json para Helm:


apiVersion: v2
name: task-api
description: A Helm chart for the Task API TypeScript application
type: application
version: 0.1.0
appVersion: "1.0.0"

  • apiVersion: Siempre v2 para charts de Helm 3. Helm 2 usaba v1.
  • name: El nombre del chart. Debe ser minuscula y puede contener guiones.
  • description: Una descripcion corta que se muestra al buscar en repositorios.
  • type: application (deployea recursos) o library (solo provee helpers para otros charts).
  • version: La version del chart. Sigue versionado semantico. Bumpeala cada vez que cambies el chart.
  • appVersion: La version de la aplicacion que se deployea. Es informativa y no afecta el comportamiento del chart.

La distincion entre version y appVersion es importante. La version del chart trackea cambios al chart en si (templates, defaults). La version de la app trackea que version de tu aplicacion deployea el chart. Evolucionan independientemente.


values.yaml: la configuracion por defecto

Este es el archivo mas importante de un chart. Define cada parametro configurable con defaults razonables:


replicaCount: 2

image:
  repository: ghcr.io/your-org/task-api
  pullPolicy: IfNotPresent
  tag: ""

nameOverride: ""
fullnameOverride: ""

serviceAccount:
  create: true
  annotations: {}
  name: ""

service:
  type: ClusterIP
  port: 3000

ingress:
  enabled: false
  className: "traefik"
  annotations: {}
  hosts:
    - host: task-api.example.com
      paths:
        - path: /
          pathType: Prefix
  tls: []

resources:
  limits:
    cpu: 200m
    memory: 256Mi
  requests:
    cpu: 100m
    memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

env:
  NODE_ENV: production
  PORT: "3000"

Algunos principios para estructurar values:


  • Agrupa configuraciones relacionadas: Pone todos los valores de imagen bajo image, toda la configuracion de ingress bajo ingress, y asi. Esto hace facil encontrar y sobreescribir cosas.
  • Provee defaults razonables: El chart deberia funcionar con cero overrides para un deployment basico. Las configuraciones especificas de produccion (nombres de dominio, limites de recursos) son lo que los usuarios sobreescriben.
  • Usa claves planas donde sea posible: Los values profundamente anidados son mas dificiles de sobreescribir con --set. Mantene el anidamiento razonable.
  • Documenta con comentarios: Agrega comentarios explicando que hace cada valor, cuales son las opciones validas, y que significa el default.

Sintaxis de templates: Go templates

Los templates de Helm usan el paquete text/template de Go con algunas funciones extra de la libreria Sprig. Si nunca viste Go templates antes, aca tenes una introduccion rapida.


La sintaxis basica usa dobles llaves {{ }} para insertar contenido dinamico. Todo lo que esta fuera de las llaves se renderiza tal cual. Veamos los patrones principales:


# Sustitucion simple de valores
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-api
  labels:
    app: {{ .Chart.Name }}
    version: {{ .Chart.AppVersion }}
spec:
  replicas: {{ .Values.replicaCount }}

Helm provee varios objetos built-in que podes acceder en los templates:


  • .Values: El resultado mergeado de values.yaml y cualquier override que el usuario provea. De aca viene la mayoria de tu data dinamica.
  • .Release: Informacion sobre el release actual. .Release.Name es el nombre del release, .Release.Namespace es el namespace, .Release.IsUpgrade te dice si es un upgrade.
  • .Chart: Contenidos de Chart.yaml. .Chart.Name, .Chart.Version, .Chart.AppVersion.
  • .Template: Informacion sobre el archivo de template actual. Se usa mayormente para debugging.
  • .Capabilities: Informacion sobre el cluster Kubernetes. .Capabilities.APIVersions te permite verificar si una API version especifica existe.

Condicionales y loops

Los templates soportan flujo de control. Asi es como incluir condicionalmente un ingress y loopear sobre hosts:


{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "task-api.fullname" . }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  ingressClassName: {{ .Values.ingress.className }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "task-api.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
          {{- end }}
    {{- end }}
  {{- if .Values.ingress.tls }}
  tls:
    {{- toYaml .Values.ingress.tls | nindent 4 }}
  {{- end }}
{{- end }}

Algunas cosas para notar:


  • {{- ... }}: El guion recorta el whitespace antes del tag. Sin el, te quedan lineas en blanco en la salida.
  • range: Itera sobre una lista o mapa. Dentro del loop, . se refiere al item actual.
  • $: Se refiere al scope raiz. Cuando estas dentro de un bloque range, . cambia al item actual. Usa $ para acceder a .Values o .Release desde dentro de un loop.
  • with: Establece el scope de . al objeto especificado. Si el objeto esta vacio, el bloque se saltea por completo. Funciona como un “si no esta vacio” y “establecer scope” combinados.
  • toYaml: Convierte una estructura de datos Go a YAML. Combinado con nindent, maneja la indentacion correctamente.
  • quote: Envuelve el valor en comillas dobles. Siempre cita hostnames y strings que puedan contener caracteres especiales.

Helpers: _helpers.tpl

El archivo _helpers.tpl (el prefijo guion bajo le dice a Helm que no lo renderice como un manifiesto) contiene templates con nombre reutilizables. Son como funciones que podes llamar desde cualquier template:


{{/*
Expand the name of the chart.
*/}}
{{- define "task-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "task-api.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 }}

{{/*
Common labels
*/}}
{{- define "task-api.labels" -}}
helm.sh/chart: {{ include "task-api.chart" . }}
{{ include "task-api.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "task-api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "task-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Llamar estos templates con nombre se hace usando include:


metadata:
  name: {{ include "task-api.fullname" . }}
  labels:
    {{- include "task-api.labels" . | nindent 4 }}

Por que include en vez de template? La accion template escribe texto directamente y no se puede pipear a otras funciones. include retorna la salida como string, asi que la podes pipear a nindent, trim, o cualquier otra funcion. Siempre preferi include sobre template.


Las llamadas a trunc 63 a lo largo de los helpers no son arbitrarias. Las labels y nombres de Kubernetes tienen un limite de 63 caracteres (reglas de DNS label del RFC 1123). Los helpers aplican esto automaticamente.


Empaquetando una API TypeScript: el chart completo

Construyamos un chart completo para nuestra API de tareas de la serie. Necesitamos un deployment, un service, un ingress, un configmap y un HPA. Ya vimos el ingress arriba, asi que cubramos el resto.


Template de Deployment (templates/deployment.yaml):


apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "task-api.fullname" . }}
  labels:
    {{- include "task-api.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "task-api.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels:
        {{- include "task-api.selectorLabels" . | nindent 8 }}
    spec:
      serviceAccountName: {{ include "task-api.serviceAccountName" . }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.port }}
              protocol: TCP
          envFrom:
            - configMapRef:
                name: {{ include "task-api.fullname" . }}-config
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Nota la anotacion checksum/config en el template del pod. Este es un patron comun de Helm. Cuando cambias un ConfigMap, Kubernetes no reinicia automaticamente los pods que lo usan. Al hashear el contenido del ConfigMap en una anotacion, cualquier cambio al ConfigMap produce un hash diferente, lo que dispara un rolling update. Inteligente y simple.


Tambien nota que cuando autoscaling esta habilitado, nos salteamos el campo replicas. El HPA gestiona la cantidad de replicas en ese caso, y setearlo en el deployment causaria conflictos.


Template de ConfigMap (templates/configmap.yaml):


apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "task-api.fullname" . }}-config
  labels:
    {{- include "task-api.labels" . | nindent 4 }}
data:
  {{- range $key, $value := .Values.env }}
  {{ $key }}: {{ $value | quote }}
  {{- end }}

Esto itera sobre cada par clave-valor en .Values.env y crea una entrada en el ConfigMap. Agregas nuevas variables de entorno simplemente agregandolas a values.yaml, sin necesidad de cambiar el template.


Template de Service (templates/service.yaml):


apiVersion: v1
kind: Service
metadata:
  name: {{ include "task-api.fullname" . }}
  labels:
    {{- include "task-api.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "task-api.selectorLabels" . | nindent 4 }}

Template de HPA (templates/hpa.yaml):


{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{ include "task-api.fullname" . }}
  labels:
    {{- include "task-api.labels" . | nindent 4 }}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {{ include "task-api.fullname" . }}
  minReplicas: {{ .Values.autoscaling.minReplicas }}
  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}

El template completo del HPA esta envuelto en un bloque if. Cuando autoscaling esta deshabilitado (el default), este archivo no produce ninguna salida.


Sobreescribiendo values

Hay dos formas principales de sobreescribir los defaults de values.yaml cuando instalas o actualizas un release.


Usando --set para valores individuales:


helm install my-api ./task-api \
  --set image.tag=v1.2.3 \
  --set replicaCount=3 \
  --set ingress.enabled=true

Usando -f (o --values) con un archivo:


helm install my-api ./task-api -f production-values.yaml

Donde production-values.yaml podria verse asi:


replicaCount: 3

image:
  tag: v1.2.3

ingress:
  enabled: true
  className: traefik
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: api-tls
      hosts:
        - api.example.com

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
  targetCPUUtilizationPercentage: 70

env:
  NODE_ENV: production
  PORT: "3000"
  LOG_LEVEL: info

El enfoque de archivo es mejor para cualquier cosa mas alla de un par de valores. Podes versionar tus archivos especificos por entorno (dev-values.yaml, staging-values.yaml, production-values.yaml) y obtener todos los beneficios del historial de Git para cambios de configuracion.


Tambien podes combinar ambos enfoques. Los values de archivos -f se aplican primero, despues los overrides de --set encima. Esto es util cuando queres un archivo base mas un override puntual:


helm install my-api ./task-api \
  -f production-values.yaml \
  --set image.tag=v1.2.4

Instalando y gestionando releases

Aca tenes el ciclo de vida completo de un release de Helm.


Instalar un release:


# Instalar desde un directorio de chart local
helm install my-api ./task-api -n task-api --create-namespace

# Instalar desde un repositorio
helm install my-api my-repo/task-api -n task-api --create-namespace

# Instalar y esperar a que todos los pods esten ready
helm install my-api ./task-api -n task-api --create-namespace --wait --timeout 5m

El flag --wait le dice a Helm que espere hasta que todos los recursos esten en estado ready antes de marcar el release como exitoso. Combinado con --timeout, te da una senal clara de exito o fallo. Sin --wait, Helm marca el release como deployed apenas los manifiestos se envian al API server, sin importar si los pods realmente arrancan.


Verificar estado del release:


# Listar todos los releases en un namespace
helm list -n task-api

# Obtener estado detallado de un release
helm status my-api -n task-api

# Ver los values que se usaron para el release actual
helm get values my-api -n task-api

# Ver todos los values (incluyendo defaults)
helm get values my-api -n task-api --all

# Ver los manifiestos renderizados
helm get manifest my-api -n task-api

Actualizar un release:


# Upgrade con un nuevo image tag
helm upgrade my-api ./task-api -n task-api --set image.tag=v1.3.0

# Upgrade con un archivo de values
helm upgrade my-api ./task-api -n task-api -f production-values.yaml

# Install o upgrade (idempotente, genial para CI/CD)
helm upgrade --install my-api ./task-api -n task-api -f production-values.yaml

El patron upgrade --install es el mas comun en pipelines de CI/CD. Instala el release si no existe, o lo actualiza si ya existe. Esto hace tu pipeline idempotente, podes correrlo multiples veces sin errores.


Ver historial de releases:


helm history my-api -n task-api

REVISION  UPDATED                   STATUS      CHART           APP VERSION  DESCRIPTION
1         2026-05-24 10:00:00       superseded  task-api-0.1.0  1.0.0        Install complete
2         2026-05-24 14:30:00       superseded  task-api-0.1.0  1.1.0        Upgrade complete
3         2026-05-24 15:00:00       deployed    task-api-0.2.0  1.2.0        Upgrade complete

Rollback a una revision anterior:


# Rollback a la revision anterior
helm rollback my-api -n task-api

# Rollback a una revision especifica
helm rollback my-api 1 -n task-api

El rollback es uno de los argumentos mas fuertes a favor de Helm. Si un deployment sale mal, podes volver a cualquier estado anterior en segundos. No necesitas averiguar que archivos YAML aplicar o que image tag estaba corriendo antes. Helm trackea todo eso por vos.


Desinstalar un release:


# Eliminar el release y todos sus recursos
helm uninstall my-api -n task-api

# Mantener el historial del release (util para auditoria)
helm uninstall my-api -n task-api --keep-history

Registries OCI: el enfoque moderno

Los repositorios Helm tradicionales son servidores HTTP que hostean un archivo index.yaml listando todos los charts disponibles. Funcionan, pero requieren mantener una pieza separada de infraestructura.


El enfoque moderno es almacenar charts Helm en registries de containers compatibles con OCI, los mismos registries que ya usas para imagenes Docker. GitHub Container Registry (GHCR), Docker Hub, ECR, GCR, y Azure Container Registry todos soportan charts Helm OCI.


Asi es como empaquetar y pushear un chart a GHCR:


# Login a GHCR
echo $GITHUB_TOKEN | helm registry login ghcr.io --username your-username --password-stdin

# Empaquetar el chart
helm package ./task-api

# Esto crea task-api-0.1.0.tgz en el directorio actual

# Push a GHCR
helm push task-api-0.1.0.tgz oci://ghcr.io/your-org/charts

# Pull desde GHCR
helm pull oci://ghcr.io/your-org/charts/task-api --version 0.1.0

# Instalar directamente desde GHCR
helm install my-api oci://ghcr.io/your-org/charts/task-api --version 0.1.0 -n task-api

El enfoque OCI tiene varias ventajas:


  • Sin index.yaml: No necesitas reconstruir y hostear un indice de charts. El registry se encarga del descubrimiento.
  • Misma infraestructura: Si ya usas GHCR para imagenes Docker, no necesitas configurar nada mas.
  • Control de acceso: Los permisos del registry aplican a charts de la misma forma que aplican a imagenes.
  • Tags inmutables: Una vez que pusheas una version, no se puede sobreescribir (dependiendo de la configuracion del registry). Esto garantiza reproducibilidad.

En un pipeline de CI/CD, construirias y pushearias el chart junto con la imagen Docker:


# .github/workflows/release.yaml (extracto relevante)
- name: Push Helm chart to GHCR
  run: |
    echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io \
      --username ${{ github.actor }} --password-stdin
    helm package ./charts/task-api --version ${{ github.ref_name }}
    helm push task-api-${{ github.ref_name }}.tgz oci://ghcr.io/${{ github.repository_owner }}/charts

Testeando charts

Antes de instalar un chart en un cluster real, deberias validarlo. Helm provee varias herramientas para esto.


Linting:


# Verificar problemas en la estructura del chart y templates
helm lint ./task-api

# Lint con values especificos
helm lint ./task-api -f production-values.yaml

helm lint atrapa errores comunes: campos requeridos faltantes en Chart.yaml, errores de sintaxis en templates, problemas de indentacion, y API versions deprecadas. Correlo en CI en cada pull request.


Renderizado de templates:


# Renderizar templates sin instalar
helm template my-api ./task-api

# Renderizar con values especificos y guardar a un archivo para revisar
helm template my-api ./task-api -f production-values.yaml > rendered.yaml

# Renderizar y validar contra la API del cluster
helm template my-api ./task-api --validate

helm template renderiza los templates localmente y printea el YAML resultante. Esto es increiblemente util para debugging. Si algo se ve mal en la salida, el problema esta en tus templates o values, no en Kubernetes. El flag --validate agrega validacion del API server, lo que atrapa problemas como usar una API version removida.


Testing de releases:


# Correr los test pods del chart
helm test my-api -n task-api

Helm soporta test hooks. Son pods definidos en templates/tests/ que se ejecutan cuando corres helm test. Un test tipico verifica que la aplicacion deployeada es alcanzable:


# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "task-api.fullname" . }}-test-connection"
  labels:
    {{- include "task-api.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['{{ include "task-api.fullname" . }}:{{ .Values.service.port }}/health']
  restartPolicy: Never

Este pod ejecuta wget contra el endpoint de health del service. Si tiene exito, el test pasa. Si falla, sabes que algo anda mal con el deployment.


Gestionando multiples charts: Helmfile y ArgoCD

Una vez que tenes mas de un punado de charts, necesitas una forma de gestionarlos juntos. Dos herramientas se destacan.


Helmfile es una spec declarativa para deployear multiples charts Helm. En vez de correr comandos helm install y helm upgrade manualmente, definis todo en un helmfile.yaml:


# helmfile.yaml
repositories:
  - name: bitnami
    url: https://charts.bitnami.com/bitnami

releases:
  - name: task-api
    namespace: task-api
    chart: ./charts/task-api
    values:
      - environments/{{ .Environment.Name }}/task-api.yaml

  - name: redis
    namespace: task-api
    chart: bitnami/redis
    version: 18.6.1
    values:
      - environments/{{ .Environment.Name }}/redis.yaml

Despues deployeas todo con:


helmfile -e production apply

ArgoCD tiene un enfoque diferente. En vez de correr comandos, definis tu estado deseado en Git y ArgoCD continuamente reconcilia el cluster para matchear. ArgoCD tiene soporte nativo de Helm, asi que lo apuntas a un repositorio Git que contiene tu chart y values, y el se encarga del resto:


# Manifiesto de Application de ArgoCD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: task-api
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/your-repo
    targetRevision: main
    path: charts/task-api
    helm:
      valueFiles:
        - ../../environments/production/task-api.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: task-api
  syncPolicy:
    automated:
      selfHeal: true
      prune: true
    syncOptions:
      - CreateNamespace=true

ArgoCD es el enfoque GitOps. Cada cambio pasa por un pull request, se revisa, se mergea a main, y ArgoCD lo aplica automaticamente. Nadie corre helm install o kubectl apply manualmente. Asi es como operan la mayoria de los equipos de produccion hoy. Si queres profundizar en ArgoCD, mira GitOps with ArgoCD de la serie SRE.


Patrones comunes y tips

Aca hay algunos patrones que vas a encontrar seguido al trabajar con Helm.


Usa helm upgrade --install en CI/CD. Esto hace los deployments idempotentes. Ya sea que el release exista o no, el comando hace lo correcto.


Siempre setea resource requests y limits. El values.yaml por defecto deberia incluir valores de recursos razonables. Sin ellos, un solo pod puede consumir todos los recursos del cluster.


Usa el patron de anotacion checksum. Como vimos antes, hashear el contenido del ConfigMap en una anotacion del pod dispara rolling updates cuando la configuracion cambia. Esto te salva de la sorpresa “cambie el ConfigMap pero no paso nada.”


Pinea las versiones de tus charts. Cuando instalas desde un repositorio, siempre especifica --version. Sin el, Helm instala la ultima version, que podria introducir breaking changes.


Mantene los secrets fuera de values.yaml. Nunca pongas passwords, API keys o tokens en archivos de values que se commitean a Git. Usa Kubernetes Secrets gestionados por una herramienta externa como External Secrets Operator o Sealed Secrets.


Usa helm diff para upgrades seguros. El plugin helm-diff te muestra exactamente que va a cambiar antes de upgradear:


# Instalar el plugin diff
helm plugin install https://github.com/databus23/helm-diff

# Previsualizar cambios antes de upgradear
helm diff upgrade my-api ./task-api -f production-values.yaml -n task-api

Esto es especialmente valioso en produccion donde queres revisar los cambios antes de aplicarlos.


Notas finales

Helm saca el dolor de gestionar aplicaciones Kubernetes. En vez de hacer malabares con archivos YAML crudos entre entornos, definis tu aplicacion una vez como chart, parametrizas las cosas que cambian, y dejas que Helm se encargue del renderizado, versionado y gestion del ciclo de vida.


En este articulo cubrimos que es Helm y por que existe, creamos un chart desde cero con todos los templates que una aplicacion real necesita, exploramos la sintaxis de Go templates incluyendo condicionales, loops y objetos built-in, construimos helpers reutilizables, gestionamos releases con install, upgrade, rollback e historial, pusheamos charts a registries OCI para distribucion moderna, y validamos todo con lint, template y test.


El takeaway clave es que Helm no se trata solo de templatear YAML. Se trata de darle a tus deployments de Kubernetes un ciclo de vida apropiado: releases versionados, gestion de configuracion, capacidad de rollback, y un lenguaje compartido para que tu equipo hable de que esta corriendo donde.


En el proximo articulo vamos a ver monitoreo y observabilidad para nuestros workloads de Kubernetes, porque deployear una aplicacion es solo la mitad del trabajo. Tambien necesitas saber si esta sana y con buen rendimiento.


Espero que te haya resultado util y lo hayas disfrutado! Hasta la proxima!


Errata

Si encontras algun error o tenes alguna sugerencia, por favor mandame un mensaje para que se corrija.

Tambien podes revisar el codigo fuente y los cambios en las fuentes aca



$ Comentarios

Online: 0

Por favor inicie sesión para poder escribir comentarios.

2026-05-24 | Gabriel Garrido