Uno de los trabajos que tenemos en Sistemas es el de simplificar las operaciones de las infraestructuras que montamos, haciendo que sea lo menos doloroso posible para los desarrolladores.

Actualmente Kubernetes es uno de los mejores orquestadores de contenedores ya que, entre otras cosas, simplifica el despliegue de nuevos microservicios.

Pero aún con la simplificación, requiere de ciertos conocimientos que aportan poco valor a los desarrolladores. Por ello, una solución para simplificar aún más los despliegues es añadir una capa de abstracción con Helm.

Con este programa escrito en Go, l****os desarrolladores pueden concentrarse en las características principales de los despliegues sin que tengan la necesidad de entender Kubernetes.

Por ejemplo, en vez de preocuparse de que versión la API de Kubernetes hay que usar para el objeto deployment y en qué estado de estabilidad está, simplemente tienen que ponerle un nombre al despliegue, definir una imagen y si les interesa, limitar los recursos que usará el despliegue.

¿Qué es Helm?

Es una herramienta para gestionar recursos empaquetados y preconfigurados de Kubernetes. Dicho de otra forma, Helm permite gestionar unas plantillas que facilitan la instalación de cualquier objeto de Kubernetes o derivados, por lo que no hace falta tener conocimientos avanzados de estos para desplegarlos. Es lo mismo que apt en Debian.

Por ejemplo, si quisiéramos instalar un registry de Docker en nuestro cluster de Kubernetes, solo tendríamos que ejecutar la siguiente orden:


helm install stable/docker-registry

Sin más. Modificar un valor del anterior despliegue es muy fácil, si quisiéramos, por ejemplo, definir el número de réplicas que tendrá el despliegue, sería tan simple como añadir el parámetro --set replicaCount=2.

El siguiente paso es empezar a crear charts (es como se llaman las plantillas de Helm). A continuación examinaremos un chart (que está disponible en el Github de Paradigma), cuya función es desplegar microservicios en Java, ya sea usando el stack de Netflix (eureka-server y config-server) o confiando en Kubernetes para el descubrimiento de servicios y la configuración de estos.

En la raíz del repositorio vemos lo siguiente:


base-java-microservice
├── Chart.yaml
├── README.md
├── templates
│   ├── horizontal-autoscaling.yaml
│   ├── load-balancer.yaml
│   └── micro-service.yaml
└── values.yaml

Esta es la estructura básica que tiene cualquier chart de Helm. Desgranemos:

En el directorio templates tenemos tres ficheros, que son:

Estructura de un chart de Helm

Como podemos ver, este es el aspecto de un yaml cualquiera de Kubernetes, hasta que llegamos al valor name.


apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: {{required "A valid .Values.msName entry required!" .Values.msName | quote}}

horizontal-autoscaling.yaml

Aquí veremos dos corchetes que tienen una apariencia extraña. Este es el equivalente a jinja2 de Go, el llamado Go template.

Básicamente sustituye todo lo que hay entre corchetes por la variable .Values.msName, a menos que esta no esté definida, en cuyo caso dará un error avisando de que hace falta definir esa variable. Esto último lo provoca el required.


spec:
  replicas: {{.Values.minReplicas}}
  template:
    metadata:
      labels:
        app: {{required "A valid .Values.msName entry required!" .Values.msName | quote}}

horizontal-autoscaling.yaml

Aquí vemos la especificación de los metadatos del despliegue que estamos creando. Una vez más, tenemos una variable que sustituiremos, que es .Values.minReplicas, que define el número de réplicas mínimas que queremos que tenga el pod.

Además, se añadirá una etiqueta llamada app con el nombre del microservicio, que es la variable .Values.msName.


spec:
      containers:
      - env:
        - name: NAME
          value: {{required "A valid .Values.msName entry required!" .Values.msName | quote}}
        - name: JAVA_OPTS_EXT
          value: {{.Values.javaOpts | quote}}
          {{if .Values.javaParameters}}
        - name: JAVA_PARAMETERS
          value: {{printf "%s --spring.profiles.active=%s" .Values.javaParameters .Values.environment}}
          {{else}}
        - name: JAVA_PARAMETERS
          value: {{printf "--spring.profiles.active=%s" .Values.environment}}
          {{end}}

Las imágenes Docker que solemos usar para microservicios con Spring Boot buscan una serie de variables de entorno:

Si os fijáis, exceptuando la variable .Values.msName, las demás variables no tienen un required delante.

Esto es debido a que este chart de Helm solo tiene como obligatorias dos variables, el nombre del microservicio (msName) y el nombre de la imagen docker a usar en el despliegue (dockerImage).

Las demás variables tienen un valor por defecto, que como ya hemos visto, se concretan en el fichero values.yml**.**

En este caso, JAVA_OPTS_EXT equivale a -Djava.security.egd=file:/dev/./urandom -Xms256m -Xmx512M.

