Guía práctica de Kubernetes

Proyectos para crear aplicaciones de éxito con Kubernetes

Brendan Burns, Eddie Villalba,

Dave Strebel y Lachlan Evenson

Guía práctica de Kubernetes

Proyectos para crear aplicaciones

de éxito con Kubernetes

Brendan Burns, Eddie Villalba,

Dave Strebel y Lachlan Evenson

Edición original publicada en inglés por O’Reilly con el título Kubernetes Best Practices, ISBN 978-1-492-05647-8 © Brendan Burns, Eddie Villalba, Dave Strebel y Lachlan Evenson, 2020. This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same.

Título de la edición en español:

Guía práctica de Kubernetes

Primera edición en español, 2021

© 2021 MARCOMBO, S.L.

www.marcombo.com

Diseño de portada: Karen Montgomery

Ilustración: Rebecca Demarest

Traducción: Francisco Martínez Carreno

Corrección: Anna Alberola

Producció del ebook: booqlab

«Cualquier forma de reproducción, distribución, comunicación pública o transformación de esta obra solo puede ser realizada con la autorización de sus titulares, salvo excepción prevista por la ley. La presente publicación contiene la opinión del autor y tiene el objetivo de informar de forma precisa y concisa. La elaboración del contenido, aunque se ha trabajado de forma escrupulosa, no puede comportar una responsabilidad específica para el autor ni el editor de los posibles errores o imprecisiones que pudiera contener la presente obra.»

ISBN: 978-84-267-3244-6

Contenidos

Prefacio

Reconocimientos

1. Configuración de un servicio básico

Visión general de la aplicación

Gestión de archivos de configuración

Creación de un servicio replicado mediante Deployments

Mejores prácticas para la gestión de imágenes

Creación de una aplicación replicada

Configuración de Ingress externa para tráfico HTTP

Configuración de la aplicación con ConfigMaps

Gestión de autenticación con Secrets

Despliegue de una sencilla base de datos con estado

Creación de un equilibrador de carga TCP con Services

Uso de Ingress para enrutar el tráfico a un servidor de archivos estáticos

Parametrización de la aplicación utilizando Helm

Mejores prácticas en el despliegue de servicios

Resumen

2. Flujos de trabajo para desarrolladores

Objetivos

Creación de un clúster de desarrollo

Configuración de un clúster compartido por varios desarrolladores

Registro de usuarios

Creación y dotación de seguridad a un espacio de nombres

Administración de espacios de nombres

Servicios a nivel de clúster

Habilitación de flujos de trabajo para desarrolladores

Instalación inicial

Preparación de la fase de desarrollo activo

Preparación de pruebas y depuración

Mejores prácticas en el establecimiento de un entorno de desarrollo

Resumen

3. Monitorización y recopilación de registros en Kubernetes

Métricas versus registros

Técnicas de monitorización

Formas de monitorización

Visión general de las métricas en Kubernetes

cAdvisor

Servidor de métricas

kube-state-metrics

¿Qué métricas debemos monitorizar?

Herramientas de monitorización

Monitorización en Kubernetes con Prometheus

Descripción general de la recopilación de registros

Herramientas para la recopilación de registros

Recopilación de registros mediante la pila EFK

Alertas

Mejores prácticas para monitorización, recopilación de registros y alertas

Monitorización

Recopilación de registros

Alertas

Resumen

4. Configuración, Secrets y RBAC

Configuración mediante ConfigMaps y Secrets

ConfigMaps

Secrets

Mejores prácticas habituales para las API de ConfigMap y Secrets

Mejores prácticas específicas en Secrets

RBAC

Manual de RBAC

Sujetos

Reeglas

Roles

RoleBindings

Mejores prácticas de RBAC

Resumen

5. Integración continua, pruebas y despliegue

Control de versiones

Integración continua

Pruebas

Compilación de contenedores

Etiquetados de imágenes de contenedores

Despliegue continuo

Estrategias de despliegue

Pruebas en producción

Configuración de una pipeline y realización de un experimento de caos

Configuración de CI

Configuración de CD

Realización de la actualización de puesta en marcha

Un sencillo experimento de caos

Mejores prácticas para CI/CD

Resumen

6. Versionado, versiones de lanzamiento y puesta en marcha

Versionado

Versiones de lanzamiento

Puesta en marcha

Todo junto

Mejores prácticas para versionado, versiones de lanzamiento y puesta en marcha

Resumen

7. Distribución y preproducción de aplicaciones a nivel mundial

Distribución de la imagen

Parametrización del despliegue

Tráfico con equilibrio de carga a nivel mundial

Puesta en marcha confiable de software a nivel mundial

Validación previa al despliegue

Región de canario

Identificación de los tipos de región

Elaboración de la puesta en marcha a nivel global

Cuando algo sale mal

Mejores prácticas de puesta en marcha a nivel mundial

Resumen

8. Administración de recursos

Planificador de Kubernetes

Predicados

Prioridades

Técnicas avanzadas de planificación

Afinidad y antiafinidad de cápsulas

nodeSelector

Manchas y tolerancias

Administración de recursos de cápsulas

Solicitud de recursos

Límites a los recursos y calidad de servicio de cápsulas

PodDisruptionBudgets

Mínimo disponible

Máximo no disponible

Administración de recursos mediante espacios de nombres

ResourceQuota

LimitRange

Escalado de clúster

Escalado manual

Escalado automático de clúster

Escalado de aplicaciones

Escalado con HPA

HPA con métricas personalizadas

Vertical Pod Autoscaler

Mejores prácticas en la gestión de recursos

Resumen

9. Interconexión, seguridad en red y malla de servicios

Principios de red en Kubernetes

Complementos de red

Kubenet

Mejores prácticas en Kubenet

El complemento CNI

Mejores prácticas en CNI

Servicios en Kubernetes

Tipo de servicio ClusterIP

Tipo de servicio NodePort

Tipo de servicio ExternalName

Tipo de servicio LoadBalancer

Ingress y controladores Ingress

Administración del protocolo HTTP

Mejores prácticas en servicios y controladores Ingress

Políticas de seguridad de red

Mejores prácticas en política de red

Mallas de servicios

Mejores prácticas en malla de servicios

Resumen

10. Seguridad de cápsulas y contenedores

API de PodSecurityPolicy

Habilitación de PodSecurityPolicy

Anatomía de PodSecurityPolicy

Retos de PodSecurityPolicy

Políticas con incumplimientos razonables

Mucho esfuerzo

¿Están interesados nuestros desarrolladores en aprender PodSecurityPolicy?

La depuración es engorrosa

¿Confiamos en artefactos fuera de nuestro control?

Mejores prácticas en PodSecurityPolicy

Siguientes pasos en PodSecurityPolicy

Aislamiento de tareas y RuntimeClass

Utilización de RuntimeClass

Aplicaciones del tiempo de ejecución

Mejores prácticas en aislamiento de tareas y RuntimeClass

Otras consideraciones sobre la seguridad

Controladores de admisión

Herramientas de detección de intrusiones y anomalías

Resumen

11. Política y gobierno del clúster

Por qué la política y la gestión son importantes

¿En qué sentido esta política es diferente?

Motor de políticas nativas en la nube

Introducción a Gatekeeper

Ejemplos de políticas

Terminología Gatekeeper

Restricción

Rego

Plantilla de restricción

Definición de plantillas de restricción

Definición de restricciones

Replicación de datos

UX (Experiencias de usuario)

Auditoría

Familiarizándonos con Gatekeeper

Siguientes pasos en Gatekeeper

Mejores prácticas en política y gestión

Resumen

12. Administración de varios clústeres

¿Por qué varios clústeres?

Consideraciones sobre la utilización de varios clústeres en el diseño

Administración de despliegues de varios clústeres

Patrones de despliegue y administración

Enfoque de GitOps para la administración de clústeres

Herramientas de administración de varios clústeres

Federation de Kubernetes

Mejores prácticas en la gestión de un conjunto de clústeres

Resumen

13. Integración de servicios externos y Kubernetes

Importación de servicios a Kubernetes

Servicios sin selector para direcciones IP fijas

Servicios basados en CNAME para nombres DNS fijos

Enfoques basados en controlador activo

Exportación de servicios desde Kubernetes

Exportación de servicios mediante equilibradores de carga internos

Exportación de servicios en NodePorts

Integración entre máquinas externas y Kubernetes

Compartición de servicios entre Kubernetes

Herramientas de terceros

Mejores prácticas en conexión de clústeres y servicios externos

Resumen

14. Ejecución de aprendizaje automático en Kubernetes

¿Por qué Kubernetes es ideal para el aprendizaje automático?

Flujo de trabajo del aprendizaje automático

Aprendizaje automático para administradores de clúster de Kubernetes

Entrenamiento del modelo en Kubernetes

Entrenamiento del primer modelo en Kubernetes

Entrenamiento distribuido en Kubernetes

Restricciones de recursos

Planificación de particularidades

Hardware especializado

Bibliotecas, controladores y módulos de kernel

Almacenamiento

Almacenamiento y distribución del conjunto de datos entre nodos esclavos durante el entrenamiento

Puntos de control y modelos de grabación

Interconexión

Protocolos especializados

Preocupaciones del científico de datos

Mejores prácticas en aprendizaje automático en Kubernetes

Resumen

15. Creación de patrones de aplicaciones de alto nivel sobre Kubernetes

Enfoques para desarrollar abstracciones de alto nivel

Extensión de Kubernetes

Extensión de clústeres de Kubernetes

Ampliación de la experiencia de usuario de Kubernetes

Consideraciones de diseño en la creación de plataformas

Apoyo a la exportación de una imagen de contenedor

Soporte a los mecanismos existentes de servicios y descubrimiento de servicios

Mejores prácticas en la creación de plataformas de aplicaciones

Resumen

16. Gestión de aplicaciones con estado y apátridas

Volúmenes y montajes de volumen

Mejores prácticas en volúmenes

Almacenamiento en Kubernetes

PersistentVolume

PersistentVolumeClaims

Clases de almacenamiento

Interfaz de almacenamiento de contenedores y FlexVolume

Mejores prácticas en almacenamiento de Kubernetes

Aplicaciones con estado

StatefulSets

Operadores

Mejores prácticas en StatefulSet y Operators

Resumen

17. Control de admisión y autorización

Control de admisión

¿Qué son?

¿Por qué son importantes?

Tipos de controladores de admisión

Configuración de webhooks de admisión

Mejores prácticas en control de admisión

Autorización

Módulos de autorización

ABAC

RBAC

Webhook

Mejores prácticas de autorización

Resumen

18. Conclusión

Prefacio
A quién va dirigido este libro

