Despliegues sin caídas de servicio
Conseguir despliegues automáticos sin caídas en producción es el sueño de todo desarrollador experto, o mejor aún, que exista un despliegue continuo donde no se “lanzan despliegues”.
¡Ojo!, no es algo imposible y hay muchas formas de conseguirlo sin mucha sofisticación, pero a veces supone un sobreesfuerzo del desarrollador que para “aplicar un parchecito”, no siempre compensa.
La solución a este problema, aplicable a muchos otros, es integrar el mecanismo de despliegue en la propia infraestructura. De esta forma, se consigue hacer transparente el proceso al equipo de desarrolladores sin hacerles pasar un mal rato cada vez que aplican una mejora o nueva funcionalidad en el software de producción.
Tipos de técnicas
Hay muchos tipos de técnicas. Esta vez explicamos un ejemplo práctico donde vamos a configurar dos tipos de despliegue y comparar ventajas/desventajas de cada uno:
- “rolling updates” o “rolling deployments”
Se basa en utilizar la estrategia de tipo “RollingUpdate” de un deployment de Kubernetes, donde se genera un replicaset paralelo con nuevos pods de la nueva versión del software. Estos pods no reciben tráfico hasta que la nueva versión está desplegada. Éstos informan del estado a través de los checks “readiness” y “liveness”, por lo que es esencial definir de forma fiable estos checks de servicio.
Una vez desplegados los pods de la nueva versión correctamente, se produce el cambio de replicaset en el deployment sin pérdida de servicio de las conexiones entrantes. Para afinar el despliegue sin cortes de conexiones, podemos definir variables del deployment de la propia estrategia como “MaxSurge” o “MaxUnavailable” que mejorarán la forma en la que son sustituidos los pods antiguos por los nuevos.
También ayuda a definir qué hacer con las conexiones en curso de los pods antiguos, definiendo un “terminationGracePeriodSeconds” o ejecución de acciones en los hooks del deployment “lifecycle.preStop” que asegure un apagado de pods, ordenado y sin interrupciones o cortes de conexiones abiertas.
- canary deploy
Este tipo de despliegue es una técnica de actualización del software de forma escalonada, donde mediante configuraciones del balanceador o del software cliente, podemos decidir qué porcentaje de tráfico de usuario se envía a la versión nueva del software y cual otro se mantiene en la versión antigua. De manera progresiva se implanta la nueva versión en la plataforma y se va eliminando la versión antigua. En caso de detectar alguna incidencia de mal funcionamiento, podemos revertir el reenvío de tráfico a la versión nueva y solucionarlo haciendo o no, rollback de los cambios según decida el equipo de desarrollo.
Tomando como base esta técnica, se puede implementar a/b testing, blue-green, beta-testing… y todo tipo de despliegues para definir, en definitiva, un despliegue por fases con los matices que aporta cada una de las soluciones, para adaptarse a cada caso de uso particular de los equipos de desarrollo.
Un ejemplo que nos ayude a entenderlo:
Partimos de una implementación en Kubernetes de un servicio web con ingress + nginx-controller y un backend con “n” pods con el software del cliente.
Configuramos dos “ingress” idénticos en kubernetes ambos con el mismo dominio.
A: ingress de producción (stable): gestionará el tráfico de la versión actual.
B: ingress canary: gestionará el tráfico de la nueva versión.
En el ingress de tipo “canary” se definen algunos parámetros de configuración que nos permiten enviar tráfico seleccionado a la nueva versión del software en base a:
- Peso (weight): porcentaje de peticiones que atenderá.
- Cookies: atenderá las peticiones que tenga una cookie establecida previamente.
- Cabeceras específicas: se atienden las peticiones que tengan una cabecera concreta.
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20"
nginx.ingress.kubernetes.io/canary-by-header: "x-my-new-version"
nginx.ingress.kubernetes.io/canary-by-header-value: “v2”
nginx.ingress.kubernetes.io/canary-by-cookie: “cookie-name=value”
Si combinamos varias opciones el orden de aplicación es el siguiente:
- canary-by-header
- canary-by-cookie
- canary-weight
Por simplificar utilizaremos el tráfico mediante pesos:
# ingress stable
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-stable
spec:
ingressClassName: nginx-default
rules:
- host: www.mywebapp.com
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: app-v1
port:
number: 80
# ingress canary
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20" # % de tráfico
name: ingress-canary
spec:
ingressClassName: nginx-default
rules:
- host: www.mywebapp.com
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: app-v2
port:
number: 80
El ingress de tipo “stable”, atenderá el resto de peticiones con la versión de software actual.
Variando el parámetro nginx.ingress.kubernetes.io/canary-weight de forma progresiva, podemos enrutar tráfico de usuario a la nueva versión del software hasta llegar al 100%.
Una vez reenviado todo el tráfico a la nueva versión, podemos eliminar los pods de la versión actual y reconfigurar los ingress para deshabilitar las peticiones de tipo canary y eliminar el ingress sobrante.
En definitiva, no siempre se necesita tener una implementación compleja para desplegar software en producción. La propia infraestructura puede implementar este proceso de forma transparente al equipo de desarrollo de forma que se dediquen a hacer lo que mejor saben hacer: desarrollar software de calidad; el resto se encarga la infraestructura con automatización de despliegues.