Czytaj książkę: «Guía práctica de Kubernetes»
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.
CAPÍTULO 3
Monitorización y recopilación de registros en Kubernetes
En este capítulo, presentaremos las mejores prácticas para la monitorización y recopilación de registros en Kubernetes. Nos adentraremos en los detalles sobre las diferentes formas de monitorización y las métricas importantes a recopilar, y hablaremos de la creación de paneles a partir de estas métricas en bruto. A continuación, terminaremos con ejemplos en los que se implementa la monitorización para nuestro clúster de Kubernetes.
Métricas versus registros
En primer lugar, necesitamos entender la diferencia entre colección de registros y colección de métricas. Se complementan entre sí, pero sirven para fines diferentes.
Métricas
Series de valores numéricos medidos durante un período de tiempo.
Registros
Se utilizan para el análisis exploratorio de un sistema.
Un ejemplo en el que necesitamos utilizar tanto métricas como registros es cuando una aplicación tiene un rendimiento deficiente. Nuestra primera indicación del problema podría ser una alerta de alta latencia en las cápsulas que albergan la aplicación, pero las métricas podrían no dar una buena indicación del problema. A continuación, podríamos buscar en los registros para realizar una investigación de los errores que arroja la aplicación.
Técnicas de monitorización
La monitorización Black-box se centra en la monitorización de una aplicación desde el exterior y es el procedimiento que se ha utilizado tradicionalmente cuando se monitorizan sistemas en relación con componentes como CPU, memoria, almacenamiento, etc. La monitorización Black-box puede seguir siendo útil para la monitorización a nivel de infraestructura, pero carece de información y contexto sobre cómo está funcionando la aplicación. Por ejemplo, para probar si un clúster funciona correctamente, podríamos planificar una cápsula y, si tenemos éxito, sabemos que el scheduler (planificador) y la detección de servicios funcionan correctamente en nuestro clúster, así que podemos suponer que los componentes del clúster también funcionan bien.
La monitorización White-box se centra en los detalles, en el contexto del estado de la aplicación, como pueden ser el total de peticiones HTTP, el número de errores 500, la latencia de las peticiones, etc. Con la monitorización White-box, podemos empezar a entender el por qué del estado de nuestro sistema. Nos permite preguntar: «¿Por qué se ha llenado el disco?» y no solo «El disco se ha llenado».
Formas de monitorización
Podríamos ver la monitorización y decir: «¿Qué dificultad puede tener esto? Siempre hemos monitorizado nuestros sistemas». Sí, algunas de las formas de monitorización típicas que se aplican hoy en día también encajan con la forma en que monitoriza Kubernetes. La diferencia está en que las plataformas como Kubernetes son mucho más dinámicas y transitorias, y tendremos que cambiar nuestra forma de pensar sobre cómo monitorizar estos entornos. Por ejemplo, cuando se monitoriza un sistema virtual (VM), esperamos que VM se mantenga activo las 24 horas del día, los 7 días de la semana y que, además, preserve su estado. En Kubernetes, las cápsulas pueden ser muy dinámicas y de corta duración, por lo que necesitamos tener una monitorización que pueda manejar esta naturaleza dinámica y transitoria.
Hay un par de formas de monitorización distintas que tendremos en cuenta cuando se monitorizan sistemas distribuidos.
El método USE, popularizado por Brendan Gregg, se centra en lo siguiente:
• U: utilización
• S: saturación
• E: errores
Este método se centra en la monitorización de la infraestructura porque presenta limitaciones cuando se utiliza en la monitorización a nivel de aplicación. El método USE se describe así: «Para cada recurso, verificar la utilización, saturación y tasas de error». Este método nos permite identificar rápidamente las limitaciones de recursos y las tasas de error de los sistemas. Por ejemplo, para verificar el funcionamiento de la red en relación con los nodos en el clúster, tendremos interés en monitorizar la utilización, saturación y tasa de errores para poder identificar fácilmente cualquier cuello de botella de la red o errores que se produzcan en la pila de la red. El método USE es una herramienta que forma parte de una caja de herramientas más grande y no es el único método que utilizaremos para monitorizar nuestros sistemas.
Otro enfoque en lo que respecta a la monitorización es el llamado método RED, popularizado por Tom Wilkie. El enfoque del método RED se centra en lo siguiente:
• R: rate (tasa)
• E: errores
• D: duración
La filosofía fue adoptada de las Four Golden Signals (cuatro señales doradas) de Google:
• Latencia (cuánto tiempo se tarda en atender una solicitud)
• Tráfico (cuánta demanda hay en nuestro sistema)
• Errores (tasa de solicitudes que fallan)
• Saturación (cómo se utiliza nuestro servicio)
Por ejemplo, podríamos utilizar este método para monitorizar un servicio de frontend que se ejecuta en Kubernetes y calcular lo siguiente:
• ¿Cuántas solicitudes está procesando el servicio de frontend?
• ¿Cuántos errores 500 están recibiendo los usuarios del servicio?
• ¿El servicio está sobreexplotado por las solicitudes?
Como se puede ver en el ejemplo anterior, este método se centra más en lo experimentados que sean los usuarios y en su experiencia con el servicio.
Los métodos USE y RED son complementarios, ya que el método USE se centra en los componentes de la infraestructura y el método RED se centra en monitorizar la experiencia del usuario final de la aplicación.
Visión general de las métricas en Kubernetes
Ahora que conocemos las diferentes técnicas y formas de monitorización, veamos qué componentes deberíamos monitorizar en nuestro clúster de Kubernetes. El clúster de Kubernetes consta de los componentes del plano de control y los componentes del nodo worker (esclavo). Los componentes del plano de control son el servidor de la API, etcd, el scheduler (planificador) y el controller manager (administrador de controladores). Los nodos worker están formados por kubelet, container runtime (software de tiempo de ejecución del contenedor), kubeproxy, kube-dns y las cápsulas. Es necesario monitorizar todos estos componentes para garantizar un correcto funcionamiento del clúster y de la aplicación.
Kubernetes presenta estas métricas de varias maneras, así que echemos un vistazo a los diferentes componentes que podemos utilizar para recopilar métricas en el clúster.
cAdvisor
Container Advisor, o cAdvisor, es un proyecto de código abierto que recopila recursos y métricas de los contenedores que se ejecutan en un nodo. cAdvisor está integrado en kubelet de Kubernetes, que se ejecuta en todos los nodos del clúster. Recoge métricas de memoria y CPU a través del árbol de grupos de control (cgroup) de Linux. Si no estás familiarizado con los cgroups, es una característica del kernel (núcleo) de Linux que permite el aislamiento de recursos de CPU, E/S de disco, o E/S de red. cAdvisor también recopilará las métricas del disco a través del sistema statfs, que está integrado en el kernel (núcleo) de Linux. Estos son los detalles de la aplicación de los que realmente no tenemos que preocuparnos, pero debemos entender cómo se presentan estas métricas y el tipo de información que podemos recopilar.
Debemos considerar a cAdvisor como fuente de verdad para todas las métricas de los contenedores.
Servidor de métricas
El servidor de métricas de Kubernetes y la API de Metrics Server (servidor de métricas) sustituyen al servidor obsoleto Heapster. Heapster tenía algunas desventajas arquitectónicas derivadas de la forma en que se implementó el receptor de datos, que dio lugar a muchas soluciones comercializadas en el código base del núcleo de Heapster. Este problema se resolvió mediante la aplicación de un recurso y la API de Custom Metrics (métricas personalizadas) como una API agregada a Kubernetes. Esto permite cambiar las aplicaciones sin cambiar la API.
Hay dos aspectos que debemos entender de la API de Metrics Server y del servidor de métricas.
En primer lugar, la aplicación clásica de la API de Resource Metrics (métricas de recursos) es el servidor de métricas. El servidor de métricas recoge métricas de recursos como la CPU y la memoria. Recoge estas métricas de la API de kubelet y, luego, las almacena en la memoria. Kubernetes utiliza estas métricas de recursos en el scheduler (planificador), en el Horizontal Pod Autoscaler (autoescalador horizontal de cápsulas) (HPA) y en el Vertical Pod Autoscaler (autoescalador vertical de cápsulas) (VPA).
En segundo lugar, la API de Custom Metrics permite a los sistemas de monitorización recopilar métricas de forma arbitraria. Esto hace posible que las soluciones de monitorización puedan crear adaptadores a medida que permiten extender la monitorización más allá de las métricas de recursos básicos. Por ejemplo, Prometheus creó uno de los primeros adaptadores de métricas personalizadas, que permite usar HPA basado en una métrica personalizada. Esto abre la posibilidad a una mejor escalabilidad basada en nuestro caso de uso, ya que ahora podemos incorporar métricas, como el tamaño de la cola y la escala, basadas en una métrica que podría ser externa a Kubernetes.
Ahora que existe una API de Metrics (métricas) estandarizada, se abren muchas posibilidades que van más allá de las antiguas métricas de CPU y memoria.
kube-state-metrics
kube-state-metrics es un accesorio de Kubernetes que monitoriza el objeto almacenado en Kubernetes. Donde cAdvisor y el servidor de métricas se utilizan para proporcionar métricas detalladas sobre el uso de recursos, kube-state-metrics se centra en la identificación de las condiciones de los objetos de Kubernetes desplegados en el clúster.
A continuación, se presentan algunas preguntas que kube-state-metrics puede responder por nosotros:
• Cápsulas
- ¿Cuántas cápsulas están desplegadas en el clúster?
- ¿Cuántas cápsulas están en estado de pendiente?
- ¿Existen suficientes recursos para atender la solicitud de una cápsula?
• Deployments (implementaciones)
- ¿Cuántas cápsulas están en estado de funcionamiento en comparación con las que están en estado deseado?
- ¿Cuántas réplicas hay disponibles?
- ¿Qué despliegues se han actualizado?
• Nodos
- ¿Cuál es el estado de los nodos worker (esclavo)?
- ¿Cuáles son los núcleos de CPU asignables en el clúster?
- ¿Hay algún nodo que no se pueda programar?
• Trabajos
- ¿Cuándo ha comenzado un trabajo?
- ¿Cuándo se ha completado un trabajo?
- ¿Cuántos trabajos han fracasado?
En el momento de escribir esto, kube-state-metrics rastrea 22 tipos de objetos. Su número siempre va en aumento. Podemos encontrar la documentación en el repositorio de Github.
¿Qué métricas debemos monitorizar?
La respuesta fácil es «Todo», pero si tratamos de controlar en exceso podemos crear demasiado ruido que filtra las auténticas señales de las señales cuya situación necesitamos conocer. Cuando pensamos en la monitorización en Kubernetes, adoptamos un enfoque por capas que tiene en cuenta lo siguiente:
• Nodos físicos o virtuales
• Componentes del clúster
• Accesorios del clúster
• Aplicaciones de usuario final
El uso de este enfoque de la monitorización por capas nos permite identificar con mayor facilidad las señales correctas en nuestro sistema de monitorización. Nos permite abordar los problemas con un planteamiento selectivo. Por ejemplo, si tenemos cápsulas que entran en un estado de pendiente, podemos empezar con la utilización de recursos de los nodos y, si todo está bien, podemos centrarnos en los componentes a nivel de clúster. A continuación, se presentan las métricas que podríamos utilizar como objetivo en nuestro sistema:
• Nodos
- Utilización de la CPU
- Utilización de la memoria
- Utilización de la red
- Utilización del disco
• Componentes del clúster
- Latencia etcd
• Accesorios del clúster
- Autoescalador de clúster
- Controlador Ingress
• Aplicación
- Utilización y saturación de la memoria del contenedor
- Utilización de la CPU del contenedor
- Utilización de la red de contenedores y tasa de errores
- - Métricas del marco específico de la aplicación
Herramientas de monitorización
Hay muchas herramientas de monitorización que pueden integrarse en Kubernetes. Su número aumenta cada día y, para mejorar su integración, se diseñan teniendo en cuenta el conjunto de características de Kubernetes. A continuación, se presentan algunas herramientas populares que se integran con Kubernetes:
Prometheus
Prometheus es un conjunto de herramientas de monitorización y alerta de sistemas, de código abierto, que originalmente creó SoundCloud. Desde su aparición en 2012, muchas empresas y organizaciones han adoptado Prometheus. El proyecto tiene un desarrollador muy activo y una comunidad de usuarios. Ahora es un proyecto autónomo de código abierto y se mantiene independiente de cualquier empresa. Para destacar lo anterior, y para clarificar la estructura de gobierno del proyecto Prometheus, podemos decir que se unió a la Cloud Native Computing Foundation (CNCF) en 2016 como segundo proyecto acogido, después de Kubernetes.
InfluxDB
InfluxDB es una base de datos de series temporales diseñada para manejar altas cargas de escritura y consulta. Es un componente integral de la pila TICK (Telegraf, InfluxDB, Chronograf, y Kapacitor). InfluxDB está destinado a utilizarse como almacén de reserva para cualquier caso práctico que involucre grandes cantidades de datos con fecha y hora, incluyendo monitorización de DevOps, métricas de aplicación, datos de sensores de IoT y análisis en tiempo real.
Datadog
Datadog proporciona un servicio de monitorización para aplicaciones a escala de la nube. Ofrece monitorización de servidores, bases de datos, herramientas y servicios a través de una plataforma de análisis de datos basada en SaaS.
Sysdig
Sysdig Monitor es una herramienta comercial que proporciona monitorización de Docker y Kubernetes de aplicaciones nativas de contenedores. Sysdig también nos permite recoger, correlacionar y consultar las métricas de Prometheus con integración directa en Kubernetes.
Herramientas para proveedores de la nube
GCP Stackdriver
Stackdriver Kubernetes Engine Monitoring está diseñado para monitorizar clústeres de Google Kubernetes Engine (GKE). Gestiona conjuntamente los servicios de monitorización y recopilación de registros y cuenta con una interfaz que proporciona un panel personalizado para los clústeres GKE. Stackdriver Monitoring proporciona visibilidad sobre el rendimiento, el tiempo de actividad y el estado general de aplicaciones en la nube. Recopila métricas, eventos y metadatos de Google Cloud Platform (GCP), Amazon Web Services (AWS), sondas de tiempo de actividad alojadas e instrumentación de aplicaciones.
Microsoft Azure Monitor para contenedores
Azure Monitor para contenedores es una función diseñada para monitorizar el rendimiento de tareas de contenedores implementadas en cualquiera de las Azure Container Instances o en clústeres administrados de Kubernetes y alojados en Azure Kubernetes Service. La monitorización de contenedores es crítica, especialmente cuando ejecutamos un clúster de producción, a escala, con varias aplicaciones. Azure Monitor para contenedores nos ofrece visibilidad sobre el rendimiento a través de la recopilación de métricas de memoria y procesador en relación con controladores, nodos y contenedores que están disponibles en Kubernetes a través de la API de Metrics. También se recopilan los registros de los contenedores. Después de habilitar la monitorización de los clústeres de Kubernetes, las métricas y los registros se recopilan automáticamente a través de una versión en contenedor del agente Log Analytics de Linux.
Información sobre contenedores AWS
Si utilizamos Amazon Elastic Container Service (ECS), Amazon Elastic Kubernetes Service u otras plataformas de Kubernetes en Amazon EC2, podemos usar CloudWatch Container Insights para recopilar, agregar y hacer resúmenes de métricas y registros de nuestras aplicaciones en contenedores y microservicios. Las métricas incluyen la utilización de recursos como la CPU, la memoria, el disco y la red. Container Insights también proporciona información de diagnóstico, como los fallos de reinicio de contenedor, para ayudarnos a aislar los problemas y resolverlos rápidamente.
Un aspecto importante a la hora de considerar la aplicación de una herramienta para monitorizar métricas es observar cómo las almacena. Las herramientas que proporcionan bases de datos de series temporales con pares clave/valor nos proporcionarán un mayor grado de atributos para las métricas.
| Siempre evaluaremos las herramientas de monitorización que ya tenemos, porque si utilizamos una nueva herramienta de monitorización, esta tiene una curva de aprendizaje y un coste debido a su aplicación operativa. Muchas de las herramientas de monitorización están integradas en Kubernetes, así que evaluaremos las que tenemos actualmente y si cumplen nuestros requisitos. |
Monitorización en Kubernetes con Prometheus
En esta sección nos centramos en el seguimiento de métricas con Prometheus, que ofrece integraciones aceptables con el etiquetado, el descubrimiento de servicios y los metadatos de Kubernetes. Los conceptos de alto nivel que implementamos a lo largo del capítulo también se podrán aplicar a otros sistemas de monitorización.
Prometheus es un proyecto de código abierto que está alojado en la CNCF. Originalmente lo desarrolló SoundCloud, y muchos de sus conceptos tienen su fundamento en BorgMon, el sistema de monitorización interna de Google. Implementa un modelo de datos multidimensional con pares de claves que funcionan de forma muy parecida a como lo hace el sistema de etiquetado de Kubernetes. Prometheus presenta las métricas en un formato que pueden leer los usuarios, como se aprecia en el siguiente ejemplo:
# HELP node_cpu_seconds_total Seconds the CPU is spent in each mode. # TYPE node_cpu_seconds_total counter node_cpu_seconds_total{cpu="0",mode="idle"} 5144.64 node_cpu_seconds_total{cpu="0",mode="iowait"} 117.98
Para recopilar métricas, Prometheus utiliza un modelo de extracción en el que se raspa el punto final de métricas para recoger las métricas que ingiere el servidor de Prometheus. Sistemas como Kubernetes ya presentan sus métricas en formato Prometheus, facilitando así la recopilación de métricas. Muchos otros proyectos del ecosistema de Kubernetes (NGINX, Traefik, Istio, LinkerD, etc.) también presentan sus métricas en formato Prometheus. Prometheus también puede utilizar exportadores, que nos permiten tomar las métricas que emite nuestro servicio y traducirlas a métricas con formato Prometheus.
Prometheus tiene una arquitectura muy simplificada, como se muestra en la Figura 3-1.

