Contenido

Kubernetes (2): Helm

Hoy en día es raro gestionar recursos en kubernetes de forma manual, ya que existe una forma de compartir grupos de recursos: Helm. Ellos lo definen como un “gestor de paquetes”, es decir, como pip para python o npm para node.

Aunque aún no hemos visto cómo utilizar kubernetes, sólo los conceptos básicos sobre kubernetes. Pero para poder gestionar es necesario tener algo que gestionar, así que comenzaremos la casa por el tejado mostrando cómo se instala algo hoy en día.

En concreto mostraré lo básico para crear un paquete helm.

Kubernetes

Instalar helm

Lo primero será descargarnos helm. Mis mecanismos favoritos son snap o bien descargar el binario directamente sobre ~/.local/bin. El caso es que debe estar en la ruta.

La versión 3 es la más actual, que es algo incompatible con la 2, así que me centraré en esta versión 3.

Así que sugiero instalarlo con:

1
sudo snap install helm --classic

En la documentación hay más opciones para descargarnos helm.

Creación de un paquete helm

Crear un paquete es bastante sencillo. Supongamos que vamos a crear el paquete mi-application:

1
helm create mi-application

Y ya está creado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
╰─$ tree
.
└── mi-application
    ├── charts
    ├── Chart.yaml
    ├── templates
    │   ├── deployment.yaml
    │   ├── _helpers.tpl
    │   ├── hpa.yaml
    │   ├── ingress.yaml
    │   ├── NOTES.txt
    │   ├── serviceaccount.yaml
    │   ├── service.yaml
    │   └── tests
    │       └── test-connection.yaml
    └── values.yaml

4 directories, 10 files

Contenido del directorio

Veamos lo básico del directorio:

  • archivo Chart.yaml, que contiene el nombre y versión del paquete.
  • archivo values.yaml, con los valores por defecto. Luego lo entenderemos.
  • directorio templates, con las plantillas para generar recursos

y ya. Luego veremos algunos archivos más, pero quedémonos con esos tres importantes.

Mencionaré también que el directorio charts es sólo para descargarse dependencias, por lo que se gestiona de forma automática, habría que eliminarlo de nuestro repositorio metiéndolo en el .gitignore y podemos ignorar que está ahí.

Archivo Chart.yaml

El contenido de este archivo tras borrar los comentarios es bastante pobre:

1
2
3
4
5
6
7
apiVersion: v2
name: mi-application
description: A Helm chart for Kubernetes

type: application
version: 0.1.0
appVersion: "1.16.0"

Con decir que apiVersion debe ser v2 para que se reconozca como un paquete de tipo helm 3, y que el type puede ser application o library pero que el primero puede actuar como el segundo así que en la práctica no se utiliza, creo que lo demás es auto-explicativo o podéis entenderlo con el comentario.

Otros campos interesantes:

  • dependencies, para definir dependencias de un proyecto.
  • annotations, clave-valor con metadatos
  • deprecated, boolean indicando si el paquete está obsoleto. Suele usarse sólo para indicar los que lo están.

Si queréis verlos todos o más detalle sobre alguno, consultad el apartado The Chart.yaml File de la guía oficial.

Archivo values.yaml

Este archivo es más largo, pero veamos sólo una parte del mismo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
replicaCount: 1

image:
  repository: nginx
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: ""

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

Como se ve, es un archivo YAML. Contiene claves y valores, y cuando tenemos una lista vacía, es necesario inicializarla como [].

Estos valores se utilizarán en las templates, pero eso lo veremos luego. Quedémonos con que podemos utilizar cualquier clave y valor, de momento.

Directorio de templates

Aquí es necesario explicar cómo funciona Helm.

Helm es un programa go, y utiliza las templates de go. Éstas se encuentran en el directorio templates para no despistar y sustituyen los valores de distintas fuentes:

  • Release, que describe la release misma. Contiene los siguientes campos:
    • Release.Name
    • Release.Namespace
    • Release.IsUpgrade
    • Release.IsInstall
    • Release.Revision
    • Release.Service
  • Values, con todo el contenido del archivo values.yaml
  • Chart, con el contenido del archivo Chart.yaml
  • Files, que es una api para acceder a otros archivos. Muy útil para crear ConfigMaps o Secrets.
  • Capabilities, con información sobre qué soporta el cluster Kubernetes actual
  • Template, con información sobre la plantilla actual, como el nombre.

Podéis verlo más en detalle en la sección Built-in Objects de la documentación oficial.

Estas fuentes se utilizan en las plantillas de la siguiente forma:

1
{{ .Release.Name }}

Que se sustituirá por el nombre de la release actual.

Ejemplo