Kubernetes es el estándar de facto para el desarrollo nativo en la nube. Es una potente herramienta que puede hacer que tu próxima aplicación sea más fácil de desarrollar, más rápida de desplegar y más fiable a la hora de operar. Sin embargo, para descubrir el poder de Kubernetes es necesario saber usarlo correctamente. Este libro está dirigido a cualquier persona que implemente aplicaciones del mundo real con Kubernetes y que esté interesada en aprender los diseños y las prácticas que se pueden emplear en las aplicaciones que se crean con esta herramienta.

Es importante destacar que este libro no es una introducción a Kubernetes. Suponemos que tú, lector, estás familiarizado a nivel básico con la API y con sus herramientas, y que sabes cómo crear e interactuar con el clúster de Kubernetes.

Este libro es un recurso para todos aquellos que quieran profundizar en cómo desplegar aplicaciones específicas y tareas en Kubernetes. Te será de utilidad tanto si estás a punto de desplegar tu primera aplicación con esta herramienta como si llevas años utilizando Kubernetes.

Por qué hemos escrito este libro

Entre los cuatro autores, acumulamos mucha experiencia ayudando a una gran cantidad de usuarios a desplegar sus aplicaciones en Kubernetes. A través de esta experiencia, hemos visto dónde encuentran más dificultades los usuarios, y los hemos ayudado a encontrar su camino hacia el éxito. Cuando nos sentamos a escribir este libro, intentamos plasmar estas experiencias para que muchas más personas pudieran aprender con las lecciones que asimilamos de la escucha de las necesidades de los usuarios. Esperamos que al ponerlo por escrito podamos difundir nuestro conocimiento y ayudarte a que puedas implementar y administrar con éxito tu aplicación en Kubernetes.

Exploración del libro

Aunque el libro se puede leer de principio a fin en una sola sesión, no es así como pretendemos que se haga. Lo diseñamos como una colección de capítulos independientes; cada capítulo ofrece una visión general completa de una tarea en particular de Kubernetes. Os animamos a que os sumerjáis en el contenido del libro para aprender sobre un tema o sobre algo en lo que tengáis un interés específico, y que luego lo dejéis para regresar a él solo cuando surja un nuevo tema.

A pesar de este enfoque independiente, hay algunos temas recurrentes a lo largo del libro. Por ejemplo, hay varios capítulos que tratan el desarrollo de aplicaciones en Kubernetes. El capítulo 2 se ocupa de los flujos de trabajo del desarrollador. El capítulo 5 trata sobre la integración continua y las pruebas. El capítulo 15 aborda la creación de plataformas de alto nivel sobre Kubernetes, y en el capítulo 16 se discute la gestión del estado de aplicaciones con estado. Además de tratar el desarrollo de aplicaciones, hay varios capítulos sobre servicios operativos en Kubernetes. El capítulo 1 se dedica a la configuración de un servicio básico, y el capítulo 3 trata la monitorización y las métricas. El capítulo 4 aborda la gestión de la configuración, mientras que el capítulo 6 se dedica a versiones y lanzamientos. El capítulo 7 se ocupa de la implantación de las aplicaciones a nivel mundial.

También hay varios capítulos sobre gestión de clústeres, incluido el capítulo 8 sobre gestión de recursos, el capítulo 9 sobre interconexión, el capítulo 10 sobre seguridad de cápsulas, el capítulo 11 sobre políticas y gobierno, el capítulo 12 sobre la administración de varios clústeres, y el capítulo 17 sobre control de admisión y autorización. Finalmente, hay varios capítulos que son totalmente independientes, que tratan sobre el aprendizaje automático (capítulo 14) y la integración con servicios externos (capítulo 13).

Aunque puede ser útil leer todos los capítulos antes de poner en práctica cualquier aspecto en el mundo real, tenemos la esperanza de que el libro se trate como un manual principal de referencia. Tiene la intención de ser una guía, ya que pone en práctica estos temas en el mundo real.

Convenciones que se utilizan en el libro

Este elemento indica un consejo o una sugerencia.

Este elemento indica una nota general.

Este elemento indica una advertencia o precaución.

Uso de los ejemplos de código

Hay material complementario actualizado (ejemplos de código, ejercicios, etc.) disponible para su descarga en https://oreil.ly/KBPsample.

Si tienes alguna pregunta técnica o algún problema cuando utilices los ejemplos de código, por favor, envía un correo electrónico a info@marcombo.com.

Este libro está aquí para ayudarte a hacer tu trabajo. Los códigos de ejemplo que se facilitan en este libro puedes usarlos en tus programas y documentación. No necesitas ponerte en contacto con nosotros para pedir permiso, a menos que vayas a reproducir una parte importante del código. Por ejemplo, escribir un programa que use varios fragmentos de código de este libro no requiere permiso; vender o distribuir ejemplos de los libros de Marcombo requiere permiso; responder a una pregunta citando este libro y citar un código de ejemplo no requiere permiso; y la incorporación de una cantidad importante de código de los ejemplos de este libro en la documentación de tu producto requiere permiso.

Generalmente no pedimos que se incluya una atribución, pero apreciamos que se haga. Una atribución contiene título, autor, editor e ISBN. Por ejemplo: Guía práctica de Kubernetes de Brendan Burns, Eddie Villalba, Dave Strebel y Lachlan Evenson, Editorial Marcombo, ISBN: 978-84-267-2880-7.

Si crees que el uso por tu parte de los ejemplos de código no está justificado o no respeta los permisos otorgados más arriba, no dudes en ponerte en contacto con nosotros en info@marcombo.com.

Reconocimientos

A Brendan le gustaría dar las gracias a su maravillosa familia, Robin, Julia y Ethan, por el amor y el apoyo que le brindan en todo lo que hace; a la comunidad de Kubernetes, sin la cual nada de esto sería posible; y a sus fabulosos coautores, sin los cuales este libro no existiría.

A Dave le gustaría agradecer a su bella esposa, Jen, y a sus tres hijos, Max, Maddie y Mason, todo su apoyo. Agradece también a la comunidad de Kubernetes todos los consejos y la ayuda que ha brindado a lo largo de los años. Finalmente, le gustaría dar las gracias a sus coautores por hacer realidad esta aventura.

A Lachlan le gustaría agradecer a su esposa y a sus tres hijos su amor y su apoyo. También le gustaría dar las gracias a todos los componentes de la comunidad de Kubernetes, incluidas las maravillosas personas que han dedicado su tiempo a enseñarle a lo largo de los años. Quisiera enviar un agradecimiento especial a Joseph Sandoval por su tutoría. Y, finalmente, querría dar las gracias a sus fantásticos coautores por hacer posible este libro.

A Eddie le gustaría agradecer a su esposa, Sandra, su apoyo moral y que lo dejara desaparecer durante horas para escribir mientras ella estaba en el último trimestre de su primer embarazo. También le gustaría agradecer a su nueva hija, Giavanna, el impulso que le ha dado para seguir adelante. Finalmente, querría dar las gracias a la comunidad de Kubernetes y a sus coautores, que siempre han sido guías en su viaje para convertirse en un nativo de la nube.

A todos nos gustaría agradecer a Virginia Wilson su trabajo en el desarrollo del manuscrito y su ayuda a la hora de reunir todas nuestras ideas; y a Bridget Kromhout, Bilgin Ibryam, Roland Huß y Justin Domingus, por cuidar los detalles finales.

CAPÍTULO 1
Configuración de un servicio básico

En este capítulo se describen las prácticas para configurar una sencilla aplicación multinivel en Kubernetes. La aplicación consta de una aplicación web básica y de una base de datos. Aunque seguramente no se trata de la aplicación más complicada, es un buen ejemplo para comenzar a orientarnos en la administración de una aplicación en Kubernetes.

Visión general de la aplicación

La aplicación que usaremos para nuestro ejemplo no es particularmente compleja. Es un sencillo servicio de publicaciones que almacena sus datos en un backend de Redis. Tiene un servidor de archivos estáticos independiente que usa NGINX. Presenta dos rutas web en una sola URL. Una de ellas es para la interfaz de programación de aplicaciones (API) RESTful de la publicación, https://my-host.io/api, y la otra es un servidor de archivos en la URL principal, https://my-host.io. Utiliza el servicio Let’s Encrypt para administrar certificados de la capa de conexión segura (Secure Sockets Layer) (SSL). La Figura 1-1 presenta el diagrama de la aplicación. A lo largo de este capítulo vamos a ir creando esta aplicación, usando en primer lugar archivos de configuración YAML y después diagramas de Helm.

Figura 1-1 Diagrama de la aplicación.

Gestión de archivos de configuración

Antes de exponer en detalle cómo crear esta aplicación en Kubernetes, vale la pena discutir cómo vamos a gestionar las propias configuraciones. Con Kubernetes, todo se representa de forma declarativa. Esto significa que escribimos los estados deseados de la aplicación en el clúster (generalmente en archivos YAML o JSON) y estos estados deseados que se han declarado definen todas las partes de la aplicación. Este enfoque declarativo es mucho más conveniente que un enfoque imperativo, en el que el estado del clúster es la suma de una serie de cambios en el mismo. Si un clúster está configurado de forma imperativa, es muy complicado replicar y entender cómo el clúster ha llegado a estar en ese estado. Esto hace que sea muy difícil comprender la aplicación o recuperarse de los problemas que esta pueda tener.

Cuando se declara el estado de la aplicación, los programadores suelen preferir YAML a JSON, aunque Kubernetes soporta ambos tipos de archivo. Esto se debe a que YAML es algo menos prolijo y más editable que JSON. Sin embargo, vale la pena señalar que YAML es sensible al sangrado. A menudo, los errores en las configuraciones de Kubernetes se deben a un sangrado incorrecto en YAML. Si algo no se comporta como se espera, es aconsejable comprobar el sangrado.

Debido a que el estado declarativo contenido en estos archivos YAML sirve como fuente de verdad para la aplicación, la gestión correcta de este estado es fundamental para lograr nuestros objetivos. Cuando modifiquemos la aplicación para llevarla al estado deseado, nos interesará poder gestionar los cambios, validar que sean correctos, auditar quién realizó esos cambios y, posiblemente, si las cosas fallan poder volver al punto de partida. Afortunadamente, en el contexto de la ingeniería de software, ya hemos desarrollado las herramientas necesarias para gestionar tanto los cambios en el estado declarativo como la auditoría y el proceso de reversión. Es decir, las mejores prácticas se aplican directamente a la tarea de administrar el estado declarativo de la aplicación, en relación tanto con el control de versiones como con la revisión de código.

En la actualidad, la mayoría de los desarrolladores almacenan sus configuraciones de Kubernetes en Git. Aunque los detalles específicos del sistema de control de versiones no son importantes, en el ecosistema de Kubernetes muchas herramientas esperan archivos en un repositorio Git. Para la revisión de código hay mucha más heterogeneidad; aunque claramente GitHub es bastante popular, otros usan herramientas o servicios locales de revisión de código. Independientemente de cómo implementemos la revisión del código para la configuración de la aplicación, debemos tratarla con la misma diligencia y atención que aplicamos al control del código fuente.