Al definir la variable de entorno JAVA_PARAMETERS, vemos la primera estructura de control, un condicional. Si la variable javaParameters está definida, concatena el valor de esta variable con el string --spring.profiles.active y el valor del entorno, que es lo que usamos para escoger el perfil de Spring Boot.

Si no está definida, solo se concatena el string para elegir el perfil de spring boot junto a la variable de entorno.


        {{if (.Values.isConfigServer) and (.Values.netflix)}}
        - name: EUREKA_SERVER_URL
          value: http://eureka-server:8080
        {{else if .Values.netflix}}
        - name: CONFIG_SERVER
          value: {{.Values.configServer | quote}}
        {{end}}

Esta parte es relevante para los microservicios que usan la suite de Netflix, como el config-server y eureka-server.

En la primera línea podemos ver un ejemplo de cómo agrupar dos condicionales, que son tanto que exista la variable isConfigServer como la variable Netflix.

Cuando ambas variables están definidas, se crea una variable de entorno para el pod llamada EUREKA_SERVER_URL, que Spring Boot buscará para apuntar al servidor de Eureka.

Cuando no esté definida esta variable, pero sí lo esté la variable Netflix (es decir, cuando el microservicio que desplegamos no sea el config-server) se creará una variable de entorno llamada CONFIG_SERVER que apuntará al config-server.

Si no estuviese definida la variable Netflix, esta parte se ignorará y no se añadirá ninguna variable de entorno.


        image: {{required "A valid .Values.dockerImage entry required!" .Values.dockerImage | quote}}
        {{if .Values.imagePullPolicy}}
        imagePullPolicy:  {{.Values.imagePullPolicy | quote}}
        {{else}}
        imagePullPolicy: IfNotPresent
        {{end}}
        name: {{required "A valid .Values.msName entry required!" .Values.msName | quote}}

Esta parte es bien simple, comparada con lo que ya hemos visto. Vemos otra variable que es necesaria para que el chart de Helm funcione (definida con el required), que es dockerImage.

También podemos ver la política de pulleo de la imagen de docker, que si no está definida, tendrá el valor por defecto IfNotPresent.

Por último, vemos el nombre del microservicio, otra variable requerida para el funcionamiento del chart de Helm, como ya hemos visto.


        ports:
        - containerPort: {{.Values.containerPort}}
          protocol: TCP

Aquí se define el puerto que se expondrá del pod.


        readinessProbe:
          httpGet:
            path: {{.Values.readinessPath}}
            port: {{.Values.containerPort}}
          initialDelaySeconds: {{.Values.readinessDelay}}
          timeoutSeconds: {{.Values.readinessTimeout}}
          periodSeconds: {{.Values.readinessPeriod}}
          successThreshold: {{.Values.readinessSuccess}}
          failureThreshold: {{.Values.readinessFailure}}
        livenessProbe:
          httpGet:
            port: {{.Values.containerPort}}
            path: {{.Values.livenessPath}}
          initialDelaySeconds: {{.Values.livenessDelay}}
          timeoutSeconds: {{.Values.livenessTimeout}}
          periodSeconds: {{.Values.livenessPeriod}}
          successThreshold: {{.Values.livenessSuccess}}
          failureThreshold: {{.Values.livenessFailure}}

Esta sección es de las más importantes, ya que se definen los endpoint que se usarán para comprobar que los contenedores están sanos.


        resources:
          requests:
            cpu: {{.Values.requestsCpu | quote}}
            memory: {{.Values.requestsMemory | quote}}
          limits:
            cpu: {{.Values.limitsCpu | quote}}
            memory: {{.Values.limitsMemory | quote}}

En esta penúltima parte, se puede concretar los valores de los recursos máximos a consumir por el despliegue.


      {{if .Values.hasConfigMap}}
        volumeMounts:
        - name: config
          mountPath: {{.Values.configMapPath}}{{.Values.hasConfigMap}}
          readOnly: true
          subPath: {{.Values.hasConfigMap}}
      volumes:
      - name: config
        configMap:
          defaultMode: 0644
          name: {{.Values.msName}}
      {{end}}

En esta última sección, podemos definir un configmap a usar. El uso de esta variable tiene sentido si no estamos usando el config-server de Netflix.

Y por fin hemos terminado. El mejor sitio para empezar a aprender sobre la creación de charts de Helm es en la propia documentación, en la que además se definen unas buenas prácticas.

Como se puede ver, es muy recomendable empezar a usar esta herramienta, ya que además de aportar lo que hemos ya visto, se está convirtiendo en el estándar del empaquetado de aplicaciones en Kubernetes.

Herramientas como Prometheus, Grafana o Elasticsearch ya se pueden instalar en una sola orden de terminal. ¡No esperes más a usarlo!

PD: Este chart de Helm tiene licencia GPL3, por lo que cualquier mejora o sugerencia será bienvenida.

Notas

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.