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
ytests/
están fuera del alcance de este post. Para más información consultar enlace-1, enlace-2 y enlace-3.deployment.yaml
yservice.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.yaml
y 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.yaml
y 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!