Cuando se trata de diseñar el sistema de archivos de la aplicación para organizar los componentes, generalmente vale la pena usar la organización de carpetas que viene con el sistema de archivos. Por lo general, se utiliza un único directorio para incluir Application Service (servicio de la aplicación), cualquiera que sea la definición de Application Service que sea útil para el equipo de trabajo. Dentro de ese directorio, los subdirectorios se utilizan para los subcomponentes de la aplicación.

Para nuestra aplicación, presentamos los archivos de la siguiente manera:

journal/ frontend/ redis/ fileserver/

Dentro de cada directorio se encuentran los archivos YAML específicos que se necesitan para definir el servicio. Como veremos más adelante, a medida que vayamos desplegando nuestra aplicación en varias regiones o clústeres diferentes, la disposición de archivos se irá complicando.

Creación de un servicio replicado mediante Deployments

Para describir nuestra aplicación, comenzaremos por el frontend y trabajaremos hacia abajo. La aplicación frontend para la publicación es una aplicación Node.js implementada en TypeScript. La aplicación completa ocupa demasiado espacio para incluirla en el libro. Presenta un servicio HTTP en el puerto 8080 que atiende peticiones de la ruta / api / * y usa el backend de Redis para añadir, eliminar o devolver las entradas habituales de la publicación. Esta aplicación se puede compilar en una imagen de contenedor utilizando el Dockerfile incluido y se puede enviar a nuestro repositorio de imágenes. Después, sustituimos el nombre de esta imagen en los ejemplos de YAML que vendrán a continuación.

Mejores prácticas para la gestión de imágenes

Aunque, en general, la creación y el mantenimiento de imágenes de contenedores van más allá del alcance de este libro, vale la pena identificar algunas de las mejores prácticas para crear y dar nombre a las imágenes. El proceso de creación de imágenes puede ser vulnerable a «ataques en la cadena de suministro». En tales ataques, un usuario malintencionado inyecta código o dígitos binarios en alguna dependencia desde una fuente confiable que luego se incorpora a la aplicación. Debido al riesgo de tales ataques, es fundamental que cuando procedamos a crear las imágenes confiemos solo en proveedores de imágenes conocidos y confiables. Opcionalmente, podemos crear todas las imágenes desde cero. La creación a partir de cero es fácil para algunos lenguajes (por ejemplo, Go) que pueden crear binarios estáticos, pero es considerablemente más complicada para lenguajes interpretados como Python, JavaScript o Ruby.

Otras buenas prácticas para las imágenes se relacionan con los nombres. Aunque la versión de una imagen de contenedor en un archivo de imágenes es teóricamente mutable, debemos tratar la etiqueta de la versión como inmutable. En particular, alguna combinación de la versión semántica y el hash SHA del commit (confirmación) donde se ha creado la imagen es una buena práctica para nombrar imágenes (por ejemplo, v1.0.1-bfeda01f). Si no especificamos una versión de imagen, se usa la última por defecto. Aunque esto puede ser conveniente en la fase de desarrollo, no es una buena idea para su uso en la fase de producción porque la última cambia cada vez que se crea una nueva imagen.

Creación de una aplicación replicada

Nuestra aplicación de frontend es apátrida. Para su estado depende totalmente del backend de Redis. Como consecuencia, podemos replicarla arbitrariamente sin afectar al tráfico. Aunque es poco probable que la aplicación soporte un uso a gran escala, es una buena idea que se ejecute en al menos dos réplicas para poder resolver una caída inesperada o poner en marcha una nueva versión de la aplicación sin paradas.

Aunque en Kubernetes ReplicaSet es el recurso que gestiona la replicación de una aplicación contenida en un contenedor, no es una buena práctica utilizarlo directamente. En su lugar, se utiliza el recurso Deployment (implementación). Deployment combina las capacidades de replicación de ReplicaSet con el versionado y la capacidad de realizar un despliegue por etapas. Mediante el uso de Deployment podemos utilizar las herramientas incorporadas en Kubernetes para pasar de una versión de la aplicación a la siguiente.

El recurso Deployment para nuestra aplicación tiene el siguiente aspecto:

apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: frontend name: frontend namespace: default spec: replicas: 2 selector: matchLabels: app: frontend template: metadata: labels: app: frontend spec: containers: - image: my-repo/journal-server:v1-abcde imagePullPolicy: IfNotPresent name: frontend resources: request: cpu: "1.0" memory: "1G" limits: cpu: "1.0" memory: "1G"

Hay varias cosas a tener en cuenta en este Deployment. La primera es que utilizamos Labels (etiquetas) para identificar el Deployment, así como los ReplicaSets y las pods (cápsulas) que crea Deployment. Hemos añadido la etiqueta layer: frontend a todos estos recursos para que podamos examinar todos los recursos de una capa en particular en una sola petición. Esto lo veremos a medida que añadamos otros recursos, donde seguiremos el mismo procedimiento.

Además, hemos añadido comentarios en varias partes de YAML, aunque estos comentarios no se convierten en un recurso de Kubernetes almacenado en el servidor. Como ocurre con los comentarios en el código, sirven de ayuda para orientar a los desarrolladores que analizan la configuración por primera vez.

También debemos tener en cuenta que para los contenedores en Deployment hemos especificado tanto las peticiones de recursos de Request (solicitud) como las de Limit (límite), y hemos establecido que Request es igual a Limit. Cuando se ejecuta una aplicación, Request es la reserva de recursos que se garantiza en la máquina host (anfitriona) en la que se ejecuta. Limit indica la cantidad máxima de recursos que se le permitirá usar al contenedor. Cuando empezamos, al establecer Request igual a Limit (solicitud igual a límite) se consigue el comportamiento más previsible de la aplicación. Esta previsibilidad se produce a expensas de la utilización de recursos. Dado que la configuración en la que Request es igual a Limit evita que las aplicaciones se sobreprogramen o consuman recursos inútiles, no podremos impulsar la utilización óptima a menos que ajustemos Request y Limit muy cuidadosamente. A medida que avancemos en la comprensión del modelo de recursos de Kubernetes, podremos considerar modificar Request y Limit en la aplicación de forma independiente. Pero, en general, la mayor parte de los usuarios cree que merece la pena la estabilidad de la previsibilidad frente a lo que se reduce su utilización.

Ahora que tenemos definido el recurso Deployment, lo comprobaremos en el control de versiones y lo desplegaremos en Kubernetes:

git add frontend/deployment.yaml git commit -m "Added deployment" frontend/deployment.yaml kubectl apply -f frontend/deployment.yaml

También es una buena práctica comprobar que el contenido del clúster coincide exactamente con el contenido del control del código fuente. La mejor forma de comprobarlo es adoptar una aproximación GitOps y hacer el despliegue en producción solo desde una rama específica del control del código fuente, utilizando la automatización de Continuous Integration (integración continua) (CI)/Continuous Delivery (entrega continua) (CD). De esta manera, se garantiza que los contenidos del control del código fuente y producción coinciden. Aunque una pipeline (canalización) CI/CD completa puede parecer excesiva para una aplicación sencilla, la automatización en sí misma —independientemente de la fiabilidad que proporciona— normalmente merece la pena, a pesar del tiempo que conlleva montarla. Y CI/CD es extremadamente difícil de reequipar en una aplicación existente e implementada con un enfoque imperativo.

Hay algunas partes de esta descripción de la aplicación YAML (por ejemplo, ConfigMap y los volúmenes secret), así como la Quality of Service (calidad de servicio) de las cápsulas, que examinamos en secciones posteriores.

Configuración de Ingress externa para tráfico HTTP

Los contenedores de nuestra aplicación ya están implementados, pero en este momento no es posible acceder a la aplicación. Por defecto, los recursos del clúster solo están disponibles dentro del mismo clúster. Para presentar nuestra aplicación al mundo, necesitamos crear un Service (servicio) y un balanceador de carga para proporcionar una dirección IP externa y traer tráfico a nuestros contenedores. Para la presentación externa vamos a usar dos recursos de Kubernetes. El primero es un Service que equilibra la carga de tráfico de Transmission Control Protocol (protocolo de control de transmisión) (TCP) o de User Datagram Protocol (protocolo de datagrama de usuario) (UDP). En nuestro caso, usamos el protocolo TCP. Y el segundo es un recurso Ingress (acceso), que proporciona balanceo de carga HTTP(S) con enrutamiento inteligente de peticiones basado en rutas y hosts HTTP. Con una aplicación tan sencilla como esta, te preguntarás por qué elegimos usar la Ingress más compleja. Pero, como verás en secciones posteriores, incluso esta sencilla aplicación servirá peticiones HTTP desde dos servicios diferentes. Además, al disponer de Ingress tenemos la ventaja de contar con cierta flexibilidad en caso de una futura expansión de nuestro servicio.

Antes de que se pueda definir el recurso Ingress, es necesario que exista un Service de Kubernetes al que Ingress apunte. Usaremos Labels (etiquetas) para dirigir el Service a las cápsulas que hemos creado en la sección anterior. Service es considerablemente más sencillo de definir que Deployment, y se ve de la siguiente manera:

apiVersion: v1 kind: Service metadata: labels: app: frontend name: frontend namespace: default spec: ports: - port: 8080 protocol: TCP targetPort: 8080 selector: app: frontend type: ClusterIP

