Helm: el gestor de paquetes para Kubernetes

Helm (timón en español) es un administrador de paquetes de recursos para aplicaciones en Kubernetes. Permite definir, instalar, actualizar y hacer rollback de las aplicaciones desplegadas a través de este gestor. Este administra los recursos que necesita a través de los llamados Charts, que en español significa “cartas de navegación”.

Helm se compone de dos partes:

  • El servidor llamado Tiller: interactúa con la API de Kubernetes para instalar, actualizar, listar y eliminar recursos del clúster.
  • El cliente: es responsable de hablar con el servidor Tiller para llevar a cabo las acciones.

En este artículo vamos a ver cómo instalar Helm, cómo trabajar con charts de terceros y, además, crearemos uno propio.

Instalar Helm

Para instalar Helm primero necesitas tener un clúster de Kubernetes. En este artículo voy a utilizar Minikube, del que ya te hablé. Aunque pueda parecer extraño, primero necesitamos instalar el cliente, ya que este va a ser quien instale el servidor (será un pod dentro de nuestro clúster). Dependiendo del sistema operativo que estés utilizando, tendrás que utilizar un comando u otro. En mi caso trabajo con Mac Os, desde donde se puede instalar con Homebrew:

brew install kubernetes-helm

y también con Windows, donde la forma más sencilla es a través del gestor Chocolatey:

choco install kubernetes-helm

Una vez instalado, lo siguiente que necesitas es iniciarlo:

helm init --history-max 200

Según la documentación de helm, es recomendable limitar el histórico, a través de history-max, ya que sino puede crecer demasiado y nunca se purgaría.

Charts

Los charts son colecciones de archivos organizados en una estructura específica. Cada vez que se ejecuta una instancia de un chart se llama release. Existen repositorios públicos de charts que puedes usar, como Kubeapps. Para verlo con un ejemplo, vamos a lanzar un chart que nos genere un entorno de WordPress, el cual está compuesto del CMS y una base de datos MariaDB.

#Update helm repo
helm repo update

#Install WordPress using helm
helm install stable/wordpress --set wordpressSkipInstall=no,service.type=NodePort

El resultado será el siguiente:

Giselas-iMac:docker-scripts gis$ helm install stable/wordpress --set wordpressSkipInstall=no,service.type=NodePort
NAME:   rolling-marmot
LAST DEPLOYED: Fri Apr 12 08:38:54 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/ConfigMap
NAME                          DATA  AGE
rolling-marmot-mariadb        1     0s
rolling-marmot-mariadb-tests  1     0s

==> v1/Deployment
NAME                      READY  UP-TO-DATE  AVAILABLE  AGE
rolling-marmot-wordpress  0/1    0           0          0s

==> v1/PersistentVolumeClaim
NAME                      STATUS  VOLUME                                    CAPACITY  ACCESS MODES  STORAGECLASS  AGE
rolling-marmot-wordpress  Bound   pvc-a43c2852-5ced-11e9-ae78-080027a740ef  10Gi      RWO           standard      0s

==> v1/Pod(related)
NAME                                       READY  STATUS   RESTARTS  AGE
rolling-marmot-mariadb-0                   0/1    Pending  0         0s
rolling-marmot-wordpress-59989889d4-nr2ck  0/1    Pending  0         0s

==> v1/Secret
NAME                      TYPE    DATA  AGE
rolling-marmot-mariadb    Opaque  2     0s
rolling-marmot-wordpress  Opaque  1     0s

==> v1/Service
NAME                      TYPE       CLUSTER-IP    EXTERNAL-IP  PORT(S)                     AGE
rolling-marmot-mariadb    ClusterIP  10.99.70.150  <none>       3306/TCP                    0s
rolling-marmot-wordpress  NodePort   10.98.5.81    <none>       80:31193/TCP,443:31177/TCP  0s

==> v1beta1/StatefulSet
NAME                    READY  AGE
rolling-marmot-mariadb  0/1    0s


NOTES:
1. Get the WordPress URL:

  export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services rolling-marmot-wordpress)
  export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  echo "WordPress URL: http://$NODE_IP:$NODE_PORT/"
  echo "WordPress Admin URL: http://$NODE_IP:$NODE_PORT/admin"

2. Login with the following credentials to see your blog

  echo Username: user
  echo Password: $(kubectl get secret --namespace default rolling-marmot-wordpress -o jsonpath="{.data.wordpress-password}" | base64 --decode)

Como puedes ver, este comando genera mucha información de salida. Como no le hemos especificado un nombre a esta release, lo primero que hace es asignarle uno (en mi caso rolling-marmot) y lo aloja en el namespace default. A partir de este momento, crea todos los recursos necesarios para esta aplicación en concreto. Por último, te da indicaciones de cómo acceder a tu nuevo WordPress. Si estás usando Minikube como yo, la IP del balanceador se quedará como pending, ya que en este entorno no es posible generar IPs de manera dinámica. Sin embargo, a través de set, he modificado el tipo de servicio que genera este chart a NodePort para poder acceder de esta otra manera. Por supuesto, puedes comprobar el estado de los pods como siempre:

kubectl get pods

Una vez que estos estén en estado ready, puedes acceder a tu nuevo WordPress y comprobar que todo funciona perfectamente. Para ello, localiza el nombre del servicio de WordPress a través de kubectl get services y pregúntale a minikube por sus URLs.

minikube service rolling-marmot-wordpress --url

Este comando devolverá dos URLs: una para el puerto 80 y otra para el 443. Debes acceder a través del puerto 80, por lo que elige aquella que el puerto esté mapeado a este. Si accedes a través del navegador verás que tu WordPress funciona correctamente.

WordPress ejecutándose gracias a Helm

Para ver las releases que tienes desplegadas de diferentes charts utiliza la opción list:

helm list

Si quieres eliminar alguna release utiliza este otro:

helm delete NOMBRE_DE_LA_RELEASE --purge

Crear tu propio Chart

Ahora que has visto cómo funciona Helm, ha llegado el momento de crear tu propio chart. Este va a ser un ejemplo sencillo, con un único contenedor, pero así será más fácil el hacerte una idea de cómo funciona.

Para crear la estructura de carpetas que te mencionaba antes solo tienes que lanzar este comando:

helm create my-first-chart

Esto creará una carpeta llamada my-first-chart con la siguiente estructura en su interior:

Estructura de un chart de Helm
  • Chart.yaml es el archivo principal, que contiene la información sobre tu chart (es como el package.json en Node.js, donde se describe el paquete).
  • values.yaml es el que contiene los valores por defecto de nuestro chart. Si no se especifica ninguno a través de –set, como hice con el chart de WordPress, estos son los valores que se usarán.
  • En templates te encontrarás los recursos de Kubernetes (services, deployments, ingress) creados como plantillas. Ahora veremos de qué se trata.
  • El directorio charts es opcional y es para incluir otros charts dentro de este.
  • .helmignore es similar a .gitignore o .dockerignore. Es útil para definir qué archivos o directorios no entrarán en el paquete final.

Ahora que tenemos la estructura creada, vamos a modificarla para un despliegue de Kubernetes que contendrá una aplicación en ASP.NET Core. Lo primero que vamos a modificar es el archivo values.yaml, donde vamos a definir qué imagen quiero utilizar para mi aplicación web, además del tipo de servicio, que será NodePort:

# Default values for my-first-chart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
  repository: mcr.microsoft.com/dotnet/core/samples
  tag: aspnetapp
  pullPolicy: IfNotPresent

service:
  type: NodePort
  port: 80

ingress:
  enabled: false
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths: []

  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

resources: {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

nodeSelector: {}

tolerations: []

affinity: {}

Sólo he modificado el valor de repository y tag, dentro de image, y de type dentro de service.

El archivo deployment.yaml se quedará exactamente igual:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-first-chart.fullname" . }}
  labels:
    app.kubernetes.io/name: {{ include "my-first-chart.name" . }}
    helm.sh/chart: {{ include "my-first-chart.chart" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "my-first-chart.name" . }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ include "my-first-chart.name" . }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
    {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
    {{- end }}
    {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
    {{- end }}

Como ves, este archivo se corresponde con el YAML que hemos utilizado en artículos anteriores para generar un deployment, pero contiene algunos valores entre dobles llaves que son variables en Helm. Estas son definidas en el archivo values.yaml, que vimos antes que este.

El archivo service.yaml podemos dejarlo tal cual está e ingress.yaml no es utilizado en este caso, ya que en el archivo values.yaml está deshabilitado.

Lo siguiente que podemos hacer es comprobar que nuestro chart está bien formado a través de este comando:

helm lint ./my-first-chart

Como resultado, nos mostrará si todo lo que hemos editado, eliminado o añadido es correcto.

helm lint my-first-chart

Otro comando interesante es el que nos permite ver cómo quedarán las plantillas, dentro de templates, una vez se hayan mapeado los valores en los apartados con llaves:

helm template ./my-first-chart

Por último, para instalar este chart podemos hacerlo de la misma manera que con los charts de terceros:

helm install --name my-first-chart ./my-first-chart

En este caso, he utilizado el parámetro name para asignarle un nombre a la release, y que no me genere uno aleatorio (esto es lo más cómodo cuando tienes un script). Al igual que en el caso anterior, puedo utilizar minikube para recuperar la URL del servicio y comprobar que todo funciona correctamente:

ASP.NET Core web app generada desde un chart de Helm

Si modificas el chart y quieres actualizar la release que acabas de lanzar puedes utilizar la opción upgrade.

helm upgrade my-first-chart ./my-first-chart

Por otro lado, si lo que necesitas es hacer rollback a una versión anterior, existe otra opción, que es la de rollback.

helm rollback my-first-chart 1

El 1 es el número de la revisión (se puede ver con helm list, en qué revisión estás).

El número de la revisión en Helm

¡Saludos!