Despliega tus aplicaciones en Kubernetes con Helm (III): Crea tus propios Charts

Publicado por SSCC el

DevOpsKubernetesK8sHelmMonocularChart Museum

En este último artículo de la serie Como desplegar tus aplicaciones en Kubernetes con Helm, vamos a crear nuestro propio chart, completando así el ciclo de vida de empaquetado de una aplicación sobre Kubernetes usando Helm.

Partiendo de lo que hemos aprendido en los artículos anteriores, lo que faltaría hacer es crearnos un chart a partir de una aplicación de ejemplo para instalarla en  Chart Museum y poder visualizarla mediante Monocular.

Requisitos

En esta sección vamos a asegurarnos que tenemos las piezas necesarias que hemos ido instalando en los artículos anteriores (Helm, Monocular y Chart Museum). También estamos usando minikube con 4GB de RAM y kubernetes 1.15.2.

Nota: Puedes seguir los post anteriores para instalar estas piezas.

Ingress

kubectl get pods -n kube-system -w | grep ingress  

Helm

kubectl get pods -w -n kube-system | grep tiller  

Chart Museum

kubectl get pods -w -n chart-museum  

Monocular

kubectl get pods -w -n monocular  

La aplicación

Vamos a utilizar como aplicación de ejemplo para empaquetar con Helm la voting app de docker.

Esta aplicación tiene la siguiente arquitectura:

Vamos a ir dando pasos en la dirección de refactorizar el despliegue, que inicialmente vamos a realizar mediante los descriptores de Kubernetes (sin Helm). Posteriormente iremos añadiendo a nuestro chart cada uno de los servicios hasta que todos estén con formato Helm.

Desplegamos el stack completo con los descriptores que podemos encontrar en el repositorio.

kubectl create ns voting-app  
kubectl apply -Rf k8s-specs/ -n voting-app  

Los dos frontend's son vote y result, que son accesibles por los puertos 31000 y 31001 respectivamente sobre la ip de minikube (minikube ip).

vote

result

Creación del chart

Creamos el chart con helm create:

helm create voting-app-enmilocalfunciona

Obtenemos la siguiente estructura de ficheros:

enmilocalfunciona-voting-app  
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── ingress.yaml
│   ├── deployment.yaml
│   ├── service.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml
  • Chart.yaml: Contiene la descripción del chart. Esta información es accesible desde un template.
  • charts/ puede contener otros chart, que llamaremos subcharts.
  • templates/: Es el directorio para los ficheros template y donde se lanza internamente el motor de renderización de templates.(NOTES.txt, helpers.tpl y tests/ están fuera del alcance de este post. Para más información consultar enlace-1, enlace-2 y enlace-3.
  • deployment.yaml y service.yaml son manifests creados por defecto y que usaremos como punto de partida para crear el chart. ingress.yaml lo podemos borrar, ya que nuestro ejemplo no usa ingress.

Vote

Vamos a empezar con el frontal (vote). La idea es que nos vamos a basar en los manifests deployment.yamly service.yaml para todas las aplicaciones. Nos creamos una carpeta vote dentro de templates y movemos deployment.yaml y service.yaml. También para facilitar la creación del chart vamos a eliminar NOTES.txt:

enmilocalfunciona-voting-app  
├── Chart.yaml
├── templates
│   ├── _helpers.tpl
│   └── vote
│       ├── deployment.yaml
│       └── service.yaml
└── values.yaml

A partir del manifest service.yaml original, modificamos el service.yaml template para que renderice con la misma salida:

apiVersion: v1  
kind: Service  
metadata:  
  name: {{ include "voting-app.vote.fullname" . }}
  labels:
{{ include "voting-app.labels" . | indent 4 }}
spec:  
  type: {{ .Values.vote.service.type }}
  ports:
    - port: {{ .Values.vote.service.port }}
      targetPort: http
      protocol: TCP
      name: http
      nodePort: {{ .Values.vote.service.nodePort }}
  selector:
    app.kubernetes.io/name: {{ include "voting-app.vote.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}

Realizamos el mismo proceso con deployment.yaml
original:

apiVersion: apps/v1  
kind: Deployment  
metadata:  
  name: {{ include "voting-app.vote.fullname" . }}
  labels:
{{ include "voting-app.labels" . | indent 4 }}
spec:  
  replicas: {{ .Values.vote.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "voting-app.vote.name" . }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ include "voting-app.vote.name" . }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      containers:
        - name: {{ include "voting-app.vote.name" . }}
          image: "{{ .Values.vote.image.repository }}:{{ .Values.vote.image.tag }}"
          imagePullPolicy: {{ .Values.vote.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.vote.container.port }}
              protocol: TCP

Necesitamos modificar Values.yaml donde crearemos el mapping entre variables (nótese que estamos creando una jerarquía Values.vote.XXX.YYY)

# Voting app
vote:  
  nameOverride: "vote"
  service:
    type: NodePort
    port: 5000
    nodePort: 31000
  replicaCount: 1
  image:
    repository: dockersamples/examplevotingapp_vote
    tag: before
    pullPolicy: IfNotPresent
  container:
    port: 80

Necesitamos añadir algunas variables custom dentro de _helpers.tpl:

...
{{- define "voting-app.vote.name" -}}
{{- default .Chart.Name .Values.vote.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
...

{{- define "voting-app.vote.fullname" -}}
{{- if .Values.vote.fullnameOverride -}}
{{- .Values.vote.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.vote.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

Borramos la aplicación con kubectl:

kubectl delete all -lapp=vote -n voting-app  

y ahora desplegamos mediante Helm:

helm upgrade --install voting-app-enmilocalfunciona . --namespace voting-app  

Comprobamos que vote se ha desplegado correctamente:

y que es accesible por el puerto 31000:

Redis

Seguimos con el mismo planteamiento, partiendo de manifests deployment.yamly service.yaml creamos los templates dentro de una nueva carpeta redis.

service.yaml:

apiVersion: v1  
kind: Service  
metadata:  
  # we need to use this service name as vote as redis hardcoded!!!
  name: redis
  labels:
{{ include "voting-app.labels" . | indent 4 }}
spec:  
  type: {{ .Values.redis.service.type }}
  ports:
  - port: {{ .Values.redis.service.port }}
    targetPort: {{ .Values.redis.service.port }}
  selector:
    app.kubernetes.io/name: {{ include "voting-app.redis.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}

deployment.yaml

apiVersion: extensions/v1beta1  
kind: Deployment  
metadata:  
  name: {{ include "voting-app.redis.fullname" . }}
  labels:
{{ include "voting-app.labels" . | indent 4 }}
spec:  
  replicas: {{ .Values.redis.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "voting-app.redis.name" . }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ include "voting-app.redis.name" . }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      containers:
      - image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}"
        imagePullPolicy: {{ .Values.redis.image.pullPolicy }}
        name: {{ include "voting-app.redis.name" . }}
        volumeMounts:
        - mountPath: /data
          name: redis-data
      volumes:
      - name: redis-data
        emptyDir: {}  

Y añadimos las variables a Values.yaml.

...
# Redis
redis:  
  nameOverride: "redis"
  replicaCount: 1
  image:
    repository: redis
    tag: alpine
    pullPolicy: IfNotPresent
  service:
    type: ClusterIP
    port: 6379

Necesitamos añadir algunas variables custom dentro de _helpers.tpl:

...
{{- define "voting-app.redis.name" -}}
{{- default .Chart.Name .Values.redis.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
...

{{- define "voting-app.redis.fullname" -}}
{{- if .Values.redis.fullnameOverride -}}
{{- .Values.redis.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.redis.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

Borramos la versión de redis basada en manifests:

kubectl delete all -lapp=redis -n voting-app  

Redesplegamos una nueva release del chart:

helm upgrade --install voting-app-enmilocalfunciona . --namespace voting-app  

Comprobamos que se ha desplegado correctamente:

Worker

Seguimos los mismos pasos que anteriormente.
Nota: Worker no require service, sólo deployment.

deployment.yaml:

apiVersion: apps/v1  
kind: Deployment  
metadata:  
  name: {{ include "voting-app.worker.fullname" . }}
  labels:
{{ include "voting-app.labels" . | indent 4 }}
spec:  
  replicas: {{ .Values.worker.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "voting-app.worker.name" . }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ include "voting-app.worker.name" . }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      containers:
      - image: {{ .Values.worker.image.repository }}
        imagePullPolicy: {{ .Values.worker.image.pullPolicy }}
        name: {{ include "voting-app.worker.name" . }}

Values.yaml

...
# Worker
worker:  
  nameOverride: "worker"
  replicaCount: 1
  image:
    repository: dockersamples/examplevotingapp_worker
    pullPolicy: IfNotPresent

Necesitamos añadir algunas variables custom dentro de _helpers.tpl:

...
{{- define "voting-app.worker.name" -}}
{{- default .Chart.Name .Values.worker.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
...

{{- define "voting-app.worker.fullname" -}}
{{- if .Values.worker.fullnameOverride -}}
{{- .Values.worker.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.worker.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

Borramos el despliegue basado en manifests:

kubectl delete all -lapp=worker -n voting-app  

Redesplegamos una nueva release del chart:

helm upgrade --install voting-app-enmilocalfunciona . --namespace voting-app  

Comprobamos que se ha desplegado correctamente:

Db

Seguimos los mismos pasos que anteriormente.

service.yaml

apiVersion: v1  
kind: Service  
metadata:  
  name: db # we need to use this service name as worker as db hardcoded!!!
  labels:
{{ include "voting-app.labels" . | indent 4 }}
spec:  
  type: {{ .Values.db.service.type }}
  ports:
  - port: {{ .Values.db.service.port }}
    targetPort: {{ .Values.db.service.port }}
  selector:
    app.kubernetes.io/name: {{ include "voting-app.db.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}

deployment.yaml:

apiVersion: extensions/v1beta1  
kind: Deployment  
metadata:  
  name: {{ include "voting-app.db.fullname" . }}
  labels:
{{ include "voting-app.labels" . | indent 4 }}
spec:  
  replicas: {{ .Values.db.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "voting-app.db.name" . }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ include "voting-app.db.name" . }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      containers:
      - image: "{{ .Values.db.image.repository }}:{{ .Values.db.image.tag }}"
        name: {{ include "voting-app.db.name" . }}
        volumeMounts:
        - mountPath: /var/lib/postgresql/data
          name: db-data
      volumes:
      - name: db-data
        emptyDir: {}

Values.yaml

...
# Db
db:  
  nameOverride: "db"
  replicaCount: 1
  image:
    repository: postgres
    tag: 9.4
    pullPolicy: IfNotPresent
  service:
    type: ClusterIP
    port: 5432

Necesitamos añadir algunas variables custom dentro de _helpers.tpl:

...
{{- define "voting-app.db.name" -}}
{{- default .Chart.Name .Values.db.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
...

{{- define "voting-app.db.fullname" -}}
{{- if .Values.db.fullnameOverride -}}
{{- .Values.db.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.db.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

Borramos el despliegue basado en manifests:

kubectl delete all -lapp=db -n voting-app  

Redesplegamos una nueva release del chart:

helm upgrade --install voting-app-enmilocalfunciona . --namespace voting-app  

Comprobamos que se ha desplegado correctamente:

Result

Seguimos los mismos pasos que anteriormente.

service.yaml

apiVersion: v1  
kind: Service  
metadata:  
  name: {{ include "voting-app.result.fullname" . }}
  labels:
{{ include "voting-app.labels" . | indent 4 }}
spec:  
  type: {{ .Values.result.service.type }}
  ports:
  - name: result-service
    port: {{ .Values.result.service.port }}
    targetPort: {{ .Values.result.service.targetPort }}
    nodePort: {{ .Values.result.service.nodePort }}
  selector:
    app.kubernetes.io/name: {{ include "voting-app.result.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}

deployment.yaml:

apiVersion: extensions/v1beta1  
kind: Deployment  
metadata:  
  name: {{ include "voting-app.result.fullname" . }}
  labels:
{{ include "voting-app.labels" . | indent 4 }}
spec:  
  replicas: {{ .Values.result.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "voting-app.result.name" . }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ include "voting-app.result.name" . }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      containers:
      - image: "{{ .Values.result.image.repository }}:{{ .Values.result.image.tag }}"
        imagePullPolicy: {{ .Values.vote.image.pullPolicy }}
        name: {{ include "voting-app.vote.name" . }}

Values.yaml

# Result
result:  
  nameOverride: "result"
  replicaCount: 1
  image:
    repository: dockersamples/examplevotingapp_result
    tag: before
    pullPolicy: IfNotPresent
  service:
    type: NodePort
    port: 5001
    targetPort: 80
    nodePort: 31001

Necesitamos añadir algunas variables custom dentro de _helpers.tpl:

...
{{- define "voting-app.result.name" -}}
{{- default .Chart.Name .Values.db.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
...

{{- define "voting-app.result.fullname" -}}
{{- if .Values.result.fullnameOverride -}}
{{- .Values.result.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.result.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

Borramos el despliegue basado en manifests:

kubectl delete all -lapp=result -n voting-app  

Redesplegamos una nueva release del chart:

helm upgrade --install voting-app-enmilocalfunciona . --namespace voting-app  

Comprobamos que se ha desplegado correctamente:

Publicar el chart

Le pasamos lint

helm lint .  

Empaquetamos el chart

helm package .  

Obtenemos el nombre del chart empaquetado para subirlo a chart museum

Chart Museum no es accesible desde fuera del clúster con lo cual habilitamos acceso a chart museum via NodePort. Además por defecto el acceso via API viene deshabilitado.

Redesplegamos chart museum:

helm upgrade --install chart-museum stable/chartmuseum --set service.type=NodePort,service.nodePort=32000,env.open.DISABLE_API=false --namespace chart-museum  

Publicamos el chart (alternativamente podríamos haber usado helm-push plugin):

curl -L --data-binary "@enmilocalfunciona-voting-app-0.1.0.tgz" $(minikube ip):32000/apis/charts

{"saved":true}% 

Ahora vamos a comprobar que el chart está publicado en nuestro repositorio de chart museum.

Via curl:

curl $(minikube ip):32000/apis/charts

{
  "enmilocalfunciona-voting-app": [
    {
      "name": "enmilocalfunciona-voting-app",
      "version": "0.1.0",
      "description": "enmilocalfunciona.io voting-app Helm chart for Kubernetes",
      "maintainers": [
        {
          "name": "Carlos M. Cornejo",
          "email": "cmcornejo@atsistemas.com",
          "url": "https://enmilocalfunciona.io/tag/helm/"
        }
      ],
      "icon": "https://enmilocalfunciona.io/content/images/2019/08/Perrete_helm-02-2-1.png",
      "apiVersion": "v1",
      "appVersion": "1.0",
      "urls": [
        "charts/enmilocalfunciona-voting-app-0.1.0.tgz"
      ],
      "created": "2019-09-23T10:22:50.666Z",
      "digest": "a619866858a2fcd28fd2cbe9e0822f3da652abdce4bce8088e9311af46376937"
    }
  ]
}

Vía Monocular:

Vemos que no aparece ....

La razón por lo que no aparece el chart que hemos creado es porque hay que esperar a que se ejecute el cron que actualiza los chart de chart museum en monocular (lo pusimos cada minuto).

y tras actualizar aparece:

Clean up

minikube delete  

Repositorio de código fuente

Podéis encontrar todo el código que se ha utilizado a lo largo de este post en el repositorio accesible en este enlace.

Conclusiones

Hemos implementado un primer proceso (opinionated) de empaquetado con Helm end-to-end. El siguiente paso sería el de crear un chart por cada uno de los servicios que componen voting app y referenciarlos internamente mediante el fichero requirements.yaml y subcharts (más información en este enlace). Más aspectos a considerar podrían ser el firmado de charts, securización de endpoints, gestión de la persistencia de chart museum, etc..

También hay que destacar que la última versión de Helm a día de hoy está en beta (3.0.0-beta.1) y que sustituirá la versión actual a corto/medio plazo. Para más información consultar el release notes de la v3.

Si te ha gustado, ¡síguenos en Twitter para estar al día de nuevas entregas!