Veamos un uso real investigando un archivo pequeñito, como templates/service.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
  name: {{ include "mi-application.fullname" . }}
  labels:
    {{- include "mi-application.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "mi-application.selectorLabels" . | nindent 4 }}

Aunque pequeñito, es bastante matón.

Veamos primero algo sencillo, como es la clave spec.type, con el valor {{ .Values.service.port }}. Éste se encuentra en el archivo values.yaml y vale ClusterIP, os invito a comprobarlo. Así que cuando ejecutemos helm, reemplazará dicho valor, generando algo como:

1
2
spec:
  type: ClusterIP

La clave metadata.name tiene el valor {{ include "mi-application.fullname" . }}. Ésta es la manera de invocar una “función” en el lenguaje de plantillas de go. Su implementación se encuentra en el archivo templates/_helpers.tpl y tiene esta pinta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this
(by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "mi-application.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 }}

No es muy obvio, pero tampoco complejo de seguir. Baste decir que tenemos todas las funciones proporcionadas por el sistema estándar de templates de go y las de sprig.

Finalmente, algo muy importante es que puede haber más de un archivo values.yaml, y helm los mezclará todos para obtener el valor final.

Usando helm

Basta ya de teoría y vamos a la práctica. Todos los comandos los ejecutaremos desde el directorio my-application. Vamos a instalar nuestra aplicación en my-application . un cluster Kubernetes. Para ello crearemos un cluster con Kind:

1
kind create cluster

Y en unos 5 minutos tendremos nuestro cluster en pie. Ahora usaremos helm para descargar las dependencias aunque no tengamos, ya que si no lo hacemos y hay dependencias fallará, por lo que no es una mala costumbre:

1
helm dependency update .

y ahora lo instalamos:

1
helm upgrade -i my-application .

Siempre utilizo upgrade -i ya que install fallará si ya existía.

Eso mostrará esta salida, más o menos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Release "my-application" does not exist. Installing it now.
NAME: my-application
LAST DEPLOYED: Thu Dec 30 08:12:57 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=mi-application,app.kubernetes.io/instance=my-application" -o jsonpath="{.items[0].metadata.name}")
  export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT

El final, NOTES, es el contenido renderizado del archivo templates/NOTES.txt de nuestro paquete.

Y podemos preguntar a Helm por el estado de nuestro paquete:

$ helm list
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                   APP VERSION
my-application  default         1               2021-12-30 08:12:57.930000745 +0100 CET deployed        mi-application-0.1.0    1.16.0

o ver el estado de los pods con kubectl:

$ kubectl get pods
NAME                                             READY   STATUS    RESTARTS   AGE
my-application-mi-application-7dc8c8c7df-khx7j   0/1     Pending   0          3m52s

Depurando un paquete Helm

Si nos ponemos a crear nuestro paquete Helm tarde o temprano la liaremos parda. Así que es muy útil ver qué es lo que va a generar el paquete:

1
helm template .

Nos mostrará todos los recursos que se van a generar en distintos documentos YAML separados por triple guión ---. Ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
---
# Source: mi-application/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: RELEASE-NAME-mi-application
  labels:
    helm.sh/chart: mi-application-0.1.0
    app.kubernetes.io/name: mi-application
    app.kubernetes.io/instance: RELEASE-NAME
    app.kubernetes.io/version: "1.16.0"
    app.kubernetes.io/managed-by: Helm
---
# Source: mi-application/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: RELEASE-NAME-mi-application
  labels:
    helm.sh/chart: mi-application-0.1.0
    app.kubernetes.io/name: mi-application
    app.kubernetes.io/instance: RELEASE-NAME
    app.kubernetes.io/version: "1.16.0"
    app.kubernetes.io/managed-by: Helm
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app.kubernetes.io/name: mi-application
    app.kubernetes.io/instance: RELEASE-NAME

Como vemos, los valores de .Release se expanden a valores por defecto y no a los definitivos.

Personalizando un paquete helm

Lo más divertido de un paquete Helm es que es personalizable. Basta con crear un archivo values.yaml adicional y pasárselo a upgrade, install o template para sobreescribir valores.

Por ejemplo, podemos escribir el pequeño archivo values.foo.yaml:

1
2
3
service:
  type: ClusterIP
  port: 9000

Veamos dónde estaba escuchando nuestro servicio:

$ kubectl get svc my-application-mi-application
NAME                            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
my-application-mi-application   ClusterIP   10.107.195.23   <none>        80/TCP    23m

Y ahora lo instalamos con ese archivo:

1
helm upgrade -i my-application . --values values.foo.yaml

Y volvemos a comprobar:

$ kubectl get svc my-application-mi-application
NAME                            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
my-application-mi-application   ClusterIP   10.107.195.23   <none>        9000/TCP  23m

Con lo que el puerto del servicio ha cambiado.

Recursos Helm

Es interesante conocer el mayor repositorio de paquetes helm, el artifacthub, donde podemos encontrar casi de todo, aunque puede que no siempre nos convenza lo que veamos.

También se pueden instalar plugins para Helm.

Cuando instalamos un paquete de la nube, primero tendremos que instalar el repositorio Helm. Por ejemplo, para instalar Grafana:

1
helm repo add grafana https://grafana.github.io/helm-charts

Y a continuación se puede instalar el programa:

1
helm install my-grafana grafana/grafana --version 6.20.3

Saber más

Recomiendo jugar un poco con helm: borrar paquetes, sobreescribir valores, …

Si ya se tienen algunos conocimientos de Kubernetes, a lo mejor el lector se siente con fuerzas para intentar instalar algo útil pero sencillo, como un Grafana como se comentó antes o un Prometheus.

En el próximo artículo veremos cómo instalar ambos, Grafana y Prometheus, para que trabajen juntos, lo que dará pie a hablar de más usos para kubectl, namespaces, …