Después de haber definido Service, podemos definir el recurso Ingress. A diferencia de los recursos de Service, Ingress necesita que un contenedor del controlador Ingress se ejecute en el clúster. Hay una serie de aplicaciones diferentes entre las que podemos elegir, ya sea las que proporciona el proveedor de la nube o las que se implementan utilizando servidores de código abierto. Si decidimos instalar un proveedor de Ingress de código abierto, es una buena idea utilizar el administrador de paquetes Helm (https://helm.sh) para su instalación y mantenimiento. Los proveedores de Ingress nginx o haproxy son las opciones más habituales:

apiVersion: extensions/v1beta1 kind: Ingress metadata: name: frontend-ingress spec: rules: - http: paths: - path: /api backend: serviceName: frontend servicePort: 8080

Configuración de la aplicación con ConfigMaps

Cada aplicación necesita un cierto grado de configuración. Esta configuración puede estar relacionada con el número de entradas de la publicación para mostrarlas por páginas, el color de un fondo en particular, una presentación especial para un día festivo o muchos otros tipos de configuraciones. Normalmente, una buena práctica a seguir es la de separar la información de la configuración de la propia aplicación.

Hay un par de razones para esta separación. La primera es que podríamos desear configurar el mismo código binario de la aplicación con diferentes configuraciones en función del escenario. Es posible, por ejemplo, que en Europa deseemos iluminar un especial de Pascua mientras que en China seguramente querremos mostrar un especial para el Año Nuevo Chino. Además de esta especialización en cuanto al entorno, hay razones de agilidad para realizar la separación. Por lo general, la versión en código binario contiene múltiples características nuevas y diferentes. Si activamos estas características mediante código, la única manera de modificar las características activas es crear y lanzar un nuevo binario, que puede ser un proceso costoso y lento.

El uso de la configuración para activar un conjunto de características significa que podemos, de forma inmediata (incluso de forma dinámica), activar y desactivar características en respuesta a las necesidades del usuario o a fallos en el código de la aplicación. Dependiendo de cada característica, se puede desplegar o replegar. Esta flexibilidad asegura que podamos progresar continuamente con la mayor parte de las características, incluso en el caso de que algunas necesiten replegarse para mejorar el rendimiento o corregir problemas.

En Kubernetes este tipo de configuración está representado por un recurso llamado ConfigMap. ConfigMap contiene pares clave/valor que representan la información de configuración o un archivo. Esta información de configuración se puede presentar a un contenedor en una cápsula, bien a través de archivos o bien mediante variables de entorno. Imaginemos que queremos configurar la aplicación de la publicación en línea para visualizar un número configurable de entradas por página. Para lograrlo, podemos definir ConfigMap como se indica a continuación:

kubectl create configmap frontend-config --from-literal=journalEntries=10

Para configurar la aplicación, presentamos la información de configuración como una variable de entorno en la propia aplicación. Para hacer esto, podemos añadir lo siguiente al recurso container en el Deployment que definimos antes:

... # The containers array in the PodTemplate inside the Deployment containers: - name: frontend ... env: - name: JOURNAL_ENTRIES valueFrom: configMapKeyRef: name: frontend-config key: journalEntries ...

Aunque esto muestra cómo podemos usar ConfigMap para configurar la aplicación, en el mundo real de Deployments puede que queramos poner en marcha cambios periódicos en la configuración con actualizaciones semanales o incluso con mayor frecuencia. Podría ser tentador hacer esto simplemente cambiando el propio ConfigMap, pero no sería una buena práctica. Hay varias razones para ello. La primera es que cambiar la configuración no desencadena una actualización de las cápsulas existentes; solo se aplica la configuración cuando la cápsula se vuelve a arrancar. Por este motivo, la actualización no se realiza correctamente y puede ser ad hoc o aleatoria.

Un enfoque más adecuado es poner un número de versión en el nombre del propio ConfigMap. En lugar de llamarlo frontend-config, lo llamamos frontendconfig-v1. Cuando queramos hacer un cambio, en lugar de actualizar el ConfigMap en uso, creamos una nueva v2 de ConfigMap y, a continuación, actualizamos el recurso Deployment de nuevo para utilizar esa configuración. Al hacerlo, se desencadena automáticamente la puesta en marcha del Deployment, utilizando las adecuadas health checking (pruebas de funcionamiento correcto) y las pausas entre cambios. Además, si alguna vez necesitamos hacer rollback (repliegue), la configuración v1 está disponible en el clúster y el rollback es tan simple como actualizar Deployment de nuevo.

Gestión de autenticación con Secrets

Hasta ahora no hemos entrado en detalle en el servicio Redis al que se conecta nuestro frontend. Pero en cualquier aplicación real necesitamos que las conexiones entre nuestros servicios sean seguras. En parte, se trata de garantizar la seguridad de la comunicación de los usuarios y de sus datos. Además, es esencial evitar errores como, por ejemplo, conectar un frontend de desarrollo con una base de datos de producción.

La autenticación en la base de datos de Redis se realiza mediante una simple contraseña. Parecería conveniente pensar en almacenar esta contraseña en el código fuente de la aplicación, o en un archivo en la imagen, pero ninguna de estas opciones es buena idea por múltiples razones. La primera es que hemos filtrado nuestro secreto (la contraseña) en un entorno en el que no estamos necesariamente pensando en el control de acceso. Si ponemos una contraseña en el control del código fuente, estamos alineando el acceso al código fuente con el acceso a todos los datos secretos. Esto seguramente no sea correcto. Es probable que tengamos un conjunto más amplio de usuarios que puedan acceder a nuestro código fuente del que realmente debería tener acceso a nuestra instancia de Redis. De la misma manera, alguien que tiene acceso a la imagen de nuestro contenedor no necesariamente debe tener acceso a nuestra base de datos de producción.

Además de las preocupaciones sobre el control de acceso, otra razón para evitar la vinculación de los datos secretos al control del código fuente y/o a las imágenes es la parametrización. Deseamos poder utilizar el mismo código fuente e imágenes en una gran variedad de entornos (por ejemplo, desarrollo, canario y producción). Si los datos secretos están estrechamente ligados con el código fuente o con la imagen, se necesita un archivo imagen diferente (o un código diferente) para cada entorno.

Como acabamos de ver ConfigMaps en la sección anterior, el primer pensamiento podría ser el de almacenar la contraseña como si se tratara de una configuración y, a continuación, introducirla en la aplicación como una configuración específica de la misma. Estamos en lo cierto al creer que la separación de la configuración de la aplicación es la misma que la separación de los datos secretos de la aplicación. Pero la verdad es que los datos secretos son un concepto importante en sí mismo. Es probable que deseemos gestionar el control de acceso, la gestión y las actualizaciones de los datos secretos de una manera diferente a la de una configuración. Y lo que es más importante aún, queremos que nuestros desarrolladores piensen de forma diferente cuando accedan a los datos secretos y cuando accedan a la configuración. Por estas razones Kubernetes tiene incorporado el recurso Secret (secreto) para gestionar datos secretos.

Podemos crear una contraseña secreta para nuestra base de datos Redis de la siguiente manera:

kubectl create secret generic redis-passwd --from-literal=passwd=${RANDOM}

Obviamente, es posible que deseemos utilizar algo más que un número aleatorio para la contraseña. Además, es probable que tengamos interés en utilizar un servicio de gestión de secret/key (secreto/clave), ya sea a través del proveedor de cloud computing (computación en la nube) —como Microsoft Azure Key Vault— o mediante un proyecto de código abierto —como HashiCorp’s Vault—. Cuando se utilizan servicios de gestión de claves, estos generalmente tienen una integración más estrecha con los datos secretos de Kubernetes.

Por defecto, los datos secretos en Kubernetes se almacenan sin cifrar. Si deseamos almacenar datos secretos cifrados, podemos hacerlo a través de un proveedor de claves, que nos proporcione una clave que Kubernetes utilizará para cifrar todos los datos secretos en el clúster. Hay que tener en cuenta que, aunque esta acción protege las claves contra ataques directos a la base de datos etcd, necesitamos también tener la seguridad de que el acceso a través del servidor de la API de Kubernetes está debidamente protegido.

Después de haber almacenado la contraseña de Redis como un dato secreto en Kubernetes, necesitamos enlazar ese dato secreto a la aplicación en ejecución cuando se implemente en Kubernetes. Para hacer esto, podemos usar un Volume (volumen) de Kubernetes. Un Volume es un archivo o directorio que puede montarse en un contenedor en ejecución en un lugar especificado por el usuario. En el caso de datos secretos, Volume se crea como un sistema de archivos con respaldo de RAM tmpfs y, luego, se monta en el contenedor. Esto asegura que, incluso si la máquina está físicamente comprometida (bastante improbable en la nube, pero posible en el centro de datos), sea mucho más difícil que el atacante consiga los datos secretos.

Para añadir un volumen de datos secretos a Deployment, necesitamos especificar dos nuevas entradas en el YAML de Deployment. La primera es la entrada volume para la cápsula, que añade el volumen a la cápsula:

... volumes: - name: passwd-volume secret: secretName: redis-passwd

Con el volumen en la cápsula, es necesario montarlo en un contenedor específico. Lo hacemos mediante el campo volumeMounts en la descripción del contenedor:

... volumeMounts: - name: passwd-volume readOnly: true mountPath: "/etc/redis-passwd" ...

Esto incorporará el volumen de datos secretos al directorio redis-passwd para el acceso con el código de cliente. Poniendo todo esto junto, tenemos el Deployment completo de la siguiente manera:

apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: frontend name: frontend namespace: default spec: replicas: 2 selector: matchLabels: app: frontend template: metadata: labels: app: frontend spec: containers: - image: my-repo/journal-server:v1-abcde imagePullPolicy: IfNotPresent name: frontend volumeMounts: - name: passwd-volume readOnly: true mountPath: "/etc/redis-passwd" resources: request: cpu: "1.0" memory: "1G" limits: cpu: "1.0" memory: "1G" volumes: - name: passwd-volume secret: secretName: redis-passwd

En este momento hemos configurado la aplicación de cliente, con lo que tenemos disponibles los datos secretos para la autenticación en el servicio de Redis. La configuración de Redis para usar esta contraseña es similar; la montamos en la cápsula de Redis y cargamos la contraseña desde el archivo.

Despliegue de una sencilla base de datos con estado

Aunque conceptualmente el despliegue de una aplicación stateful (con estado) es similar al despliegue de un cliente como nuestro frontend, el estado trae consigo más complicaciones. La primera es que en Kubernetes podemos necesitar reprogramar una cápsula por una serie de razones, como pueden ser la comprobación de los nodos, una actualización o un rebalanceo. Cuando esto sucede, la cápsula podría trasladarse a una máquina diferente. Si los datos asociados con la instancia de Redis están localizados en una máquina en particular o dentro del propio contenedor, estos datos se pierden cuando el contenedor migra o se reinicia. Para evitar esto, al ejecutar tareas de estado en Kubernetes es importante usar PersistentVolumes remotos para administrar el estado asociado con la aplicación.

Hay una gran variedad de aplicaciones de PersistentVolumes en Kubernetes, y todas comparten características comunes. Como en los volúmenes de datos secretos descritos anteriormente, se asocian a una cápsula y se montan en un contenedor en un lugar determinado. A diferencia de los datos secretos, PersistentVolumes suelen estar montados en almacenamiento remoto a través de algún tipo de protocolo de red, ya sea basado en archivos —como Network File System (sistema de archivos de red) (NFS) o Server Message Block (bloque de mensajes del servidor) (SMB)— o basado en bloques —iSCSI, discos basados en la nube, etc.—.

Generalmente, para aplicaciones como bases de datos son preferibles los discos basados en bloques porque normalmente ofrecen un mejor rendimiento. Pero si el rendimiento no tiene mucha importancia, a veces los discos basados en archivos pueden ofrecer una mayor flexibilidad.

La gestión del estado, en general, es complicada, y Kubernetes no es una excepción. Si ejecutamos la aplicación en un entorno que soporta servicios con estado (por ejemplo, MySQL como servicio, Redis como servicio), generalmente es una buena idea usar esos servicios con estado. Inicialmente, el coste suplementario de un software como servicio (SaaS) con estado puede parecer caro. Pero cuando se tienen en cuenta todos los requisitos operativos de estado (copia de seguridad, localización de datos, redundancia, etc.) y el hecho de que la presencia de estado en un clúster de Kubernetes dificulta mover la aplicación entre clústeres, queda claro que en la mayoría de los casos vale la pena el precio adicional de las aplicaciones SaaS de almacenamiento. En entornos locales en los que no se dispone de SaaS de almacenamiento y que cuentan con un equipo de personas dedicado a proporcionar almacenamiento como servicio a toda la organización es, definitivamente, mejor práctica que permitir que cada equipo de trabajo haga lo suyo.

Para desplegar nuestro servicio Redis, utilizamos el recurso StatefulSet. Añadido después del lanzamiento inicial de Kubernetes como complemento a los recursos de ReplicaSet, StatefulSet ofrece unas garantías un poco más sólidas, como nombres consistentes (¡sin hashes aleatorios!) y un orden definido para la ampliación y la reducción de escala. Cuando implementamos una instancia única, esto es menos importante, pero cuando deseamos desplegar un estado replicado, estos atributos son muy convenientes.

Para obtener un PersistentVolume para nuestro Redis, utilizamos PersistentVolumeClaim. Podemos pensar que se trata de una demanda de «solicitud de recursos». Nuestro Redis declara en abstracto que quiere 50 GB de almacenamiento, y es el clúster de Kubernetes el que determina cómo aprovisionar el PersistentVolume apropiado. Hay dos razones para ello. La primera es que podemos escribir un StatefulSet que sea portátil entre diferentes nubes e instalaciones, donde los detalles de los discos pueden ser diferentes. La otra razón es que, aunque se pueden montar muchos tipos de PersistentVolume en una sola cápsula, podemos usar la demanda de volumen para escribir una plantilla que se pueda replicar y, sin embargo, tener cada cápsula con su propio PersistentVolume específico asignado.

El siguiente ejemplo muestra un Redis StatefulSet con PersistentVolumes:

apiVersion: apps/v1 kind: StatefulSet metadata: name: redis spec: serviceName: "redis" replicas: 1 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:5-alpine ports: - containerPort: 6379 name: redis volumeMounts: - name: data mountPath: /data volumeClaimTemplates: - metadata: name: data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 10Gi

Esto implementa una única instancia de nuestro servicio Redis. Pero supongamos que queremos replicar el clúster de Redis para ampliar las lecturas y la resistencia a fallos. Para ello, es necesario aumentar el número de réplicas a tres, pero también necesitamos asegurar que las dos nuevas réplicas se conectan al master (maestro) de Redis para poder escribir en él.

Cuando creamos Service sin encabezamiento para StatefulSet de Redis, se crea una entrada DNS redis-0.redis; esta es la dirección IP de la primera réplica. Podemos utilizarla para crear un sencillo script que se puede lanzar en todos los contenedores:

#!/bin/bash PASSWORD=$(cat /etc/redis-passwd/passwd) if [[ "${HOSTNAME}" == "redis-0" ]]; then redis-server --requirepass ${PASSWORD} else redis-server --slaveof redis-0.redis 6379 --masterauth ${PASSWORD} -- requirepass ${PASSWORD} fi

Podemos crear este script como ConfigMap:

kubectl create configmap redis-config --from-file=launch.sh=launch.sh

A continuación, añadimos este ConfigMap a StatefulSet y lo utilizamos como comando para el contenedor. También agregamos la contraseña para la autenticación que hemos creado anteriormente en este capítulo.

El Redis completo de tres réplicas se ve de la siguiente manera:

apiVersion: apps/v1 kind: StatefulSet metadata: name: redis spec: serviceName: "redis" replicas: 3 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:5-alpine ports: - containerPort: 6379 name: redis volumeMounts: - name: data mountPath: /data - name: script mountPath: /script/launch.sh subPath: launch.sh - name: passwd-volume mountPath: /etc/redis-passwd command: - sh - -c - /script/launch.sh volumes: - name: script configMap: name: redis-config defaultMode: 0777 - name: passwd-volume secret: secretName: redis-passwd volumeClaimTemplates: - metadata: name: data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 10Gi

Creación de un equilibrador de carga TCP con Services

Ahora que hemos implementado el servicio con estado de Redis, tenemos que ponerlo a disposición de nuestro frontend. Para ello, creamos dos Services de Kubernetes diferentes. El primero es el Service de lectura de datos de Redis. Debido a que Redis replica los datos a los tres miembros de StatefulSet, no nos importa a quién va dirigida nuestra solicitud. En consecuencia, utilizamos un Service básico para las lecturas:

apiVersion: v1 kind: Service metadata: labels: app: redis name: redis namespace: default spec: ports: - port: 6379 protocol: TCP targetPort: 6379 selector: app: redis sessionAffinity: None type: ClusterIP

Para habilitar la escritura, necesitamos apuntar al master de Redis (replica #0). Para ello, creamos un Service sin encabezamiento. Un Service sin encabezamiento no tiene una dirección IP del clúster, sino que programa una entrada DNS para cada cápsula en el StatefulSet.

Esto significa que podemos acceder al master a través del nombre DNS redis-0.redis:

apiVersion: v1 kind: Service metadata: labels: app: redis-write name: redis-write spec: clusterIP: None ports: - port: 6379 selector: app: redis

Por lo tanto, cuando queramos conectarnos a Redis por escrito o mediante pares de lectura/escritura transaccionales, podemos crear un cliente de escritura separado y conectado al servidor redis-0.redis.

Uso de Ingress para enrutar el tráfico a un servidor de archivos estáticos

El componente final de nuestra aplicación es un servidor de archivos estáticos. El servidor de archivos estáticos es responsable de servir archivos HTML, CSS, JavaScript y archivos de imágenes. Es muy eficaz y, a la vez, está enfocado a permitir que podamos separar el servicio de archivos estáticos de nuestro frontend, descrito anteriormente, que atiende las peticiones API. Podemos utilizar cómodamente un servidor de archivos estáticos de alto rendimiento como NGINX para servir archivos, lo cual permite al mismo tiempo que nuestros equipos de desarrollo se concentren en el código con el que implementar nuestra API.

Afortunadamente, el recurso Ingress hace que este principio de arquitectura de mini-microservicio sea muy fácil. Al igual que en el frontend, podemos usar el recurso Deployment para describir un servidor NGINX replicado. Vamos a crear las imágenes estáticas en el contenedor de NGINX y las desplegaremos en cada réplica. El recurso Desployment tiene el siguiente aspecto:

apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: fileserver name: fileserver namespace: default spec: replicas: 2 selector: matchLabels: app: fileserver template: metadata: labels: app: fileserver spec: containers: - image: my-repo/static-files:v1-abcde imagePullPolicy: Always name: fileserver terminationMessagePath: /dev/termination-log terminationMessagePolicy: File resources: request: cpu: "1.0" memory: "1G" limits: cpu: "1.0" memory: "1G" dnsPolicy: ClusterFirst restartPolicy: Always

Ahora que hay un servidor web estático replicado funcionando, también crearemos un recurso Service para que actúe como equilibrador de carga:

apiVersion: v1 kind: Service metadata: labels: app: frontend name: frontend namespace: default spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: app: frontend sessionAffinity: None type: ClusterIP

Ahora que tenemos un Service para el servidor de archivos estáticos, extendemos el recurso Ingress para que contenga la nueva ruta. Es importante tener en cuenta que debemos colocar la ruta / después de la ruta /api; de lo contrario, subsumiría /api y dirigiría las peticiones de la API al servidor de archivos estáticos. El nuevo Ingress tiene el aspecto siguiente:

apiVersion: extensions/v1beta1 kind: Ingress metadata: name: frontend-ingress spec: rules: - http: paths: - path: /api backend: serviceName: frontend servicePort: 8080 - path: / backend: serviceName: nginx servicePort: 80

Parametrización de la aplicación utilizando Helm

Todo lo que hemos discutido hasta ahora se centra en el despliegue de una sola instancia de nuestro servicio en un solo clúster. Sin embargo, en realidad, casi todos los servicios y casi todos los servicios de los equipos de trabajo van a necesitar desplegarse en varios entornos diferentes (aunque compartan un clúster). Incluso si se trata de un único desarrollador que trabaja en una sola aplicación, es probable que desee tener al menos una versión de desarrollo y una versión de producción de la aplicación —para poder hacer iteraciones y desarrollos sin interrumpir a los usuarios en producción—. Después de tener en cuenta las pruebas de integración y CI/CD, es probable que incluso con un solo servicio y un puñado de desarrolladores deseemos desplegar hasta al menos tres entornos diferentes, y posiblemente más si consideramos gestionar los fallos a nivel de centros de datos.

Un tipo de fallo habitual, al principio, en muchos equipos de trabajo se produce al copiar los archivos de un clúster a otro. En lugar de tener un solo directorio /frontend, tenemos un par de directorios frontend-production/ y frontend-development/. La razón por la que esto es tan peligroso es porque ahora tenemos que asegurarnos de que estos archivos permanezcan sincronizados entre ellos. Si estuvieran destinados a ser totalmente idénticos sería fácil, pero se espera que haya alguna diferencia entre el desarrollo y la producción porque desarrollamos nuevas características. Es fundamental que la diferencia sea premeditada y fácil de gestionar.

Otra opción para lograr esto sería usar ramas y control de versiones, donde las ramas de producción y desarrollo parten de un repositorio central, y las diferencias entre las ramas son claramente visibles. Esta puede ser una opción viable para algunos equipos de trabajo, pero la mecánica de moverse entre ramas se convierte en un reto cuando deseamos desplegar software simultáneamente en diferentes entornos (por ejemplo, un CI/CD que se implementa en varias regiones diferentes de la nube).

En consecuencia, la mayoría de los desarrolladores terminan con un sistema de plantillas. Un sistema de plantillas combina plantillas, que forman la columna vertebral centralizada de la configuración de la aplicación, con parámetros que especializan la plantilla para una configuración de entorno específica. De esta manera, podemos tener una configuración compartida en general con una deliberada personalización (y fácil de entender) cuando sea necesario. Hay una amplia variedad de sistemas de plantillas para Kubernetes, pero el más popular con diferencia es un sistema llamado Helm (https://helm.sh).

En Helm, una aplicación es un paquete formado por un conjunto de archivos llamado carta náutica (los chistes de náutica abundan en el mundo de los contenedores y de Kubernetes). La carta náutica empieza con un archivo chart.yaml, que define los metadatos de la propia carta:

apiVersion: v1 appVersion: "1.0" description: A Helm chart for our frontend journal server. name: frontend version: 0.1.0

Este archivo se coloca en la raíz del directorio de la carta náutica (por ejemplo, frontend/). Dentro de este directorio, hay un directorio de plantillas, en el que se colocan las plantillas. Una plantilla es básicamente un archivo YAML como los de los ejemplos anteriores, con algunos de los valores del archivo reemplazados con referencias a parámetros. Por ejemplo, imaginemos que queremos parametrizar el número de réplicas en el frontend. Anteriormente, esto es lo que tenía Deployment:

... spec: replicas: 2 ...

En el archivo de plantillas (frontend-deployment.tmpl), se ve de la siguiente forma:

... spec: replicas: {{ .replicaCount }} ...

Esto significa que cuando despleguemos la carta náutica, sustituiremos el valor por réplicas con el parámetro apropiado. Los propios parámetros están definidos en el archivo values.yaml. Habrá un archivo de valores por cada entorno en el que se debe implementar la aplicación. El archivo de valores para esta sencilla carta náutica se vería así:

replicaCount: 2

Juntando todo esto, podemos desplegar esta carta náutica usando la herramienta helm, como se muestra a continuación:

helm install path/to/chart --values path/to/environment/values.yaml

Esto parametriza la aplicación y la implementa en Kubernetes. Con el tiempo, estas parametrizaciones crecerán para incluir la variedad de diferentes entornos de la aplicación.

Mejores prácticas en el despliegue de servicios

Kubernetes es un sistema potente que puede parecer complicado. Pero poner en marcha una aplicación básica y tener éxito puede resultar fácil si utilizamos las siguientes prácticas:

• La mayoría de los servicios deberían desplegarse como recursos Deployments. Los Deployments crean réplicas idénticas en redundancia y escala.

• Los Deployments se pueden presentar utilizando un Service, que es un equilibrador de carga. Un Service se puede presentar dentro de un clúster (por defecto) o externamente. Si deseamos presentar una aplicación HTTP, podemos utilizar un controlador Ingress para agregar cosas como solicitar enrutamiento y SSL.

• Eventualmente, estaremos interesados en parametrizar la aplicación para hacer su configuración más reutilizable en diferentes entornos. Las herramientas de empaquetado como Helm son la mejor opción para este tipo de parametrización.

Resumen

La aplicación creada en este capítulo es sencilla, pero contiene casi todos los conceptos que necesitaremos para crear aplicaciones más grandes y complejas. Comprender cómo encajan las piezas y cómo utilizar los componentes más importantes de Kubernetes es clave para tener éxito cuando trabajamos con esta herramienta.

Sentar unas buenas bases en el control de versiones, la revisión de código y la ininterrumpida entrega del servicio nos asegurará que lo que creemos se creará de una manera sólida. A medida que progresemos sobre los temas más avanzados en los siguientes capítulos, habrá que tener en cuenta esta información fundamental.

CAPÍTULO 2
Flujos de trabajo para desarrolladores

Kubernetes se creó para operar software de manera confiable. Simplifica el despliegue y la gestión de software con una API orientada a la aplicación, con propiedades de autorregeneración y herramientas útiles como Deployments (implementaciones) para la puesta en marcha de software con tiempo de inactividad cero. Aunque todas estas herramientas tienen su utilidad, no contribuyen de forma importante a facilitar el desarrollo de aplicaciones en Kubernetes. Además, a pesar de que muchos clústeres están diseñados para ejecutar aplicaciones en el entorno de producción —y, por lo tanto, rara vez se accede a ellos mediante los flujos de trabajo de los desarrolladores—, también es fundamental permitir que los flujos de trabajo de desarrollo tengan como objetivo Kubernetes, y esto normalmente significa tener un clúster o al menos una parte de un clúster destinado a desarrollo. La configuración de este tipo de clústeres para facilitar el desarrollo de aplicaciones en Kubernetes es un aspecto necesario para asegurar el éxito. Si no hay ningún código que se cree para nuestro clúster, el clúster por sí mismo no va a conseguir mucho.

Objetivos

Antes de describir las mejores prácticas para la creación de clústeres de desarrollo, merece la pena establecer los objetivos para dichos clústeres. Obviamente, el objetivo final es hacer posible que los desarrolladores puedan crear aplicaciones en Kubernetes fácilmente y de forma rápida. Pero ¿qué significa eso en la práctica y cómo se refleja en las características prácticas del clúster de desarrollo?

Es útil identificar las fases de interacción del desarrollador con el clúster.

La primera fase es la de incorporación. Esto ocurre cuando un nuevo desarrollador se une al equipo de trabajo. Esta fase incluye facilitar al usuario el acceso al clúster, así como proporcionarle orientación en su primer despliegue. El objetivo para esta fase es conseguir que el desarrollador adquiera experiencia en un corto espacio de tiempo. Para este proceso deberíamos establecer el objetivo de Key Performance Indicator (indicador clave de desempeño) (KPI). Un objetivo razonable sería que el usuario pudiera pasar de la nada a poder dirigir la ejecución de la aplicación en curso en menos de media hora. Cada vez que alguien se incorpore al equipo de trabajo, comprobaremos cómo nos va con este objetivo.

La segunda fase es la de desarrollo. Esta es la actividad diaria del desarrollador. El objetivo de esta fase es asegurar una iteración y una depuración rápidas. Los desarrolladores necesitan pasar código al clúster rápidamente y de forma repetitiva. También deben ser capaces de probar su código y depurarlo cuando no funciona correctamente. El KPI para esta fase es más difícil de medir, pero se puede estimar midiendo el tiempo en obtener una pull request (petición de validación) (PR) o el tiempo empleado en el cambio y la ejecución en el clúster, o con encuestas sobre la productividad que percibe el usuario, o ambas. También podremos medirlo en la productividad global de los equipos de trabajo.

La tercera fase es de la de pruebas. Esta fase se intercala con la de desarrollo y se utiliza para validar el código antes de su envío y fusión. El objetivo de esta fase es doble. En primer lugar, el desarrollador debe ser capaz de realizar todas las pruebas de su entorno antes de presentar la PR. En segundo lugar, todas las pruebas deben ejecutarse automáticamente antes de que el código se fusione en el repositorio. Además de estos objetivos, también debemos establecer un KPI para la duración del tiempo que tardan las pruebas en realizarse. A medida que el proyecto se vuelve más complejo, es natural que un mayor número de pruebas necesite más tiempo. Cuando esto sucede, puede ser rentable identificar un conjunto más reducido de pruebas de humo que el desarrollador puede utilizar para la validación inicial antes de enviar la PR. También debemos tener un KPI muy estricto en cuanto a la debilidad de las pruebas. Una prueba problemática es una prueba que ocasionalmente (o no tan ocasionalmente) falla. En cualquier proyecto activo aceptable, un índice de debilidad de más de un fallo por cada mil ejecuciones hará que el desarrollador tenga problemas. Necesitamos asegurar que el entorno del clúster no conduce a pruebas problemáticas. A veces las pruebas problemáticas ocurren debido a problemas en el código, pero también pueden ocurrir debido a interferencias en el entorno de desarrollo (por ejemplo, quedarnos sin recursos o experimentar vecinos ruidosos). Debemos tener la seguridad de que nuestro entorno de desarrollo está libre de tales problemas midiendo la debilidad y actuando rápidamente para arreglarlo.

Creación de un clúster de desarrollo

Cuando alguien empieza a pensar en un desarrollo en Kubernetes, una de las primeras dudas que aparecen es si crear un único gran clúster de desarrollo o tener un clúster de desarrollo por desarrollador. Hay que tener en cuenta que esta opción solo tiene sentido en un entorno en el que la creación de clústeres dinámicos resulta fácil, como es la nube pública. En entornos físicos, es posible que la única opción sea un clúster grande.

Si tenemos opciones, debemos considerar los pros y los contras de cada opción. Si optamos por tener un clúster de desarrollo por usuario, el principal inconveniente de este enfoque es que será más costoso y menos eficiente, y tendremos que gestionar un gran número de clústeres de desarrollo diferentes. Los costes adicionales provienen del hecho de que es probable que cada clúster esté muy infrautilizado. Además, con los desarrolladores trabajando en diferentes clústeres, se hace más difícil rastrear y recolectar recursos que ya no están en uso.

La ventaja del enfoque del clúster por usuario es su simplicidad: cada desarrollador puede gestionar su propio clúster y, debido a su aislamiento, es mucho más difícil para los diferentes desarrolladores interferirse entre ellos.

Por otra parte, un único clúster de desarrollo será mucho más eficiente. Es probable que podamos mantener el mismo número de desarrolladores en un clúster compartido por la tercera parte del precio (o menos). Además, es mucho más fácil para nosotros instalar servicios en clústeres compartidos, por ejemplo la monitorización y el registro, lo que facilita considerablemente producir un clúster amigable con el desarrollador. La desventaja de un clúster de desarrollo compartido es el proceso de gestión de usuarios y la posible interferencia entre desarrolladores. Debido a que actualmente el proceso de añadir nuevos usuarios y namespaces (espacios de nombres) al clúster de Kubernetes no es ágil, deberemos activar un proceso para incorporar nuevos desarrolladores. Aunque la gestión de recursos de Kubernetes y el Role-Based Access Control (control de acceso basado en roles) (RBAC) pueden reducir la probabilidad de que dos desarrolladores entren en conflicto, siempre es posible que un usuario pueda bloquear el clúster de desarrollo al consumir demasiados recursos —de manera que otras aplicaciones y desarrolladores no los pueden planificar—. Además, deberemos asegurarnos de que los desarrolladores no pierdan ni olviden los recursos que han creado. Esto es algo más fácil, sin embargo, que el enfoque en el que los desarrolladores crean sus propios clústeres.

Ambos enfoques son factibles, pero nuestra recomendación es tener un único clúster grande para todos los desarrolladores. Aunque puedan existir problemas de interferencia entre ellos, estos se pueden gestionar y, en última instancia, la rentabilidad y la posibilidad de agregar fácilmente capacidades al clúster a nivel de toda la organización compensan los riesgos que supone la interferencia. Pero tendremos que invertir en los procesos de incorporación de nuevos desarrolladores, de gestión de recursos y de recolección de basura. Nuestra recomendación es intentarlo con un único clúster grande como primera opción. A medida que la organización crezca (o si ya es una organización grande), podríamos considerar tener un clúster por equipo de trabajo o grupo (de 10 a 20 personas), en lugar de un clúster gigante para cientos de usuarios. Esto puede simplificar tanto la facturación como la gestión.

Configuración de un clúster compartido por varios desarrolladores

Cuando se configura un clúster grande, el objetivo principal es asegurar que lo puedan utilizar varios usuarios simultáneamente sin que interfieran entre ellos. La manera obvia de separar a los diferentes desarrolladores es con los espacios de nombres de Kubernetes. Los espacios de nombres pueden servir como marco para el despliegue de servicios, de manera que el servicio de frontend de un usuario no interfiera con el servicio de frontend de otro usuario. Los espacios de nombres son también el marco para RBAC, con lo que se asegura que un desarrollador no pueda borrar accidentalmente el trabajo realizado por otro desarrollador. Por lo tanto, en un clúster compartido tiene sentido utilizar un espacio de nombres como el espacio de trabajo de cada desarrollador. Los procesos para la incorporación de usuarios y la creación y protección de un espacio de nombres se describen en las secciones siguientes.

Registro de usuarios

Antes de que podamos asignar un usuario a un espacio de nombres, debemos registrarlo en el propio clúster de Kubernetes. Para hacerlo hay dos opciones. Podemos utilizar la autenticación basada en certificados, crear un nuevo certificado para el usuario y proporcionarle un archivo kubeconfig que puede usar para iniciar sesión. O podemos configurar nuestro clúster para que utilice un sistema de identidad externo (por ejemplo, Microsoft Azure Active Directory o AWS Identity and Access Management [IAM]) para el acceso al clúster.

En general, el uso de un sistema de identidad externo es una buena práctica porque no requiere mantener dos fuentes de identidad diferentes, pero en algunos casos esto no es posible y necesitamos usar certificados. Afortunadamente, podemos usar la API de certificados de Kubernetes para la creación y gestión de dichos certificados. A continuación, veremos el proceso para añadir un nuevo usuario a un clúster existente. En primer lugar, necesitamos crear una solicitud de firma de certificado para generar un nuevo certificado. A continuación, veremos un sencillo programa en Go que permite realizarlo:

package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "os" ) func main() { name := os.Args[1] user := os.Args[2] key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { panic(err) } keyDer := x509.MarshalPKCS1PrivateKey(key) keyBlock := pem.Block{ Type: "RSA PRIVATE KEY", Bytes: keyDer, } keyFile, err := os.Create(name + "-key.pem") if err != nil { panic(err) } pem.Encode(keyFile, &keyBlock) keyFile.Close() commonName := user // You may want to update these too emailAddress := "someone@myco.com" org := "My Co, Inc." orgUnit := "Widget Farmers" city := "Seattle" state := "WA" country := "US" subject := pkix.Name{ CommonName: commonName, Country: []string{country}, Locality: []string{city}, Organization: []string{org}, OrganizationalUnit: []string{orgUnit}, Province: []string{state}, } asn1, err := asn1.Marshal(subject.ToRDNSequence()) if err != nil { panic(err) } csr := x509.CertificateRequest{ RawSubject: asn1, EmailAddresses: []string{emailAddress}, SignatureAlgorithm: x509.SHA256WithRSA, } bytes, err := x509.CreateCertificateRequest(rand.Reader, &csr, key) if err != nil { panic(err) } csrFile, err := os.Create(name + ".csr") if err != nil { panic(err) } pem.Encode(csrFile, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bytes}) csrFile.Close() }

Podemos ejecutarlo de la siguiente manera:

go run csr-gen.go client <user-name>

Con esto creamos los archivos llamados client-key.pem y client.csr. A continuación, podemos ejecutar el siguiente script para crear y descargar un nuevo certificado:

#!/bin/bash csr_name="my-client-csr" name="${1:-my-user}" csr="${2}" cat <<EOF | kubectl create -f - apiVersion: certificates.k8s.io/v1beta1 kind: CertificateSigningRequest metadata: name: ${csr_name} spec: groups: - system:authenticated request: $(cat ${csr} | base64 | tr -d '\n') usages: - digital signature - key encipherment - client auth EOF echo echo "Approving signing request." kubectl certificate approve ${csr_name} echo echo "Downloading certificate." kubectl get csr ${csr_name} -o jsonpath='{.status.certificate}' \ | base64 --decode > $(basename ${csr} .csr).crt echo echo "Cleaning up" kubectl delete csr ${csr_name} echo echo "Add the following to the 'users' list in your kubeconfig file:" echo "- name: ${name}" echo " user:" echo " client-certificate: ${PWD}/$(basename ${csr} .csr).crt" echo " client-key: ${PWD}/$(basename ${csr} .csr)-key.pem" echo echo "Next you may want to add a role-binding for this user."

Este script imprime la información final que podemos añadir al archivo kubeconfig para habilitar al usuario. Por supuesto, el usuario no tiene privilegios de acceso, por lo que tendremos que aplicar RBAC de Kubernetes al usuario con el fin de dotar de privilegios al espacio de nombres.

Creación y dotación de seguridad a un espacio de nombres

El primer paso para proporcionar un espacio de nombres es crearlo. Podemos hacerlo usando kubectl create namespace my-namespace.

Pero la verdad es que cuando creamos un espacio de nombres, necesitamos adjuntar un montón de metadatos, como por ejemplo la información de contacto del equipo de trabajo que crea el componente que se ha implementado en el espacio de nombres. Generalmente, esto se hace en forma de anotaciones. Podemos generar el archivo YAML usando alguna plantilla, como por ejemplo Jinja u otras, o bien podemos crear el espacio de nombres y, a continuación, hacer la anotación. Veamos un sencillo script para hacer esto:

ns='my-namespace' kubectl create namespace ${ns} kubectl annotate namespace ${ns} annotation_key= annotation_value

Cuando se crea el espacio de nombres, necesitamos dotarlo de seguridad para tener la garantía de que podemos conceder acceso a un usuario específico. Para ello, podemos vincular un rol a un usuario en el contexto de ese espacio de nombres. Esto lo conseguimos creando el objeto RoleBinding dentro del propio espacio de nombres. RoleBindig podría tener este aspecto:

apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: example namespace: my-namespace roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: edit subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: myuser

Para crearlo, lo único que tenemos que hacer es ejecutar kubectl create -f role-binding.yaml. Hay que tener en cuenta que podemos reutilizar este vínculo tanto como deseemos, siempre y cuando actualicemos el espacio de nombres en el vínculo para apuntar al espacio de nombres correcto. Si tenemos la seguridad de que el usuario no tiene ningún otro vínculo de rol, podemos estar seguros de que este espacio de nombres es la única parte del clúster a la que el usuario tiene acceso. Otra práctica razonable es conceder acceso a los lectores a todo el clúster; de esta manera, los desarrolladores pueden ver lo que otros están haciendo en caso de que interfieran con su trabajo. Sin embargo, debemos tener cuidado al conceder dicho acceso de lectura porque este incluirá el acceso a recursos secretos en el clúster. Generalmente, en un clúster de desarrollo esto está bien porque todo el mundo está en la misma organización y los datos secretos se utilizan solo en desarrollo. Sin embargo, si es motivo de preocupación, podemos crear un rol más detallado en el que se elimine la posibilidad de leer datos secretos.

Si deseamos limitar la cantidad de recursos que consume un espacio de nombres determinado, podemos utilizar el recurso ResourceQuota para fijar un límite al número total de recursos que consume ese espacio de nombres en particular. Por ejemplo, la siguiente cuota limita el espacio de nombres a 10 núcleos y 100 GB de memoria, para Request y Limit, para las cápsulas en ese espacio de nombres:

apiVersion: v1 kind: ResourceQuota metadata: name: limit-compute namespace: my-namespace spec: hard: requests.cpu: "10" requests.memory: 100Gi limits.cpu: "10" limits.memory: 100Gi

Administración de espacios de nombres

Ahora que hemos visto cómo incorporar a un nuevo usuario y cómo crear un espacio de nombres para utilizarlo como espacio de trabajo, la pregunta sigue siendo cómo asignar un desarrollador a un espacio de nombres. Como con muchas otras cosas, no hay una sola respuesta; en este caso hay dos enfoques. El primero es dar a cada usuario su propio espacio de nombres como parte del proceso de integración. Esto es útil porque después de que un usuario se haya registrado, siempre tiene un espacio de trabajo donde puede desarrollar y gestionar sus aplicaciones. Sin embargo, hacer que el espacio de nombres del desarrollador se extienda demasiado en el tiempo anima a este a dejar las cosas en el espacio de nombres después de haber terminado de usarlas, y la recogida de basura y la contabilidad de los recursos individuales se complican. Un enfoque alternativo consiste en crear y asignar temporalmente un espacio de nombres con un time to live (tiempo de vida) (TTL) limitado. Esta opción asegura que el desarrollador tenga en cuenta que los recursos en el clúster son transitorios y que es fácil automatizar la eliminación de espacios de nombres completos cuando su TTL expire.

En este modelo, cuando el desarrollador quiere iniciar un nuevo proyecto, utiliza una herramienta para asignar un nuevo espacio de nombres al proyecto. Cuando creamos el espacio de nombres, este tiene una selección de metadatos asociados a él para la gestión y la contabilidad. Obviamente, estos metadatos incluyen el TTL para el espacio de nombres, pero también incluyen al desarrollador al que está asignado, los recursos que deben asignarse al espacio de nombres (por ejemplo, CPU y memoria), el equipo de trabajo y el propósito del espacio de nombres. Estos metadatos garantizan que podamos realizar un seguimiento del uso de los recursos y eliminar el espacio de nombres en el momento adecuado.

El desarrollo de herramientas para asignar espacios de nombres a petición puede parecer un reto, pero las herramientas sencillas son relativamente fáciles de desarrollar. Por ejemplo, se puede lograr la asignación de un nuevo espacio de nombres con un sencillo script que crea el espacio de nombres y solicita los metadatos relevantes para adjuntarlos al mismo.

Si queremos tener una mayor integración con Kubernetes, podemos usar las custom resources definitions (definiciones de recursos personalizados) (CRD), que permiten a los usuarios crear y asignar dinámicamente nuevos espacios de nombres mediante la herramienta kubectl. Si tienes tiempo y ganas, esta es definitivamente una buena opción porque hace que la gestión de los espacios de nombres sea declarativa y también permite el uso de RBAC de Kubernetes.

Una vez disponemos de las herramientas para habilitar la asignación de espacios de nombres, necesitamos añadir algunas herramientas para reutilizarlos cuando su TTL haya expirado. Una vez más, podemos conseguirlo con un sencillo script que examina los espacios de nombres y borra aquellos que tienen una TTL caducada.

Podemos crear este script en un contenedor y usar ScheduledJob para ejecutarlo con una frecuencia de una hora. Combinadas entre sí, estas herramientas pueden garantizar que los desarrolladores puedan asignar fácilmente recursos independientes para su proyecto en la medida en que lo necesiten. Pero esos recursos también se obtendrán en el intervalo adecuado para tener la seguridad de que no estamos malgastando recursos y de que los recursos antiguos no se interponen en el camino del nuevo desarrollo.

Servicios a nivel de clúster

Además de las herramientas para asignar y gestionar espacios de nombres, también hay servicios útiles a nivel de clúster, y es una buena idea habilitarlos en nuestro clúster de desarrollo. El primero es la agregación de registros a un sistema central de Logging as a Service (administración de registros como servicio) (LaaS). Una de las cosas más fáciles de hacer para que los desarrolladores entiendan el funcionamiento de sus aplicaciones es escribir algo en STDOUT. Aunque podemos acceder a estos registros a través de kubectl logs, estos tienen una longitud limitada y no son localizables particularmente. Si, en cambio, enviamos automáticamente esos registros a un sistema LaaS —como puede ser un servicio en la nube o un clúster de búsqueda elástica—, los desarrolladores pueden buscar fácilmente información relevante en los registros, así como información de registros añadida a través de varios contenedores a su servicio.

Habilitación de flujos de trabajo para desarrolladores

Ahora que hemos tenido éxito en la configuración de un clúster compartido y que podemos registrar nuevos desarrolladores en el clúster, necesitamos conseguir que estos desarrollen sus aplicaciones. Recordemos que uno de los KPI clave que medimos es el tiempo que transcurre desde la incorporación hasta la aplicación inicial que se ejecuta en el clúster. Está claro que, a través de lo que acabamos de describir, podemos autenticar con rapidez a un usuario en un clúster y asignarle un espacio de nombres. Pero, ¿qué hay de empezar con la aplicación? Desafortunadamente, aunque hay algunas técnicas que ayudan en este proceso, por lo general poner en marcha la aplicación inicial requiere más de convención que de automatización. En las siguientes secciones, describimos un enfoque para lograrlo. De ninguna manera se trata de un único enfoque o una única solución. Opcionalmente, podemos aplicar el enfoque tal cual o inspirarnos en ideas para llegar a nuestra propia solución.

Instalación inicial

Uno de los principales retos para implementar una aplicación es la instalación de todas las dependencias. En muchos casos, especialmente en las arquitecturas modernas de microservicios, incluso para empezar la labor de desarrollo en uno de los microservicios se requiere el despliegue de múltiples dependencias, ya sean bases de datos u otros microservicios. Aunque el despliegue de la aplicación en sí es relativamente sencillo, la tarea de identificar e implementar todas las dependencias para crear la aplicación completa es a menudo una tarea frustrante de prueba y error combinada con instrucciones incompletas o desactualizadas.

Para abordar este tema, a menudo es útil introducir una convención para describir e instalar dependencias. Esto se puede ver como el equivalente de algo como npm install, que instala todas las dependencias JavaScript necesarias. Con el tiempo, es probable que exista una herramienta similar a npm que proporcione este servicio para los usuarios de Kubernetes, pero hasta entonces la mejor práctica es confiar en la convención dentro de nuestro equipo de trabajo.

Una de las opciones a adoptar como convención es la creación del script setup.sh dentro del directorio raíz de todos los repositorios del proyecto. Es responsabilidad de este script crear todas las dependencias dentro de un espacio de nombres en particular para asegurar que todas las dependencias de la aplicación se crean correctamente. Por ejemplo, un script de instalación puede parecerse a lo siguiente:

kubectl create my-service/database-stateful-set-yaml kubectl create my-service/middle-tier.yaml kubectl create my-service/configs.yaml

A continuación, podríamos integrar este script con npm añadiendo lo siguiente a nuestro package.json:

{ ... "scripts": { "setup": "./setup.sh", ... } }

Con esta configuración, un desarrollador nuevo tiene simplemente que ejecutar npm run setup y se instalarán las dependencias en el clúster. Obviamente, esta integración particular es específica de Node.js/npm. Con otros lenguajes de programación, tendrá más sentido integrar las herramientas específicas del correspondiente lenguaje. Por ejemplo, en Java podemos integrar en su lugar el archivo Maven pom.xml.

Preparación de la fase de desarrollo activo

Una vez configurado el espacio de trabajo del desarrollador con las dependencias necesarias, la siguiente tarea es permitirle que pueda iterar de forma inmediata su aplicación. La primera condición previa para ello es que exista la posibilidad de crear y transmitir una imagen del contenedor. Supongamos que ya la tenemos configurada. Si no es así, puedes leer cómo hacerlo en otros libros y recursos en línea.

Después de que hayamos hecho built (crear) y push (subir) una imagen del contenedor, la tarea es extenderla al clúster. A diferencia de los despliegues tradicionales, en el caso de las iteraciones de los desarrolladores mantener la disponibilidad no es realmente una preocupación. Por lo tanto, la manera más fácil de desplegar un nuevo código es simplemente eliminar el objeto Deployment asociado con el Deployment anterior y, luego, crear un nuevo despliegue que apunte a la imagen recién creada. También es posible actualizar un Deployment existente, pero esto desencadenará la lógica de despliegue en el recurso Deployment. Aunque es posible configurar un Deployment para poner en marcha el código de forma inmediata, hacerlo así introduce una diferencia entre el entorno de desarrollo y el entorno de producción, lo cual puede ser peligroso o desestabilizador. Imaginemos, por ejemplo, que accidentalmente hacemos push (subir) sobre la configuración de desarrollo de Deployment a producción. De repente, y de forma accidental, desplegaremos nuevas versiones en producción sin las pruebas y retardos adecuados entre las fases de despliegue. Debido a este riesgo, y como hay una alternativa, la mejor práctica es eliminar y volver a crear el Deployment.

Al igual que sucede en la instalación de dependencias, aquí también es una buena práctica crear un script para ejecutar este despliegue. Un ejemplo de script deploy.sh podría ser el siguiente:

kubectl delete -f ./my-service/deployment.yaml perl -pi -e 's/${old_version}/${new_version}/' ./my-service/deployment.yaml kubectl create -f ./my-service/deployment.yaml

Como antes, podemos integrarlo con las herramientas del lenguaje de programación para que, por ejemplo, un desarrollador pueda simplemente ejecutar npm run deploy para desplegar su nuevo código en el clúster.

Preparación de pruebas y depuración

Después de que un usuario haya implementado con éxito la versión de desarrollo de su aplicación, necesita probarla y, si hay problemas, depurar cualquier inconveniente que aparezca con la aplicación. Esto también puede ser un obstáculo a la hora del desarrollo en Kubernetes, porque no siempre está claro cómo interactuamos con el clúster. La línea de comandos de kubectl, como herramienta para conseguirlo, es una verdadera navaja suiza, desde kubectl logs a kubectl exe y kubectl port-forward. Pero aprender a usar las diferentes opciones y conseguir familiarizarse con la herramienta puede requerir una considerable experiencia. Además, debido a que la herramienta se ejecuta en el terminal, a menudo requiere la composición de varias ventanas para examinar simultáneamente el código fuente de la aplicación y la propia aplicación en ejecución.

Para agilizar la experiencia de pruebas y depuración, las herramientas de Kubernetes se integran cada vez más en entornos de desarrollo, como por ejemplo la extensión de código abierto de Visual Studio (VS) Code en Kubernetes. La extensión se instala fácilmente de forma gratuita desde el marketplace de VS Code. Cuando se instala, descubre automáticamente cualquier clúster que ya tengamos en nuestro archivo kubeconfig, y proporciona un panel de navegación en forma de árbol para que podamos ver el contenido de nuestro clúster de un vistazo.

Además de poder ver rápidamente el estado de nuestro clúster, la integración permite al desarrollador utilizar las herramientas disponibles mediante kubectl de una manera intuitiva y reconocible. Desde la vista en forma de árbol, si hacemos clic con el botón derecho del ratón en una cápsula de Kubernetes, podemos utilizar de forma inmediata el enrutamiento de puertos para establecer una conexión de red de la cápsula directamente con la máquina local. Del mismo modo, podemos acceder a los registros de la cápsula o incluso obtener un terminal en el contenedor en ejecución.

La integración de estos comandos con las expectativas de la prototípica interfaz de usuario (por ejemplo, si hacemos clic con el botón derecho del ratón, se muestra un menú contextual), así como la integración de estas experiencias además del código de la propia aplicación, permiten a los desarrolladores con una mínima experiencia en Kubernetes ser productivos con el clúster de desarrollo en un tiempo record.

Por supuesto, esta extensión de VS Code no es la única integración entre Kubernetes y un entorno de desarrollo. Hay otras que podemos instalar dependiendo de la elección del entorno y del estilo de programación (vi, emacs, etc.).

Mejores prácticas en el establecimiento de un entorno de desarrollo

Establecer flujos de trabajo que tengan éxito es clave para ser productivos y estar satisfechos. Seguir estas mejores prácticas ayudará a asegurar que los desarrolladores estén operativos de forma inmediata:

• Podemos pensar en la experiencia del desarrollador en tres fases: incorporación, desarrollo y pruebas. Debemos tener la seguridad de que el entorno de desarrollo que creamos es compatible con estas tres fases.

• Cuando se crea un clúster de desarrollo, se puede elegir entre un clúster grande y un clúster para cada desarrollador. Hay ventajas y desventajas en cada uno de ellos, pero en general un único clúster grande es el mejor enfoque.

• Cuando añadimos usuarios a un clúster, los añadimos con su propia identidad y acceso a su propio espacio de nombres. Usamos las limitaciones de recursos para restringir la porción de clúster que pueden usar.

• Cuando administramos espacios de nombres, debemos pensar en cómo podemos recoger recursos antiguos y no utilizados. Los desarrolladores pueden tener la mala costumbre de no eliminar las cosas que no usan. Utilizamos la automatización para eliminarlas si ellos no lo hacen.

• Pensemos en los servicios a nivel de clúster como son los registros y la supervisión, que podemos configurar para todos los usuarios. A veces, las dependencias a nivel de clúster, como las bases de datos, también es útil configurarlas en nombre de todos los usuarios utilizando plantillas como los gráficos de Helm.

Resumen

Hemos llegado al punto en el que crear un clúster de Kubernetes, especialmente en la nube, es relativamente sencillo, pero conseguir que los desarrolladores utilicen de forma productiva el clúster es considerablemente menos obvio y menos fácil. Al pensar en habilitar a los desarrolladores para crear aplicaciones en Kubernetes y que estas que tengan éxito, es importante pensar en los objetivos clave entorno a la incorporación, iteración, prueba y depuración de aplicaciones. De la misma manera, vale la pena invertir en algunas herramientas básicas específicas para la incorporación de usuarios, provisión de espacio de nombres y servicios de clúster, como la agregación básica de registros. Ver el clúster de desarrollo y los repositorios de código como una oportunidad para estandarizar y aplicar de la mejor manera posible las mejores prácticas nos asegurará desarrolladores satisfechos y productivos, que creen código de forma eficaz para desplegarlo en nuestros clústeres de producción de Kubernetes.

Конец ознакомительного фрагмента.

Текст предоставлен ООО «ЛитРес».

Прочитайте эту книгу целиком, купив полную легальную версию на ЛитРес.

Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.