Figura 3-1 Arquitectura de Prometheus.
| Podemos instalar Prometheus dentro o fuera del clúster. Una buena práctica es monitorizar nuestro clúster desde un «clúster utilitario», para evitar que un problema de producción afecte también a nuestro sistema de monitorización. Hay herramientas, como Thanos, que proporcionan una alta disponibilidad de Prometheus y nos permiten exportar métricas a un almacenamiento externo al sistema. |
Una inmersión a fondo en la arquitectura de Prometheus iría más allá del alcance de este libro. Un buen libro para empezar que trata el tema en profundidad es Prometheus: Up & Running (O’Reilly).
Ahora vamos a poner la configuración de Prometheus en nuestro clúster de Kubernetes. Hay muchas maneras diferentes de hacerlo, y el despliegue dependerá de la aplicación específica. En este capítulo se instala Prometheus Operator:
Servidor Prometheus
Extrae y almacena las métricas que se recogen de los sistemas.
Operador Prometheus
Hace que la configuración de Prometheus sea nativa de Kubernetes, y administra y opera los clústeres de Prometheus y Alertmanager. Nos permite crear, destruir y configurar los recursos de Prometheus mediante las definiciones de recursos nativos de Kubernetes.
Exportador de nodos
Exporta las métricas de hosts desde los nodos de Kubernetes en el clúster.
kube-state-metrics
Recopila métricas específicas de Kubernetes.
Alertmanager
Permite configurar y enviar alertas a sistemas externos.
Grafana
Proporciona la visualización en el dashboard (panel) de las capacidades.
helm install --name prom stable/prometheus-operator
Después de instalar Operator (operador), deberíamos poder ver las siguientes cápsulas implementadas en el clúster:
$ kubectl get pods -n monitoring NAME READY STATUS RESTARTS AGE alertmanager-main-0 2/2 Running 0 5h39m alertmanager-main-1 2/2 Running 0 5h39m alertmanager-main-2 2/2 Running 0 5h38m grafana-5d8f767-ct2ws 1/1 Running 0 5h39m kube-state-metrics- 7fb8b47448-k6j6g 4/4 Running 0 5h39m node-exporter-5zk6k 2/2 Running 0 5h39m node-exporter-874ss 2/2 Running 0 5h39m node-exporter-9mtgd 2/2 Running 0 5h39m node-exporter-w6xwt 2/2 Running 0 5h39m prometheus-adapter- 66fc7797fd-ddgk5 1/1 Running 0 5h39m prometheus-k8s-0 3/3 Running 1 5h39m prometheus-k8s-1 3/3 Running 1 5h39m prometheus-operator- 7cb68545c6-gm84j 1/1 Running 0 5h39m
Echemos un vistazo al servidor de Prometheus para ver cómo podemos ejecutar algunas consultas para extraer las métricas de Kubernetes:
kubectl port-forward svc/prom-prometheus-operator-prometheus 9090
Esta consulta crea un túnel a nuestro localhost en el puerto 9090. Ahora, podemos abrir un navegador web y conectarnos al servidor Prometheus en http://127.0.0.1:9090.
La Figura 3-2 muestra la pantalla que veremos si hemos implementado con éxito Prometheus en el clúster.
Ahora que hemos implementado Prometheus, exploremos algunas métricas de Kubernetes utilizando el lenguaje de consulta Prometheus PromQL. Hay una guía de PromQL Basics disponible en la página oficial de Prometheus (https://prometheus.io/docs/prometheus/latest/querying/basics/).
Anteriormente, hablamos sobre el empleo del método USE, así que vamos a recopilar algunas métricas de nodos sobre la utilización y saturación de la CPU.

Figura 3-2 Panel de Prometheus.
En la entrada Expression, introducimos la siguiente consulta:
avg(rate(node_cpu_seconds_total[5m]))
Esta consulta devolverá el valor promedio de utilización de la CPU para todo el clúster.
Si queremos obtener la utilización de la CPU por nodo, podemos escribir una consulta como la siguiente:
avg(rate(rate(node_cpu_seconds_total[5m]))) by (node_name)
Esta consulta devuelve el valor promedio de utilización de la CPU para cada nodo del clúster.
Así que, ahora que tenemos algo de experiencia en la ejecución de consultas en Prometheus, vamos a ver cómo Grafana puede ayudar a crear la visualización en el panel de estas métricas habituales del método USE que queremos rastrear. Lo mejor de Prometheus Operator es que viene con algunos paneles prediseñados de Grafana que podemos usar.
Ahora tendremos que crear un túnel de redireccionamiento de puertos a la cápsula de Grafana para que podamos acceder desde nuestra máquina local:
kubectl port-forward svc/prom-grafana 3000:3000
Ahora, apuntamos nuestro navegador web a http://localhost:3000 e iniciamos sesión usando las siguientes credenciales:
• Username (nombre de usuario): admin
• Password (contraseña): admin
Situado bajo el panel de Grafana, encontraremos un panel llamado Kubernetes / USE /Method / Cluster. Este panel nos ofrece una visión general de la utilización y saturación del clúster de Kubernetes, que es el corazón del método USE.
La Figura 3-3 presenta un ejemplo del panel.

Figura 3-3 Panel de Grafana.
Seguimos adelante; vamos a dedicar tiempo a explorar los diferentes paneles y métricas que se pueden visualizar en Grafana.
| Evitaremos crear demasiados paneles (hecho conocido como «El muro de los gráficos») porque puede resultar difícil razonar en situaciones en las que hay que resolver muchos problemas. Puede que pienses que tener más información en un panel supone una mejor monitorización, pero la mayoría de las veces lo que hace es causar más confusión al usuario que consulta el panel. Orientaremos el diseño de nuestro panel a los resultados y al tiempo de resolución. |
Descripción general de la recopilación de registros
Hasta este momento hemos tratado ampliamente sobre métricas y Kubernetes, pero para obtener una imagen completa de nuestro entorno también necesitamos recopilar y centralizar los registros del clúster de Kubernetes y los de las aplicaciones desplegadas en el clúster.
Con los registros, puede resultar fácil decir «Vamos a registrarlo todo», pero esta decisión puede ocasionar dos problemas:
• Que haya demasiado ruido para detectar problemas de forma inmediata.
• Que los registros consuman muchos recursos, lo cual supone un alto coste.
No hay una respuesta clara sobre lo que deberíamos registrar exactamente, puesto que la depuración de registros se convierte en un mal necesario. Con el tiempo empezaremos a entender mejor nuestro entorno y reconoceremos el ruido del sistema de registros al que podemos dejar de prestar atención. Además, para abordar la cantidad cada vez mayor de registros almacenados, necesitaremos implementar una política de retención y de archivado. Desde el punto de vista del usuario final, tener un histórico de registros de entre 30 y 45 días es una buena opción. Esto permite la investigación de problemas que se manifiestan en un período de tiempo más largo, pero también reduce la cantidad de recursos necesarios para almacenar los registros. Si necesitáramos un almacenamiento a más largo plazo por razones de cumplimiento, podríamos pensar en archivar los registros en recursos más baratos.
En un clúster de Kubernetes, hay varios componentes que se deben registrar. A continuación, vemos una lista de los comoponentes cuyas métricas deberíamos recopilar:
• Registros de nodos
• Registros del plano de control de Kubernetes
- Servidor API
- Administrador de controladores
- Scheduler (Planificador)
• Registros de auditoría de Kubernetes
• Registros de contenedores de la aplicación
Con los registros de nodos podemos recopilar eventos que se producen en los servicios esenciales de los nodos. Por ejemplo, podemos tener interés en recopilar registros del daemon (demonio) Docker que se ejecuta en los nodos worker (esclavos). Un daemon Docker que funcione correctamente será esencial para ejecutar los contenedores en los nodos worker. La recopilación de estos registros nos ayudará a diagnosticar cualquier problema que podamos tener con el daemon Docker, y nos proporcionará información sobre cualquier problema subyacente del mismo. También hay otros servicios esenciales del nodo subyacente que nos interesará registrar.
El plano de control de Kubernetes consta de varios componentes, de los que necesitaremos recopilar registros para conseguir más información sobre problemas subyacentes. El plano de control es fundamental para que el clúster funcione correctamente, y a nosotros nos interesará agregar los registros que este almacena en el host en /var/log/kube-APIserver.log, /var/log/kube-scheduler.log y /var/log/kube-controller-manager.log. El controller manager (administrador de controladores) es responsable de la creación de objetos definidos por el usuario final. Como ejemplo, nosotros —como usuarios— podemos crear un servicio Kubernetes con type (tipo) LoadBalancer que se encuentre en un estado pendiente. Es posible que los eventos de Kubernetes no proporcionen todos los detalles para diagnosticar el problema. Si los registros se agrupan en un sistema centralizado, nos proporcionarán más detalles sobre el problema subyacente y una forma más rápida de investigar el problema.
Podemos pensar que los registros de auditoría de Kubernetes son como una monitorización de seguridad porque nos proporcionan una visión de quién hizo qué dentro del sistema. Estos registros pueden ser muy ruidosos, por lo que nos interesa adaptarlos a nuestro entorno. En muchos casos, estos registros pueden causar un gran pico en el sistema de registros cuando se inicializa por primera vez, así que debemos asegurarnos de seguir la guía de la documentación de Kubernetes sobre la monitorización de registros de auditoría.
Los registros de los contenedores de la aplicación nos proporcionan información de los registros reales que emite la aplicación. Podemos enviar estos registros a un repositorio central de varias formas. La primera forma, y la que se recomienda, es enviar todos los registros de la aplicación a STDOUT, porque STDOUT proporciona un modo uniforme de recopilación de dichos registros, y un conjunto de daemons de monitorización pueden recopilar los registros directamente del daemon Docker. La otra forma es usar un patrón sidecar y ejecutar un contenedor de envío de registros junto al contenedor de la aplicación en una cápsula de Kubernetes. Es posible que debamos utilizar este patrón si nuestra aplicación se conecta al sistema de archivos.
| Hay muchas opciones y configuraciones para la administración de los registros de auditoría de Kubernetes. Estos registros de auditoría pueden ser muy ruidosos y puede resultar caro registrar todas las acciones. Es una buena idea considerar estudiar la documentación de registro de auditoría, para que podamos ajustar estos registros a nuestro entorno. |
Darmowy fragment się skończył.