ES-ES-Containerize Your Apps With Docker and Kubernetes PDF

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 325

Incluir aplicaciones

en contenedores
con Docker
Y Kubernetes
Implementar, escalar, organizar y administrar
contenedores con Docker y Kubernetes

www.packt.com
Dr. Gabriel N. Schenker
Incluir aplicaciones en contenedores
con Docker y Kubernetes

Implementar, escalar, organizar y administrar


contenedores con Docker y Kubernetes

Dr. Gabriel N. Schenker

BIRMINGHAM - MUMBAI
Incluir aplicaciones en contenedores con Docker
y Kubernetes
Copyright © 2018 Packt Publishing

Todos los derechos reservados. Ninguna parte de este libro puede reproducirse,
almacenarse en un sistema de recuperación o transmitirse en cualquier formato o por
cualquier medio, sin el permiso previo por escrito del editor, excepto en el caso de
citas breves incluidas en reseñas o artículos críticos.
En aras de asegurar la exactitud de la información presentada, se han realizado todos
los esfuerzos posibles en la preparación de este libro. No obstante, la información
contenida en él se proporciona sin garantía, ya sea expresa o implícita. Ni el autor ni
Packt Publishing, o sus filiales y distribuidores, serán responsables de cualquier daño
causado o presuntamente causado por este libro ya sea de forma directa o indirecta.
Si bien Packt Publishing ha procurado suministrar información sobre las marcas
comerciales de todas las empresas y productos mencionados en este libro mediante
el uso correspondiente de mayúsculas, no puede garantizar la exactitud de esta
información.

Editor responsable: Vijin Boricha


Editor de adquisiciones: Shrilekha Inani
Editores de desarrollo de contenido: Ronn Kurien
Editor técnico: Swathy Mohan
Editor de copias: Safis Editing
Coordinador del proyecto: Jagdish Prabhu
Corrector: Safis Editing
Encargados de los índices: Mariammal Chettiyar
Gráficos: Tom scaria
Coordinador de producción: Nilesh Mohite

Fecha de primera publicación: septiembre de 2018


Referencia de producción: 1260918

Publicado por Packt Publishing Ltd.


Livery Place
35 Livery Street
Birmingham B3 2PB, Reino Unido.
ISBN 978-1-78961-036-9
www.packtpub.com
mapt.io

Mapt es una biblioteca digital online que te permite disfrutar de pleno acceso a más
de 5000 libros y vídeos, así como a las herramientas líderes del sector, para ayudarte
a planificar tu desarrollo personal y avanzar en tu carrera profesional. Para obtener
más información, visita nuestro sitio web.

¿Por qué suscribirte?


• Pasa menos tiempo aprendiendo y más tiempo programando con prácticos
e-books y vídeos de más de 4000 profesionales del sector
• Aprende mejor con planes de aprendizaje diseñados especialmente para ti
• Consigue un e-book gratuito o un vídeo cada mes
• Mapt tiene una función de búsqueda completa
• Capacidad para copiar, pegar, imprimir y marcar contenidos

PacktPub.com
¿Sabías que Packt ofrece versiones en e-book de cada libro publicado en formato PDF
y ePub? Puedes actualizarte a la versión de e-book en www.PacktPub.com y, como
cliente de libros impresos, tienes derecho a un descuento en la copia del e-book.
Ponte en contacto con nosotros en la dirección de correo electrónico customercare@
packtpub.com para obtener más información.

En www.PacktPub.com, también podrás leer una colección de artículos técnicos


gratuitos, inscribirte en una gran variedad de boletines gratuitos y recibir descuentos
y ofertas exclusivos en libros y e-books de Packt.
Colaboradores

Acerca del autor


El Dr. Gabriel N. Schenker tiene más de 25 años de experiencia como consultor
independiente, arquitecto, líder, formador, mentor y desarrollador. Actualmente,
Gabriel trabaja como desarrollador sénior de programas de estudios en Confluent,
después de haber ocupado un puesto similar en Docker. Gabriel tiene un doctorado
en física y es Docker Captain, Certified Docker Associate y ASP Insider. Cuando
no está trabajando, Gabriel disfruta de su tiempo libre con su maravillosa esposa
Veronicah y sus hijos.
Acerca del revisor
Actualmente, Xijing Zhang es desarrolladora de programas de estudio técnicos
en Docker, después de haberse graduado en la Universidad del Sur de California
como ingeniera en electricidad. Anteriormente, trabajó de interina en el equipo
de análisis de fallos de SanDisk y ha ocupado varios puestos de investigación en
la USC y la Universidad de Tsinghua. Ha trabajado en proyectos relacionados con
la fabricación más eficiente de sistemas de aire acondicionado, seguridad de energía
nuclear y emisión de fotones únicos.

Peter McKee es arquitecto de software e ingeniero de software sénior en Docker,


Inc. Lidera el equipo técnico que se encarga del Docker Success Center. Lleva más de
20 años dirigiendo y asesorando equipos. Cuando no construye cosas con software,
pasa el tiempo con su esposa y sus siete hijos en la hermosa Austin, Texas.

Packt busca a autores como tú


Si quieres convertirte en un autor de Packt, visita authors.packtpub.com y envía tu
solicitud hoy mismo. Hemos trabajado con miles de desarrolladores y profesionales
de la tecnología, igual que tú, para ayudarles a compartir sus conocimientos con
la comunidad técnica global. Puedes enviar una solicitud general, sobre un tema
específico para el que estemos reclutando autores o presentar tu propia idea.
Índice
Prefacioix
Capítulo 1: ¿Qué son los contenedores y por qué debo usarlos? 1
Requisitos técnicos 2
¿Qué son los contenedores? 2
¿Por qué son importantes los contenedores? 5
Mejorar la seguridad 5
Simulación de entornos de producción 6
Estandarización de infraestructuras 6
¿Cuál es la ventaja para mí o para mi empresa? 6
El proyecto Moby 7
Productos de Docker 8
Docker CE 8
Docker EE 9
El ecosistema de contenedores 9
Arquitectura de contenedores 10
Resumen11
Preguntas12
Lectura adicional 13
Capítulo 2: Configuración de un entorno de trabajo 15
Requisitos técnicos 16
El shell de comandos de Linux 16
PowerShell para Windows 17
Uso de un administrador de paquetes 17
Instalación de Homebrew en un macOS 17
Instalación de Chocolatey en Windows 18
Selección de un editor de código 19
Docker Toolbox 19

[i]
Contenido

Docker para macOS y Docker para Windows 22


Instalación de Docker para macOS 22
Instalación de Docker para Windows 24
Uso de docker-machine en Windows con Hyper-V 24
Minikube26
Instalación de Minikube en macOS y Windows 26
Prueba de Minikube y kubectl 27
Clonación del repositorio de código fuente 28
Resumen29
Preguntas29
Lectura adicional 29
Capítulo 3: Trabajar con contenedores 31
Requisitos técnicos 32
Ejecución del primer contenedor 32
Inicio, detención y eliminación de contenedores 33
Ejecutar un contenedor de citas aleatorias 35
Listado de contenedores 37
Detención e inicio de contenedores 38
Eliminación de contenedores 39
Inspección de contenedores 40
Ejecución del comando exec en un contenedor en ejecución 42
Conexión a un contenedor en ejecución 43
Recuperación de registros de contenedores 45
Controladores de registro 46
Uso de un controlador de registro específico del contenedor 47
Tema avanzado: cambiar el controlador de registro predeterminado 47
Anatomía de los contenedores 48
Arquitectura49
Espacios de nombres 50
Grupos de control (cgroups) 51
Sistema de archivos Union (UnionFS) 52
Código de contenedores 52
Runc52
Containerd52
Resumen53
Preguntas53
Lectura adicional 53

[ ii ]
Contenido

Capítulo 4: Creación y gestión de imágenes de contenedores 55


¿Qué son las imágenes? 56
El sistema de archivos en capas 56
La capa de contenedor grabable 58
Copy-on-write (copiar al escribir) 59
Controladores de gráficos 59
Creación de imágenes 60
Creación de imágenes interactivas 60
Uso de Dockerfiles 63
La palabra clave FROM 64
La palabra clave RUN 65
Las palabras clave COPY y ADD 66
La palabra clave WORKDIR 67
Las palabras clave CMD y ENTRYPOINT 68
Un Dockerfile complejo 70
Creación de una imagen 71
Creación de imágenes en varios pasos 75
Prácticas recomendadas de Dockerfiles 77
Guardar y cargar imágenes 79
Compartir o enviar imágenes 79
Etiquetado de una imagen 80
Espacios de nombres de imagen 80
Imágenes oficiales 82
Enviar imágenes a un registro 82
Resumen83
Preguntas83
Lectura adicional 84
Capítulo 5: Administración de volúmenes y sistemas de datos 85
Requisitos técnicos 86
Creación y montaje de volúmenes de datos 86
Modificación de la capa de contenedor 86
Creación de volúmenes 87
Montaje de un volumen 89
Eliminación de volúmenes 90
Compartir datos entre contenedores 91
Uso de volúmenes de host 92
Definición de volúmenes en imágenes 95
Obtención de información sobre el sistema Docker 97
Listado del consumo de recursos 100

[ iii ]
Contenido

Eliminación de los recursos no utilizados 101


Limpieza de contenedores 101
Limpieza de imágenes 102
Limpieza de volúmenes 103
Limpieza de redes 104
Limpieza de todos los recursos 104
Consumo de eventos del sistema Docker 104
Resumen106
Preguntas106
Lectura adicional 107
Capítulo 6: Arquitectura de aplicaciones distribuidas 109
¿Qué es una arquitectura de aplicaciones distribuidas? 110
Definición de la terminología 110
Patrones y prácticas recomendadas 113
Componentes ligeramente acoplados 113
Sin estado o con estado 113
Detección de servicios 114
Enrutamiento116
Equilibrio de carga 116
Programación defensiva 117
Reintentos117
Registro117
Gestión de errores 117
Redundancia118
Comprobaciones de estado 118
Patrón de cortacircuitos 119
Ejecución en producción 120
Registro120
Seguimiento120
Supervisión121
Actualizaciones de la aplicación 121
Actualizaciones graduales 121
Implementaciones blue-green 122
Versiones Canary 122
Cambios irreversibles de datos 123
Reversión123
Resumen 124
Preguntas 124
Lectura adicional 125

[ iv ]
Contenido

Capítulo 7: Conexión en red con un solo host 127


Requisitos técnicos 128
Modelo de red de contenedores 128
Protección mediante cortafuegos de la red 130
Red de puente 131
Red de host 141
Red nula 142
Ejecución en un espacio de nombres de red existente 143
Gestión de puertos 145
Resumen 147
Preguntas 148
Lectura adicional 148
Capítulo 8: Docker Compose 149
Requisitos técnicos 150
Desmitificación del enfoque declarativo frente al imperativo 150
Ejecución de una aplicación multiservicio 151
Escalado de un servicio 156
Creación y envío de una aplicación 159
Resumen160
Preguntas160
Lectura adicional 160
Capítulo 9: Orquestadores 161
¿Qué son los orquestadores y por qué los necesitamos? 162
Las tareas de un orquestador 163
Conciliar el estado deseado 163
Servicios replicados y globales 164
Detección de servicios 165
Enrutamiento166
Equilibrio de carga 166
Escalado167
Reparación automática 168
Implementaciones sin tiempo de inactividad 169
Afinidad y reconocimiento de ubicación 170
Seguridad170
Comunicación segura e identidad de nodo criptográfica 171
Redes seguras y políticas de red 171
Control de acceso basado en roles (RBAC) 172
Secretos172

[v]
Contenido

Confianza en el contenido 173


Tiempo de actividad inverso 174
Introspección174
Información general de orquestadores populares 175
Kubernetes.175
Docker Swarm. 176
Microsoft Azure Kubernetes Service (AKS) 178
Apache Mesos y Marathon 178
Amazon ECS 179
Resumen180
Preguntas180
Lectura adicional 180
Capítulo 10: Orquestación de aplicaciones en contenedores
con Kubernetes 181
Requisitos técnicos 182
Arquitectura182
Nodos maestros de Kubernetes 185
Nodos del clúster 186
Minikube188
Compatibilidad de Kubernetes en Docker para el escritorio 190
Pods196
Comparación de la red de un contenedor Docker con un pod de Kubernetes 197
Compartir el espacio de nombres de red 198
Ciclo de vida de los pods 201
Especificación del pod 202
Pods y volúmenes 204
Conjunto de réplicas de Kubernetes 206
Especificación de ReplicaSet 207
Reparación automática 208
Implementación de Kubernetes 209
Servicio de Kubernetes 210
Enrutamiento basado en contexto 212
Resumen213
Preguntas213
Lectura adicional 214
Capítulo 11: Implementación, actualización y protección
de una aplicación con Kubernetes 215
Requisitos técnicos 216
Implementación de una primera aplicación 216

[ vi ]
Contenido

Implementación de un componente web 216


Implementación de la base de datos 220
Optimización de la implementación 225
Implementaciones sin tiempo de inactividad 226
Actualizaciones graduales. 227
Implementación blue-green 230
Secretos de kubernetes 235
Definición manual de secretos 235
Creación de secretos con kubectl 237
Utilización de secretos en un pod 237
Valores secretos en variables de entorno 240
Resumen 241
Preguntas 241
Lectura adicional 242
Capítulo 12: Ejecución de una aplicación en contenedor
desde el cloud 243
Requisitos técnicos 244
Creación de un clúster de Kubernetes completamente gestionado en Azure 244
Ejecución de la CLI de Azure 245
Grupos de recursos de Azure 247
Aprovisionamiento del clúster de Kubernetes 248
Envío de imágenes de Docker al registro de contenedores de Azure (ACR) 251
Creación de un ACR 252
Etiquetado y envío de imágenes de Docker 253
Configuración de la entidad principal del servicio 254
Implementar una aplicación en el clúster de Kubernetes 255
Escalado de la aplicación Pets 257
Escalar el número de instancias de aplicación 257
Escalar el número de nodos del clúster 258
Supervisión del clúster y la aplicación 260
Creación de un espacio de trabajo de análisis de registros 261
Supervisión del estado del contenedor 263
Visualización de los registros de los nodos maestros de Kubernetes 264
Visualización de los registros del contenedor y kublet 267
Actualización de la aplicación con cero interrupciones 272
Actualización de Kubernetes 273
Depuración de la aplicación mientras se ejecuta en AKS 275
Creación de un clúster de Kubernetes para desarrollo 275
Configuración del entorno 277
Implementación y ejecución de un servicio 278

[ vii ]
Contenido

Depuración remota de un servicio usando Visual Studio Code 280


Activación del desarrollo de estilo "editar y continuar" en el cloud 282
Limpieza 283
Resumen283
Preguntas 284
Lectura adicional 284
Evaluación 285
Capítulo 1: ¿Qué son los contenedores y por qué debo usarlos? 285
Capítulo 2: Configuración de un entorno de trabajo 286
Capítulo 3: Trabajar con contenedores 287
Capítulo 4: Creación y gestión de imágenes de contenedor 287
Capítulo 5: Administración de volúmenes y sistemas de datos 289
Capítulo 6: Arquitectura de aplicaciones distribuidas 290
Capítulo 7: Conexión en red con un solo host 291
Capítulo 8: Docker Compose 292
Capítulo 9: Orquestadores 293
Capítulo 10: Orquestación de aplicaciones en contenedores
con Kubernetes 294
Capítulo 11: Implementación, actualización y protección
de una aplicación con Kubernetes 295
Capítulo 12: Ejecución de una aplicación en contenedor desde el cloud 297
Otros libros que te podrían gustar 299

[ viii ]
Prefacio
Se dice que la inclusión de aplicaciones en contenedores es la mejor manera de
implementar DevOps, y el objetivo principal de este libro es ofrecer soluciones
de implementación integrales para tu entorno de Azure.

Al principio de este libro abordaremos la implementación y administración de


contenedores y nos familiarizaremos con Docker y Kubernetes. Posteriormente,
explicaremos las operaciones de administración y orquestación de contenedores en
Docker con las soluciones en el cloud de Azure. También aprenderás a implementar
y administrar aplicaciones de alta escalabilidad, además de a configurar un clúster
de Kubernetes listo para la producción en Azure en un entorno intacto. Por último,
el libro también te ayudará a usar las herramientas de Docker y Kubernetes de
Microsoft para crear aplicaciones que se puedan implementar rápidamente en Azure.

Al final del libro, podrás practicar con algunos temas más avanzados para
profundizar en tu conocimiento de Docker y Kubernetes.

A quién está destinado este libro


Si eres un desarrollador, administrador de sistemas o ingeniero de DevOps y quieres
usar Docker y Kubernetes para ejecutar tus aplicaciones críticas de forma escalable,
segura y con alta disponibilidad on-premises o en el cloud, este libro es para ti. Para
poder aprender de este libro, debes tener conocimientos básicos de Linux/Unix. Por
ejemplo, debes saber cómo instalar paquetes, editar archivos, administrar servicios, etc.
Además, si tienes experiencia básica en virtualización, tendrás una ventaja añadida.

[ ix ]
Prefacio

Temas tratados en este libro


Capítulo 1, ¿Qué son los contenedores y por qué debo usarlos?: en este capítulo nos
centramos en la cadena de suministro de software y la fricción que existe dentro
de ella. A continuación, presentamos los contenedores como un medio para
reducir esta fricción y añadir además medidas de seguridad de nivel empresarial.
Durante el capítulo, también indagaremos en cómo se conforman los contenedores
y el ecosistema que los rodea. En particular, destacaremos la distinción entre los
componentes ascendentes de OSS (Moby) que forman los elementos de los que
se componen los productos descendentes de Docker y otros proveedores.
Capítulo 2, Configuración de un entorno de trabajo: en este capítulo abordamos en detalle
cómo configurar un entorno ideal para desarrolladores, DevOps y operadores que
se pueda utilizar al trabajar con contenedores Docker.
Capítulo 3, Trabajar con contenedores: en este capítulo te enseñaremos a iniciar,
detener y eliminar contenedores. En el capítulo también te enseñamos a inspeccionar
los contenedores para recuperar metadatos adicionales. Además, explicamos
cómo ejecutar procesos adicionales o cómo asociarlos al proceso principal en un
contenedor que ya esté en ejecución. También te mostramos cómo recuperar desde
un contenedor información de inicio de sesión generada por los procesos que
se ejecutan en él. Por último, en el capítulo presentamos el funcionamiento interno
de un contenedor, incluidas cosas como los espacios de nombres y cgroups de Linux.
Capítulo 4, Creación y gestión de imágenes de contenedor: en este capítulo presentamos
las diferentes formas de crear imágenes de contenedor, que sirven de plantillas para
los contenedores. Presentamos la estructura interna de una imagen y cómo se crea.
Capítulo 5, Administración de volúmenes y sistemas de datos: en este capítulo presentamos
los volúmenes de datos que pueden utilizar los componentes con estado que se
ejecutan en contenedores. También introducimos los comandos de nivel de sistema
que se emplean para recopilar información sobre Docker y el sistema operativo
subyacente, además de los comandos para limpiar el sistema de recursos huérfanos.
Por último, explicamos los eventos del sistema generados por el motor de Docker.
Capítulo 6, Arquitectura de aplicaciones distribuidas: en este capítulo, introducimos el
concepto de una arquitectura de aplicaciones distribuidas y explicamos las diferentes
pautas y prácticas recomendadas que se requieren para ejecutar una aplicación
distribuida con éxito. Por último, abordaremos los requisitos adicionales que deben
cumplirse para ejecutar dicha aplicación en producción.
Capítulo 7, Conexión en red con un solo host: en este capítulo, explicaremos qué es el
modelo de red de los contenedores de Docker y su implementación de host único
en forma de red de puente. También hablaremos del concepto de "redes definidas
por software" y cómo se utilizan para proteger las aplicaciones en contenedores. Por
último, presentaremos cómo podemos abrir al público los puertos del contenedor y
hacer que los componentes en contenedores sean accesibles desde el mundo exterior.
[x]
Prefacio

Capítulo 8, Docker Compose: en este capítulo, explicaremos el concepto de aplicación


formada por varios servicios, cada uno ejecutándose en un contenedor, y de qué
forma Docker Compose nos permite crear, ejecutar y escalar una aplicación usando
un enfoque declarativo.
Capítulo 9, Orquestadores: en este capítulo introducimos el concepto de los
orquestadores. Aprenderás por qué los orquestadores son necesarios y cómo
funcionan. En el capítulo también ofrecemos información general sobre los
orquestadores más populares, así como algunas de sus ventajas e inconvenientes.
Capítulo 10, Orquestación de aplicaciones en contenedores con Kubernetes: en este capítulo
presentamos Kubernetes. Kubernetes es actualmente el líder indiscutible en el
espacio de la orquestación de contenedores. El capítulo comienza con una visión
general de la arquitectura de un clúster de Kubernetes y, a continuación, analizamos
los objetos principales utilizados en Kubernetes para definir y ejecutar aplicaciones
en contenedores.
Capítulo 11, Implementación, actualización y protección de una aplicación con Kubernetes:
en este capítulo te enseñaremos a implementar, actualizar y escalar aplicaciones en un
clúster de Kubernetes. También explicaremos cómo conseguir implementaciones sin
interrupciones para permitir las actualizaciones y la reversión de versiones anteriores
para las aplicaciones críticas. En este capítulo presentaremos también los secretos
de Kubernetes como forma de configurar servicios y proteger los datos confidenciales.
Capítulo 12, Ejecución de una aplicación en contenedor desde el cloud: en este capítulo
mostramos cómo implementar una aplicación compleja en contenedor en un clúster
de Kubernetes alojado en Microsoft Azure con el servicio Azure Kubernetes Service
(AKS). En primer lugar, explicamos cómo aprovisionar un clúster de Kubernetes,
en segundo lugar mostramos cómo alojar las imágenes de Azure Container Registry
y, finalmente, demostramos cómo implementar, ejecutar, supervisar, escalar y
actualizar la aplicación. En el capítulo también mostramos cómo actualizar
la versión de Kubernetes en el clúster sin causar tiempo de inactividad.

Para sacar el máximo partido a este libro


Idealmente, debes tener acceso a un portátil o un PC con Windows 10 Professional
o una versión de Mac OS X reciente instalada. También vale un equipo con cualquier
sistema operativo Linux instalado. Si tienes un Mac, deberías instalar Docker para
Mac, y si trabajas en Windows, debes instalar Docker para Windows. Puedes
descargarlos desde aquí: https://www.docker.com/community-edition.
Si utilizas una versión anterior de Windows o Windows 10 Home Edition,
debes tener instalado Docker Toolbox. Puedes encontrar Docker Toolbox aquí:
https://docs.docker.com/toolbox/toolbox_install_windows/.

[ xi ]
Prefacio

Para probar los comandos que vas a aprender, utiliza la aplicación Terminal en Mac
y una consola de PowerShell en Windows. También necesitas una versión reciente
de un navegador como Google Chrome, Safari o Internet Explorer. Y, obviamente,
necesitarás acceso a Internet para descargar las herramientas y las imágenes de
contenedor que vamos a usar y explicar en este libro.

Para seguir el Capítulo 12, Ejecución de una aplicación en contenedor desde el cloud,
necesitas tener acceso a Microsoft Azure. Si aún no tienes una cuenta de Azure,
puedes solicitar una cuenta de prueba aquí, en https://azure.microsoft.com/
en-us/free/.

Descargar los archivos EPUB/mobi y los


archivos de código de ejemplo
Puedes descargar gratis una versión en EPUB o mobi de este libro en Github.
Puedes descargarlas, además del paquete de código, en https://github.com/
PacktPublishing/Containerize-your-Apps-with-Docker-and-Kubernetes.

Puedes descargar los archivos del código de ejemplo de este libro desde tu cuenta
en http://www.packtpub.com. Si has comprado este libro en otro sitio, puedes
visitar http://www.packtpub.com/support y registrarte para que se te envíen los
archivos directamente por correo electrónico.

Puedes descargar los archivos de código siguiendo estos pasos:

1. Inicia sesión o regístrate en http://www.packtpub.com.


2. Selecciona la pestaña SOPORTE.
3. Haz clic en Descargas de código y erratas.
4. Introduce el nombre del libro en la casilla de Búsqueda y sigue las
instrucciones en pantalla.

Tras descargar el archivo, asegúrate de descomprimir o extraer la carpeta con


la última versión de:

• WinRAR/7-Zip para Windows


• Zipeg/iZip/UnRarX para Mac
• 7-Zip/PeaZip para Linux

[ xii ]
Prefacio

El paquete de código del libro está alojado en GitHub, en https://github.com/


appswithdockerandkubernetes/labs.

También tenemos otros paquetes de código de nuestro rico catálogo de libros


y vídeos disponibles en https://github.com/PacktPublishing/. ¡Échales
un vistazo!

Descargar las imágenes en color


También te ofrecemos un archivo PDF que tiene imágenes en color de las capturas
de pantalla/diagramas que se utilizan en este libro. Puedes descargarlo en
https://www.packtpub.com/sites/default/files/downloads/9781789610369_
ColorImages.pdf.

Convenciones utilizadas
Hay una serie de convenciones de texto que se utilizan a lo largo de este libro.

CodeInText: indica palabras de código en el texto, nombres de tablas de bases de


datos, nombres de carpetas, nombres de archivos, extensiones de archivo, rutas, URL
ficticias, entradas de usuario y alias de Twitter. Por ejemplo, "El contenido de cada
capa se asigna a una carpeta especial del sistema host, que suele ser una subcarpeta
de /var/lib/docker/".

Un bloque de código aparece de la siguiente forma:


COPY . /app
COPY ./web /app/web
COPY sample.txt /data/my-sample.txt
ADD sample.tar /app/bin/
ADD http://example.com/sample.txt /data/

Cuando queremos llamar tu atención sobre una parte específica de un bloque


de código, las líneas o elementos pertinentes se marcan en negrita:
FROM python:2.7
RUN mkdir -p /app
WORKDIR /app
COPY ./requirements.txt /app/
RUN pip install -r requirements.txt
CMD ["python", "main.py"]

[ xiii ]
Prefacio

Una entrada o resultado de la línea de comandos se escribe así:


az group create --name pets-group --location westeurope

Negrita: indica un nuevo término, una palabra importante o palabras que aparecen
en la pantalla. Por ejemplo, los menús o cuadros de diálogo aparecen en el texto en
este formato. Este es un ejemplo: "Selecciona Información del sistema en el panel
Administración".

Las advertencias o las notas importantes aparecen en


un cuadro como este.

Los consejos y trucos aparecen de esta forma.

Habla con nosotros


Los comentarios de nuestros lectores siempre son bienvenidos.

Comentarios generales: envía un correo electrónico a [email protected]


mencionando el título del libro en el asunto de tu mensaje. Si tienes preguntas
acerca de cualquier aspecto de este libro, envíanos un correo electrónico a
[email protected].

Erratas: aunque hemos tenido muchísimo cuidado para asegurarnos de que


nuestro contenido sea correcto, siempre se producen errores. Si has encontrado
un error en este libro, te agradeceríamos que nos informaras de ello. Visita
http://www.packtpub.com/submit-errata, selecciona el libro, haz clic en
el enlace del formulario de aviso de erratas y escribe los detalles.

Piratería: si te encuentras con copias ilegales de nuestros trabajos en cualquier


formato en Internet, te rogamos que nos indiques la dirección o el nombre del sitio
web inmediatamente. Ponte en contacto con nosotros en [email protected]
con un enlace al material.

Si te interesa convertirte en autor: si hay un tema en el que tengas experiencia


y te interesa escribir o contribuir a un libro, visita http://authors.packtpub.com.

[ xiv ]
Prefacio

Reseñas
Nos encantaría que nos dejaras una reseña. Tras haber leído y utilizado este libro,
¿por qué no nos dejas una reseña en el sitio en el que lo hayas comprado? Los
lectores potenciales pueden ver tu opinión imparcial y realizar decisiones de compra
en función de ella, nosotros en Packt podemos entender qué opinas de nuestros
productos y nuestros autores podrán ver tus comentarios sobre su libro. ¡Muchas
gracias!

Para obtener más información acerca de Packt, visita packtpub.com.

[ xv ]
¿Qué son los contenedores
y por qué debo usarlos?
En el primer capítulo de este libro se te presentará el mundo de los contenedores
y su orquestación. En este libro, suponemos que no tienes ningún conocimiento
previo sobre este ámbito, por lo que ofrecemos una introducción muy práctica al tema.

En este capítulo, nos centramos en la cadena de suministro del software y la fricción


que existe en ella. A continuación, presentamos los contenedores como un medio para
reducir esta fricción y añadir además medidas de seguridad de nivel empresarial.
Durante el capítulo, también indagaremos en cómo se conforman los contenedores y el
ecosistema que los rodea. En particular, destacaremos la distinción entre los componentes
ascendentes de Operations Support System (OSS), unificados bajo el nombre en código
Moby, que forman los elementos de los que se componen los productos descendentes
de Docker y otros proveedores.

En el capítulo, abordaremos los siguientes temas:

• ¿Qué son los contenedores?


• ¿Por qué son importantes los contenedores?
• ¿Cuál es la ventaja para mí o para mi empresa?
• El proyecto Moby
• Productos de Docker
• El ecosistema de contenedores
• Arquitectura de contenedores

[1]
¿Qué son los contenedores y por qué debo usarlos?

Tras completar el módulo, serás capaz de:

• Explicar en unas cuantas frases sencillas a un lego en la materia qué son los
contenedores, utilizando una analogía como la de los contenedores físicos.
• Justificar ante un lego en la materia por qué son tan importantes los
contenedores, utilizando una analogía como la de los contenedores físicos
en comparación con los envíos tradicionales, o la de los apartamentos
en comparación con las casas unifamiliares, etc.
• Nombrar al menos cuatro componentes ascendentes de código abierto
que utilizan los productos de Docker, como Docker para Mac/Windows.
• Identificar al menos tres productos de Docker.

Requisitos técnicos
Este capítulo es una introducción teórica al tema. Por lo tanto, no existen requisitos
técnicos especiales para este capítulo.

¿Qué son los contenedores?


Un contenedor de software es una cosa bastante abstracta y, por lo tanto, podría
ayudarnos empezar con una analogía que debería resultar bastante familiar a la mayoría
de los lectores. La analogía es con un contenedor para envíos en el sector del transporte.

Transportamos grandes cantidades de mercancías en trenes, barcos y camiones.


Las descargamos en los lugares de destino, que pueden ser otro medio de transporte.
Habitualmente, las mercancías son diversas y difíciles de gestionar. Antes de la invención
de los contenedores de envío, la descarga de mercancías de un medio de transporte
y su carga en otro era un proceso muy complejo y tedioso. Vamos a poner el ejemplo
de un agricultor que lleva un carro lleno de manzanas a una estación de tren central,
en la que las manzanas se cargan en los vagones, junto con las manzanas de muchos
otros productores. O de un fabricante de vino que lleva sus barricas en camión a un
puerto, donde se descargarán y se transferirán a un barco que llevará las barricas a
otro país. Cada tipo de mercancía tenía un empaquetado propio y distinto, y por tanto
debía tratarse de una manera determinada. Los productos que estuvieran sueltos
corrían el riesgo de resultar dañados o perderse en el proceso. Entonces aparecieron los
contenedores y revolucionaron por completo el sector del transporte.

[2]
Capítulo 1

Un contenedor no es más que una caja metálica con dimensiones estandarizadas. La longitud,
anchura y altura de cada contenedor es la misma. Esto es muy importante. Si no existiera un
acuerdo general para estandarizar los tamaños, los contenedores de envío no habrían tenido
tanto éxito. Hoy en día, las empresas que quieren transportar sus mercancías del punto A
al B las colocan en estos contenedores estandarizados. Una vez organizada la mercancía en
contenedores, llaman a un transportista, que utiliza métodos estandarizados. Estos métodos
pueden ser camiones diseñados para cargar esos contenedores o trenes cuyos vagones
pueden transportar uno o varios contenedores. Además, también hay barcos especializados
en el transporte de cantidades inmensas de contenedores. Los transportistas no necesitan
desempaquetar ni volver a empaquetar la mercancía. Para un transportista, el contenedor no
es más que una caja opaca: no están interesados en lo que contienen, ni debería preocuparles
(en la mayoría de los casos). No es más que una gran caja metálica con dimensiones
estandarizadas. La organización de mercancías en contenedores es una tarea reservada
totalmente a las partes que quieran enviar sus bienes, que son quienes mejor saben cómo
manejar y envasar dichos bienes. Dado que todos los contenedores tienen la misma forma y
dimensiones estandarizadas, los transportistas pueden utilizar herramientas estandarizadas
para manipular los contenedores, es decir, grúas que, por ejemplo, los descargan de un
tren o un camión y los cargan en un buque o viceversa. Un tipo de grúa es suficiente para
cargar y descargar todos los contenedores que haya que gestionar a lo largo del tiempo. Los
medios de transporte también pueden estar estandarizados, como los barcos, camiones o
trenes que transportan contenedores. Gracias a toda esta estandarización, todos los procesos
relacionados con el transporte de mercancías se pudieron también homogeneizar y, por tanto,
acabaron siendo mucho más eficientes de lo que lo eran en la época anterior.

Creo que ahora ya deberías tener una buena idea de por qué los contenedores de envío son
tan importantes y por qué revolucionaron todo el sector del transporte. He elegido esta
analogía porque los contenedores de software que vamos a estudiar cumplen exactamente
la misma función en la cadena de suministro del software que desempeñan los contenedores
de envío en la cadena de suministro de mercancías tangibles.

Vamos a hablar de lo que solían hacer los desarrolladores al crear una nueva aplicación.
Cuando los desarrolladores consideraban que una aplicación estaba ya terminada, se
la entregaban a los ingenieros de operaciones, que debían instalarla en los servidores
de producción y ejecutarla. Si los ingenieros de operaciones tenían suerte, recibían
incluso un documento con instrucciones de instalación precisas de los desarrolladores.
Hasta aquí, todo iba bien; las cosas eran sencillas. Sin embargo, las cosas se iban un
poco de madre cuando en una empresa había muchos equipos de desarrolladores que
creaban tipos diferentes de aplicaciones pero todas debían instalarse y ejecutarse en los
mismos servidores de producción. Por lo general, todas las aplicaciones tienen algunas
dependencias externas, como la plataforma en la que se compilaron, las bibliotecas que
utilizan, etc.

[3]
¿Qué son los contenedores y por qué debo usarlos?

A veces, dos aplicaciones usaban la misma plataforma, pero en diferentes versiones, que
podrían ser o no ser compatibles entre sí. Las vidas de nuestros ingenieros de operaciones
fueron complicándose a lo largo del tiempo. Tenían que ser realmente creativos a la
hora de decidir cómo cargar sus servidores, o su "barco", con distintas aplicaciones y sin
desbaratar nada. La instalación de una nueva versión de una aplicación determinada
era un proyecto complejo en sí mismo y, a menudo, requería meses de planificación y
pruebas. En otras palabras, existía mucha fricción en la cadena de suministro de software.
Sin embargo, hoy en día, las empresas dependen cada vez más del software y los ciclos
de lanzamiento son cada vez más cortos. Ya no podemos permitirnos tener una nueva
versión, por ejemplo, solo dos veces al año. Las aplicaciones tienen que actualizarse en
cuestión de semanas o días, o a veces incluso varias veces al día. Las empresas que no
lo hacen corren el riesgo de fracasar debido a su falta de agilidad. Entonces, ¿cuál es la
solución?

Un primer enfoque consistió en utilizar máquinas virtuales (MV). En lugar de ejecutar


varias aplicaciones en el mismo servidor, las empresas empaquetaban y ejecutaban
una sola aplicación por MV. Así, se resolvían los problemas de compatibilidad y todo
volvía a tener buena pinta. Lamentablemente, la felicidad no duró mucho. Las MV son
muy aparatosas, ya que contienen todo un sistema operativo, como Linux o Windows
Server, solo para una única aplicación. Esto es como si en el sector del transporte se
utilizara un barco gigantesco solo para transportar un camión cargado con plátanos.
¡Qué desperdicio! Es imposible que eso sea rentable. La solución definitiva al problema
consistía en proporcionar algo mucho más ligero que las MV, pero que también fuera
capaz de encapsular perfectamente los productos que necesitaba transportar. En este
caso, las mercancías son la aplicación en sí, escrita por los desarrolladores, además
(y esto es importante) de todas las dependencias externas de la aplicación, como la
plataforma, las bibliotecas, las configuraciones, etc. Este Santo Grial de los mecanismos
de empaquetado de software era el contenedor Docker.

Los desarrolladores utilizan contenedores Docker para empaquetar sus aplicaciones,


plataformas y bibliotecas, y luego envían esos contenedores a los evaluadores o a
los ingenieros de operaciones. Para los evaluadores y los ingenieros de operaciones,
el contenedor no es más que una caja opaca. Lo importante es que es una caja opaca
estandarizada. Todos los contenedores, independientemente de qué aplicación se
ejecute en ellos, se pueden tratar de la misma forma. Los ingenieros saben que si un
contenedor dado se puede ejecutar en sus servidores, cualquier otro contenedor podrá
ejecutarse también. Y esto es realmente cierto, a excepción de algunos casos marginales,
que siempre existen. Por tanto, los contenedores Docker son una forma de empaquetar
aplicaciones y sus dependencias de forma estandarizada. Fue entonces cuando Docker
acuñó el lema "build, ship and run anywhere" (compilar, distribuir y ejecutar en
cualquier lugar).

[4]
Capítulo 1

¿Por qué son importantes los


contenedores?
En la actualidad, el tiempo que transcurre entre las nuevas versiones de una aplicación es
cada vez menor, pero el software en sí no se vuelve más sencillo. Más bien al contrario:
la complejidad de los proyectos de software aumenta. Por eso necesitamos una forma de
domar a esta bestia y simplificar la cadena de suministro de software.

Mejorar la seguridad
Día tras día, no dejamos de oír lo mucho que están aumentando los delitos online.
Muchas empresas conocidas se ven afectadas por brechas de seguridad. Se producen
robos de datos confidenciales de los clientes, como números de seguridad social,
información de tarjetas de crédito, etc. Y no son solo los datos de los clientes los que están
en riesgo: también se roban secretos empresariales.

Los contenedores pueden ayudar de muchas maneras. En primer lugar, Gartner ha


desvelado en un informe reciente que las aplicaciones que se ejecutan en un contenedor
son más seguras que las que no. Los contenedores utilizan primitivos de seguridad
de Linux, como los espacios de nombres del kernel de Linux, para aislar en entornos
"sandbox" distintas aplicaciones que se ejecutan en los mismos equipos o grupos de
control (cgroups), con el fin de evitar el problema del "vecino ruidoso": una aplicación
defectuosa utiliza todos los recursos disponibles de un servidor y desabastece a todas las
demás.

Debido al hecho de que las imágenes de contenedor son inmutables, es fácil analizarlas
en busca de vulnerabilidades y exposiciones conocidas y, al hacerlo, aumentar la
seguridad de las aplicaciones en general.

Otra manera en que podemos hacer que nuestra cadena de suministro de software sea
más segura cuando usamos contenedores es recurrir a la confianza en el contenido. La
confianza en el contenido garantiza fundamentalmente que el autor de una imagen del
contenedor es quien dice ser y que el consumidor de la imagen del contenedor tiene la
garantía de que la imagen no ha sido manipulada en tránsito. Esto último se conoce como
un ataque man-in-the-middle (MITM).

Todo esto que acabo de mencionar es obviamente posible también sin usar contenedores,
pero ya que los contenedores han introducido un estándar globalmente aceptado, hace que
sea mucho más fácil implementar y hacer cumplir las prácticas recomendadas.

Sin embargo, la seguridad no es el único motivo por el que los contenedores son
importantes. Existen otras razones, como se explica en las dos siguientes secciones.

[5]
¿Qué son los contenedores y por qué debo usarlos?

Simulación de entornos de producción


Una de estas razones es el hecho de que los contenedores hacen que sea fácil simular
un entorno de producción, incluso en el portátil de un desarrollador. Si podemos colocar
cualquier aplicación en un contenedor, también podemos hacerlo con, por ejemplo, una
base de datos, como Oracle o MS SQL Server. Cualquiera que haya tenido que instalar
una base de datos de Oracle en un equipo sabe que no es lo más fácil del mundo, y que
ocupa mucho espacio en cualquier ordenador. Es algo que no conviene hacer en el
portátil que usas para el desarrollo, simplemente para probar si la aplicación que has
desarrollado funciona de forma integral. Si utilizamos los contenedores, podemos
ejecutar una base de datos relacional completa en un contenedor con muchísima
facilidad. Y cuando hayamos terminado las pruebas, podemos eliminar el contenedor,
y la base de datos desaparecerá sin dejar ningún rastro en mi equipo.

Dado que los contenedores son muy ligeros en comparación con las MV, no es raro tener
muchos contenedores ejecutándose al mismo tiempo en el portátil de un desarrollador
sin desbordar sus capacidades.

Estandarización de infraestructuras
Un tercer motivo por el que los contenedores son importantes es porque los operadores
pueden por fin centrarse en aquello que se les da realmente bien: aprovisionar la
infraestructura, y ejecutar y supervisar las aplicaciones en producción. Cuando las aplicaciones
que se tienen que ejecutar en un sistema de producción están todas en contenedores, los
operadores pueden empezar a estandarizar su infraestructura. Cada servidor se convierte
en otro host de Docker. No es necesario instalar bibliotecas ni plataformas especiales en esos
servidores, solo un sistema operativo y un runtime de contenedor como Docker.

Además, los operadores no tienen por qué tener ningún conocimiento exhaustivo
acerca de los detalles internos de las aplicaciones, ya que esas aplicaciones se ejecutan
de forma autónoma en contenedores que deben parecer cajas opacas a los ingenieros
de operaciones, de forma similar al aspecto que tienen los contenedores para el personal
del sector del transporte.

¿Cuál es la ventaja para mí o para


mi empresa?
Alguien dijo una vez que hoy en día, cualquier empresa que tenga cierto tamaño tiene
que reconocer que debe ser una empresa de software. El software es el que dirige
cualquier empresa, irremediablemente. Como cualquier empresa es una empresa
de software, existe la necesidad de establecer una cadena de suministro de software.
Para que la empresa siga siendo competitiva, su cadena de suministro de software tiene
que ser segura y eficiente. La eficiencia se puede lograr mediante una automatización
y estandarización minuciosas. Pero los contenedores han demostrado ser superiores
en los tres ámbitos: seguridad, automatización y estandarización.

[6]
Capítulo 1

Las empresas grandes y conocidas han reconocido que, al incluir en contenedores las
aplicaciones heredadas existentes (a los que muchos llaman aplicaciones tradicionales)
y establecer una cadena de suministro de software totalmente automatizada basada
en contenedores, pueden reducir el coste dedicado al mantenimiento de las aplicaciones
esenciales en un factor de entre el 50 y el 60 % y pueden reducir el tiempo entre las
nuevas versiones de estas aplicaciones tradicionales hasta en un 90 %.

Dicho esto, la adopción de la tecnología de contenedores ahorra a estas empresas


un montón de dinero y, al mismo tiempo, acelera el proceso de desarrollo y reduce
el tiempo de comercialización.

El proyecto Moby
Originalmente, cuando la empresa Docker introdujo sus contenedores, todo era
de código abierto. Docker no tenía ningún producto comercial en ese momento.
El motor de Docker que desarrolló la empresa era un software monolítico. Contenía
muchas partes lógicas, como el runtime de contenedor, una biblioteca de red, una API
RESTful, una interfaz de línea de comandos y mucho más.

Otros proveedores o proyectos como Red Hat o Kubernetes usaban el motor


de Docker en sus propios productos, pero la mayoría de las veces solo usaban parte
de su funcionalidad. Por ejemplo, Kubernetes no utilizaba la biblioteca de red de Docker
del motor de Docker, sino que ofrecía sus propias funciones de red. Red Hat, por su
parte, no actualizaba el motor de Docker a menudo y prefería aplicar parches no oficiales
a versiones anteriores del motor de Docker; aun así, lo llamaban el motor de Docker.

A raíz de todos estos motivos, además de muchos otros, surgió la idea de que Docker
debía hacer algo para distinguir claramente la parte de código abierto de Docker de
su parte comercial. Además, la empresa quería evitar que la competencia utilizara el
nombre Docker y usara su nombre para beneficio propio. Esta fue la razón principal por
la que nació el proyecto Moby. Sirve como paraguas para la mayoría de los componentes
de código abierto que Docker desarrolló y sigue desarrollando. Estos proyectos de código
abierto ya no llevan el nombre de Docker.

El proyecto Moby engloba componentes para la administración de imágenes,


la administración de secretos, la administración de configuración, las funciones
de red y el aprovisionamiento, por nombrar solo algunos. Además, en el proyecto
Moby existen herramientas especiales de Moby que, por ejemplo, se utilizan para
ensamblar componentes en artefactos ejecutables.

[7]
¿Qué son los contenedores y por qué debo usarlos?

Algunos de los componentes que técnicamente pertenecerían al proyecto Moby han sido
donados por Docker a la Cloud Native Computing Foundation (CNCF) y, por lo tanto,
ya no aparecen en la lista de componentes. Los principales son containerd y runc, que
en conjunto forman el runtime de contenedor.

Productos de Docker
En la actualidad, Docker divide sus líneas de productos en dos segmentos. Está la
Community Edition (CE), que es código cerrado pero totalmente gratuito, y después está
la Enterprise Edition (EE), que también es de código cerrado y requiere una licencia anual.
Los productos Enterprise tienen un servicio de soporte 24 horas, 7 días a la semana, y un
servicio de resolución de errores durante mucho más tiempo que los productos de la CE.

Docker CE
La Community Edition de Docker incluye productos como Docker Toolbox,
Docker para Mac y Docker para Windows. Estos tres productos están pensados
principalmente para los desarrolladores.

Docker para Mac y Docker para Windows son aplicaciones de escritorio fáciles
de instalar que se pueden utilizar para crear, depurar y probar aplicaciones o servicios
"dockerizados" en un Mac o un Windows. Docker para Mac y Docker para Windows
son entornos de desarrollo completos que se integran profundamente con su respectiva
plataforma de hipervisor, funciones de red y sistema de archivos. Estas herramientas
ofrecen la forma más rápida y fiable de ejecutar Docker en un Mac o en Windows.

Bajo el paraguas de la CE, hay también dos productos que están más orientados a los
ingenieros de operaciones. Esos productos son Docker para Azure y Docker para AWS.

Por ejemplo, con Docker para Azure, que es una aplicación nativa de Azure, es posible
configurar Docker en unos pocos clics, optimizarlo e integrarlo en los servicios de Azure
de infraestructura como servicio (IaaS) subyacentes. Ayuda a los ingenieros de
operaciones a acelerar el tiempo que se tarda en compilar y ejecutar aplicaciones
de Docker en Azure.

Docker para AWS funciona de manera muy similar, pero para el cloud de Amazon.

[8]
Capítulo 1

Docker EE
La EE de Docker consta de los dos productos Universal Control Plane (UCP) y Docker
Trusted Registry (DTR) que se ejecutan sobre Docker Swarm. Ambos son aplicaciones
Swarm. Docker EE se basa en los componentes ascendentes del proyecto Moby y añade
funcionalidades de nivel empresarial como el control de acceso basado en roles (RBAC),
compatibilidad con varios inquilinos, clústeres mixtos de Docker Swarm y Kubernetes,
interfaz de usuario basada en la web y confianza en el contenido, así como el análisis
de imágenes.

El ecosistema de contenedores
Jamás ha habido una nueva tecnología que se haya introducido en el entorno informático
y haya penetrado tan a fondo como los contenedores. Las empresas que no quieran
quedarse atrás no pueden pasar por alto los contenedores. Este enorme interés por
los contenedores procedente de todos los sectores de la industria ha generado muchas
innovaciones en este sector. Hay muchas empresas que se han especializado en los
contenedores y, o bien ofrecen productos basados en esta tecnología, o bien crean
herramientas que le dan soporte.

En un principio, Docker no tenía ninguna solución para la orquestación de contenedores,


por lo que otras empresas o proyectos, fueran o no de código abierto, intentaron cubrir
esta carencia. El principal de ellos es Kubernetes, que inició Google y después donó
a la CNCF. Hay otros productos de orquestación de contenedores como Apache Mesos,
Rancher, Red Hat's Open Shift, el propio Docker Swarm y muchos más.

La última tendencia apunta a la malla de servicio. Esta es la nueva palabra de moda.


A medida que vamos incluyendo cada vez más aplicaciones en contenedores y las
reformulamos para que estén más orientadas a microservicios, nos encontramos con
problemas que el software de orquestación sencillo ya no puede solucionar de forma fiable y
escalable. Los temas de este ámbito son la detección, la supervisión y el rastreo de servicios,
y la agregación de registros. Hay muchos proyectos nuevos que han surgido en este ámbito,
el más popular de los cuales es ahora Istio, que también forma parte de la CNCF.

Mucha gente opina que el siguiente paso en la evolución del software son las funciones,
o más precisamente las funciones como servicio (FaaS). Existen algunos proyectos que
proporcionan exactamente este tipo de servicio y se basan en contenedores. Un ejemplo
importante de ellos es OpenFaaS.

[9]
¿Qué son los contenedores y por qué debo usarlos?

No hemos hecho más que arañar la superficie del ecosistema de los contenedores.
Todas las grandes empresas informáticas, como Google, Microsoft, Intel, Red Hat, IBM
y muchas más, están trabajando insistentemente en los contenedores y las tecnologías
relacionadas. La CNCF, que trabaja principalmente en el ámbito de los contenedores
y tecnologías afines, tiene tantos proyectos registrados que ya no caben en un póster.
Es un momento emocionante para trabajar en este campo. Y, en mi humilde opinión,
esto es solo el principio.

Arquitectura de contenedores
Bien, ahora vamos a explicar en grandes líneas cómo se diseña un sistema que pueda
ejecutar contenedores Docker. El siguiente diagrama ilustra el aspecto de un equipo
en el que se ha instalado Docker. Por cierto: un equipo que tiene instalado Docker
se suele llamar un host de Docker, porque puede ejecutar o alojar contenedores Docker:

Diagrama de arquitectura general del motor de Docker

En el diagrama anterior, vemos tres partes esenciales:

• En la parte inferior, tenemos el sistema operativo Linux


• En la parte intermedia, en gris oscuro, tenemos el runtime de contenedor
• En la parte superior, tenemos el motor de Docker

[ 10 ]
Capítulo 1

Los contenedores solo son posibles debido al hecho de que el sistema operativo Linux
proporciona algunos elementos primitivos, como los espacios de nombres, los grupos
de control, las funciones de capa, etc., que el runtime de contenedor y el motor de Docker
utilizan de manera muy específica. Los espacios de nombres del kernel de Linux, como
los espacios de nombres de ID de proceso (pid) o los espacios de nombres de red (net),
permiten a Docker encapsular o aislar en entornos "sandbox" procesos que se ejecutan
dentro del contenedor. Los grupos de control garantizan que los contenedores no
experimenten el síndrome del "vecino ruidoso", en el que una única aplicación que se
ejecuta en un contenedor puede consumir la mayor parte o la totalidad de los recursos
disponibles para todo el host de Docker. Los grupos de control permiten a Docker limitar
los recursos, como el tiempo de la CPU o la cantidad de RAM que se asigna a cada
contenedor como máximo.

El runtime de contenedor en un host de Docker consta de containerd y de runc. runc


es la funcionalidad de bajo nivel del runtime de contenedor, y containerd, que se basa
en runc, ofrece funcionalidad de alto nivel. Ambos son de código abierto y Docker los
ha donado a la CNCF.

El runtime de contenedor es el responsable de todo el ciclo de vida del contenedor. Si es


necesario, extrae una imagen de contenedor (que es la plantilla del contenedor) desde
un registro, crea un contenedor desde dicha imagen, inicializa y ejecuta el contenedor y,
por último, detiene y elimina el contenedor del sistema cuando se le solicita.

El motor de Docker proporciona funcionalidades adicionales además del runtime


de contenedor, como bibliotecas de red o compatibilidad con complementos. También
incluye una interfaz REST sobre la cual se pueden automatizar todas las operaciones del
contenedor. La interfaz de línea de comandos de Docker que usaremos frecuentemente
en este libro es uno de los consumidores de la interfaz REST.

Resumen
En este capítulo, hemos analizado cómo los contenedores pueden reducir inmensamente
la fricción en la cadena de suministro del software y, además, hacer que dicha cadena
de suministro sea mucho más segura.

En el próximo capítulo, nos familiarizaremos con los contenedores. Aprenderemos


a ejecutar, detener y eliminar contenedores, y a manipularlos para otros fines. También
veremos una descripción general de la anatomía de un contenedor. Vamos a ponernos
realmente manos a la obra y a jugar con estos contenedores por primera vez, así que
estad atentos.

[ 11 ]
¿Qué son los contenedores y por qué debo usarlos?

Preguntas
Responde a las siguientes preguntas para evaluar tus conocimientos:

1. ¿Qué afirmaciones son correctas (varias opciones pueden ser correctas)?


1. Un contenedor es una especie de MV pero más ligera
2. Un contenedor solo se puede ejecutar en un host Linux
3. Un contenedor solo puede ejecutar un proceso
4. El proceso principal en un contenedor siempre tiene PID 1
5. Un contenedor consiste en uno o varios procesos encapsulados
por espacios de nombres de Linux y restringidos por cgroups
2. Explica a un lego en la materia, con tus propias palabras, qué es un contenedor
(puedes utilizar analogías).
3. ¿Por qué se considera que los contenedores son algo revolucionario
en el panorama informático? Menciona tres o cuatro motivos.
4. ¿Qué queremos decir cuando afirmamos que si un contenedor se ejecuta en una
plataforma determinada, entonces se puede ejecutar en cualquier lugar? Justifica
esto con dos o tres motivos.
5. Verdadero o falso: los contenedores Docker solo son realmente útiles para las
aplicaciones modernas de nueva implementación y basadas en microservicios. Justifica
tu respuesta.
6. ¿Cuánto suele ahorrar una empresa media al incluir sus aplicaciones heredadas
en contenedores?
1. 20%
2. 33%
3. 50%
4. 75%

7. ¿En qué dos conceptos principales de Linux se basan los contenedores?

[ 12 ]
Capítulo 1

Lectura adicional
Aquí tienes una lista de enlaces que te dirigen a información más detallada sobre temas
que hemos explicado (el contenido puede estar en inglés)

• Introducción a Docker en https://docs.docker.com/engine/docker-overview/


• El proyecto Moby en https://mobyproject.org/
• Productos de Docker en https://www.docker.com/get-docker
• Cloud Native Computing Foundation en https://www.cncf.io/
• containerd: el runtime de contenedor estándar del sector en https://containerd.io/

[ 13 ]
Configuración de un entorno
de trabajo
En el último capítulo, explicamos qué son los contenedores Docker y por qué son
importantes. Conocimos los tipos de problemas que solucionan los contenedores en una
cadena de suministro de software moderna.

En este capítulo, vamos a preparar nuestro entorno de trabajo o personal para trabajar
de una forma eficiente y eficaz con Docker. Analizaremos en detalle cómo configurar
un entorno ideal para desarrolladores, DevOps y operadores que se pueda utilizar para
trabajar con contenedores Docker.

En este capítulo, abordaremos los siguientes temas:

• El shell de comandos de Linux


• PowerShell para Windows
• Uso de un administrador de paquetes
• Selección de un editor de código
• Docker Toolbox
• Docker para macOS y Docker para Windows
• Minikube
• Clonación del repositorio de código fuente

[ 15 ]
Configuración de un entorno de trabajo

Después de terminar este capítulo, serás capaz de hacer lo siguiente:

• Utilizar un editor en el ordenador portátil que sea capaz de editar archivos


simples, como un Dockerfile o un archivo docker-compose.yml
• Utilizar un shell como Bash en macOS y PowerShell en Windows para ejecutar
comandos de Docker y realizar otras operaciones sencillas, como navegar por
la estructura de carpetas o crear una carpeta nueva
• Instalar Docker para macOS o Docker para Windows en el equipo
• Ejecutar comandos de Docker simples, como docker version o docker
container run, en Docker para macOS o Docker para Windows
• Instalar correctamente Docker Toolbox en el equipo
• Utilizar docker-machine para crear un host de Docker en VirtualBox
• Configurar la CLI local de Docker para acceder de manera remota a un host
de Docker que se ejecuta en VirtualBox

Requisitos técnicos
Para este capítulo, deberás tener instalado macOS o Windows, preferiblemente
Windows 10 Professional. También debes tener acceso gratuito a Internet para
descargar aplicaciones y el permiso para instalar esas aplicaciones en tu portátil.

El shell de comandos de Linux


Los contenedores Docker se desarrollaron por primera vez en Linux para Linux.
Por lo tanto, es natural que la herramienta de línea de comandos principal que se
utiliza para trabajar con Docker, que también se denomina shell, sea un shell de Unix
(recuerda que Linux procede de Unix). La mayoría de los desarrolladores utilizan el shell
Bash. En algunas distribuciones ligeras de Linux, como Alpine, Bash no está instalado
y, por lo tanto, hay que usar el shell Bourne más sencillo, que se denomina sh. Siempre
que trabajemos en un entorno Linux, como en un contenedor o en una MV de Linux,
utilizaremos /bin/bash o /bin/sh, dependiendo de su disponibilidad.

Aunque macOS X no es un sistema operativo Linux, Linux y OS X son variantes de Unix y,


por lo tanto, admiten los mismos tipos de herramientas. Entre esas herramientas están los
shell. Por lo tanto, cuando trabajes en un macOS, es probable que utilices el shell Bash.

[ 16 ]
Capítulo 2

En este libro, esperamos que los lectores estén familiarizados con los comandos de scripts
más básicos de Bash y, si trabajan en Windows, de PowerShell. Si estás empezando desde
cero, te recomendamos encarecidamente que te familiarices con las siguientes hojas
de referencia rápida:

• Linux Command Line Cheat Sheet de Dave Child en http://bit.ly/2mTQr8l


• PowerShell Basic Cheat Sheet en http://bit.ly/2EPHxze

PowerShell para Windows


En un equipo de sobremesa, ordenador portátil o servidor Windows hay disponibles
varias herramientas de línea de comandos. La más conocida es el shell de comandos, que
lleva décadas disponible en los equipos Windows. Se trata de un shell muy sencillo. Para
crear scripts más avanzados, Microsoft ha desarrollado PowerShell. PowerShell es muy
potente y muy popular entre los ingenieros que trabajan en Windows. Por último,
en Windows 10, tenemos el denominado subsistema de Windows para Linux, que nos
permite utilizar cualquier herramienta de Linux, como los shells Bash o Bourne. Aparte
de esto, también existen otras herramientas que instalan un shell Bash en Windows como,
por ejemplo, el shell Bash de Git. En este libro, todos los comandos usarán la sintaxis
Bash. La mayoría de los comandos también se ejecutan en PowerShell.

Por lo tanto, te recomendamos que utilices PowerShell o cualquier otra herramienta


Bash para trabajar con Docker en Windows.

Uso de un administrador de paquetes


La forma más fácil de instalar software en un ordenador portátil macOS o Windows
es utilizar un buen administrador de paquetes. En macOS, la mayoría de la gente
utiliza Homebrew y, en Windows, Chocolatey es una buena opción.

Instalación de Homebrew en un macOS


Instalar Homebrew en un MacOS es fácil; tan solo sigue las instrucciones que
encontrarás en https://brew.sh/

Este es el comando para instalar Homebrew:


/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/
Homebrew/install/master/install)"

[ 17 ]
Configuración de un entorno de trabajo

Una vez finalizada la instalación, para probar si Homebrew funciona, escribe


brew --version en el Terminal. Deberías ver algo como esto:

$ brew --version
Homebrew 1.4.3
Homebrew/homebrew-core (git revision f4e35; last commit 2018-01-11)

Ahora, ya estamos listos para usar Homebrew para instalar herramientas y utilidades.
Si, por ejemplo, queremos instalar el editor de texto Vi, podemos hacerlo así:
$ brew install vim

A continuación, se descargará e instalará el editor automáticamente.

Instalación de Chocolatey en Windows


Para instalar el administrador de paquetes Chocolatey en Windows, sigue las
instrucciones que encontrarás en https://chocolatey.org/ o ejecuta el siguiente
comando en un terminal de PowerShell en el que seas administrador:
PS> Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object
System.Net.WebClient).DownloadString('https://chocolatey.org/install.
ps1'))

Una vez que se haya instalado Chocolatey, pruébalo con el comando choco
sin parámetros adicionales. Deberías ver un resultado similar al siguiente:
PS> choco
Chocolatey v0.10.3

Para instalar una aplicación como el editor Vi, utiliza el siguiente comando:
PS> choco install -y vim

El parámetro y- se asegura de que la instalación se lleve a cabo sin pedir una nueva
confirmación. Ten en cuenta que, una vez que Chocolatey haya instalado una aplicación,
deberás abrir una nueva ventana de PowerShell para usarla.

[ 18 ]
Capítulo 2

Selección de un editor de código


Es esencial usar un buen editor de código para trabajar de forma productiva con Docker.
Evidentemente, la cuestión de qué editor es el mejor es muy controvertida y depende
de las preferencias personales. Mucha gente utiliza Vim u otros como Emacs, Atom,
Sublime o Visual Studio (VS) Code, por mencionar unos pocos. Si aún no has decidido
qué editor es el más adecuado para ti, te recomiendo que pruebes VS Code. Se trata
de un editor gratuito y ligero, pero es muy potente y está disponible para macOS,
Windows y Linux. Pruébalo. Puedes descargar VS Code desde https://code.
visualstudio.com/download.

Pero si ya tienes un editor de código favorito, sigue usándolo. Siempre y cuando


puedas editar archivos de texto, es perfecto. Si el editor admite el resaltado
de sintaxis para Dockerfiles y archivos JSON y YAML, mejor aún.

Docker Toolbox
Docker Toolbox lleva unos cuantos años a disposición de los desarrolladores. Es anterior
a herramientas más modernas, como Docker para macOS y Docker para Windows.
Esta caja de herramientas permite a un usuario trabajar de una forma muy elegante
con contenedores en cualquier ordenador macOS o Windows. Los contenedores deben
ejecutarse en un host de Linux. Ni Windows ni macOS pueden ejecutar contenedores
de forma nativa. Por lo tanto, tenemos que ejecutar una MV Linux en nuestro portátil,
donde podemos ejecutar luego nuestros contenedores. Docker Toolbox instala VirtualBox
en el portátil, que se utiliza para ejecutar las MV Linux que necesitamos.

Como usuario de Windows, es posible que ya sepas que existen


los llamados contenedores de Windows que se ejecutan de forma
nativa en Windows. Y tienes razón. Recientemente, Microsoft ha
portado el motor Docker a Windows y ahora es posible ejecutar
contenedores de Windows directamente en Windows Server 2016
sin necesidad de una MV. Por lo tanto, ahora tenemos dos tipos de
contenedores: contenedores de Linux y contenedores de Windows.
Los primeros solo se ejecutan en el host de Linux y los segundos en
un servidor de Windows. En este libro, hablaremos exclusivamente
de contenedores de Linux, pero la mayoría de las cosas que
aprenderemos también se aplican a los contenedores de Windows.

[ 19 ]
Configuración de un entorno de trabajo

Vamos a utilizar docker-machine para configurar nuestro entorno. En primer lugar,


obtenemos una lista de todas las MV compatibles con Docker que tenemos definidas
en nuestro sistema. Si acabas de instalar Docker Toolbox, deberías ver lo siguiente:

Lista de todas las MV compatibles con Docker

La dirección IP utilizada puede ser diferente en cada caso, pero sin duda estará
en el intervalo de 192.168.0.0/24. También podemos ver que la MV tiene instalada
la versión de Docker 18.04.0-ce.

Si, por alguna razón, no tienes una MV predeterminada o la has eliminado por accidente,
puedes crearla mediante el siguiente comando:
$ docker-machine create --driver virtualbox default

El resultado que deberías ver será similar al siguiente:

Creación de la MV denominada default en VirtualBox

Para ver cómo se conecta tu cliente de Docker al motor de Docker que se está ejecutando
en esta máquina virtual, utiliza el siguiente comando:
$ docker-machine env default

[ 20 ]
Capítulo 2

Una vez que tengamos la MV denominada default, podemos intentar instalar ssh en ella:
$ docker-machine ssh default

Al ejecutar el comando anterior, recibimos un mensaje de bienvenida de boot2docker.

Escribe docker--version en el símbolo del sistema de la siguiente manera:


docker@default:~$ docker --version
Docker version 18.06.1-ce, build e68fc7a

Ahora, vamos a intentar ejecutar un contenedor:


docker@default:~$ docker run hello-world

Esto producirá el resultado siguiente:

Ejecución del contenedor Hello Word de Docker

[ 21 ]
Configuración de un entorno de trabajo

Docker para macOS y Docker para


Windows
Si utilizas un MacOS o tienes instalado Windows 10 Professional en tu portátil,
te recomendamos que instales Docker para MacOS o Docker para Windows. Estas
herramientas ofrecen la mejor experiencia para trabajar con contenedores. Ten en cuenta
que las versiones anteriores de Windows o Windows 10 Home Edition no pueden
ejecutar Docker para Windows. Docker para Windows utiliza Hyper-V para ejecutar
contenedores de forma transparente en una MV, pero Hyper-V no está disponible
en versiones anteriores de Windows ni está disponible en la edición Home.

Instalación de Docker para macOS


Visita el siguiente enlace para descargar Docker para macOS en
https://docs.docker.com/docker-for-mac/install/.

Hay disponible una versión estable y una versión conocida


como "Edge" de la herramienta. En este libro, vamos a utilizar
algunas características más recientes y Kubernetes que, en el
momento de escribir este libro, solo estaba disponible en
la versión Edge. Por lo tanto, selecciona esta versión.

Para comenzar la instalación:

1. Haz clic en el botón Get Docker for Mac (Edge) y sigue las instrucciones.
2. Una vez que hayas instalado correctamente Docker para macOS, abre un Terminal.
Pulsa comando + barra espaciadora para abrir Spotlight y escribe terminal. A
continuación, pulsa Intro. El Terminal de Apple se abrirá de la siguiente manera:

Ventana de Terminal de Apple

3. Escribe docker --version en el símbolo del sistema y pulsa Intro. Si Docker


para MacOS está instalado correctamente, deberías ver algo similar a lo siguiente:
$ docker –version
Docker version 18.02.0-ce-rc2, build f968a2c

[ 22 ]
Capítulo 2

4. Para comprobar si se pueden ejecutar contenedores, escribe el siguiente comando


en el Terminal y pulsa Intro:
$ docker run hello-world

Si todo va bien, el resultado debería ser parecido al siguiente:

Ejecución del contenedor Hello World en Docker para macOS

¡Enhorabuena! Ya estás listo para trabajar con contenedores Docker.

[ 23 ]
Configuración de un entorno de trabajo

Instalación de Docker para Windows


Ten en cuenta que solo puedes instalar Docker para Windows en Windows 10
Professional o Windows Server 2016, ya que requiere Hyper-V, que no está disponible
en versiones anteriores de Windows ni en la edición Home de Windows 10. Si utilizas
Windows 10 Home o una versión anterior de Windows, tendrás que seguir utilizando
Docker Toolbox.

1. Visita el siguiente enlace para descargar Docker para Windows en


https://docs.docker.com/docker-for-windows/install/.

Hay disponible una versión estable y una versión conocida como


"Edge" de la herramienta. En este libro, vamos a utilizar algunas
características más recientes y Kubernetes que, en el momento
de escribir este libro, solo estaba disponible en la versión Edge.
Por lo tanto, selecciona esta versión.

2. Para iniciar la instalación, haz clic en el botón Get Docker for Windows (Edge)
y sigue las instrucciones. Con Docker para Windows, puedes desarrollar,
ejecutar y probar contenedores Linux y contenedores Windows. Sin embargo,
en este libro, solo vamos a hablar de contenedores Linux.
3. Una vez que hayas instalado correctamente Docker para Windows, abre una
ventana de PowerShell y escribe docker --version en el símbolo del sistema.
Deberías ver algo similar a lo siguiente:
PS> docker --version
Docker version 18.04.0-ce, build 3d479c0

Uso de docker-machine en Windows con


Hyper-V
Si tienes instalado Docker para Windows en tu ordenador portátil con Windows, también
tendrás activado Hyper-V. En este caso, no puedes utilizar Docker Toolbox, ya que
utiliza VirtualBox, e Hyper-V y VirtualBox no pueden coexistir y ejecutarse al mismo
tiempo. Si es así, puedes utilizar docker-machine con el controlador Hyper-V.

1. Abre una consola de PowerShell como administrador. Instala docker-machine


con Chocolatey de la siguiente manera:
PS> choco install -y docker-machine

[ 24 ]
Capítulo 2

2. Usa el administrador de Hyper-V de Windows para crear un nuevo conmutador


interno denominado DM Internal Switch, donde DM significa docker-machine.
3. Crea una MV denominada default en Hyper-V con el siguiente comando:
PS> docker-machine create --driver hyperv --hyperv-virtual-
switch "DM Internal Switch" default

Debes ejecutar el comando anterior en el modo


de administrador o se producirá un error.

Deberías ver el siguiente resultado que ha generado el comando anterior:


Running pre-create checks... (boot2docker) Image cache
directory does not exist, creating it at C:\Users\Docker\.
docker\machine\cache... (boot2docker) No default Boot2Docker
ISO found locally, downloading the latest release...
(boot2docker) Latest release for github.com/boot2docker/
boot2docker is v18.06.1-ce
....
....
Checking connection to Docker...
Docker is up and running! To see how to connect your Docker
Client to the Docker Engine running on this virtual machine,
run: C:\Program Files\Doc ker\Docker\Resources\bin\docker-
machine.exe env default

4. Para ver cómo se conecta tu cliente de Docker al motor de Docker que se está
ejecutando en esta máquina virtual, utiliza el siguiente comando:
C:\Program Files\Docker\Docker\Resources\bin\docker-machine.
exe env default

5. Este es el resultado cuando pedimos un listado de todas las MV generadas por


docker-machine:
PS C:\WINDOWS\system32> docker-machine ls
NAME ACTIVE DRIVER STATE URL
SWARM DOCKER ERRORS
default . - hyperv Running tcp://[...]:2376
v18.06.1-ce

6. Ahora, vamos a usar SSH en nuestra MV boot2docker:


PS> docker-machine ssh default

[ 25 ]
Configuración de un entorno de trabajo

Debería aparecer la pantalla de bienvenida.


Podemos probar la MV ejecutando el comando docker version, que se
muestra de la siguiente manera:

Versión del cliente (CLI) y del servidor de Docker

Sin duda, es una MV de Linux, como podemos ver en la entrada OS/Arch,


y tiene instalado Docker 18.06.1-ce.

Minikube
Si no puedes utilizar Docker para MacOS o Windows o, por alguna razón, solo tienes
acceso a una versión antigua de la herramienta que no admite Kubernetes, sería
buena idea instalar Minikube. Minikube aprovisiona un clúster Kubernetes de un solo
nodo en tu estación de trabajo. Se puede obtener acceso a él a través de kubectl, que
es la herramienta de línea de comandos que se utiliza para trabajar con Kubernetes.

Instalación de Minikube en macOS y Windows


Para instalar Minikube para macOS o Windows, visita el siguiente enlace en
https://kubernetes.io/docs/tasks/tools/install-minikube/.

Sigue las instrucciones detenidamente. Si tienes instalado Toolbox Docker, ya tienes


un hipervisor en el sistema, porque el instalador de Docker Toolbox también instala
VirtualBox. De lo contrario, te recomiendo que instales VirtualBox primero.

[ 26 ]
Capítulo 2

Si tienes instalado Docker para macOS o Windows, ya tienes instalado kubectl, por
lo que también puedes omitir ese paso. De lo contrario, sigue las instrucciones del sitio.

Finalmente, selecciona el binario más reciente de Minikube para macOS o Windows


e instálalo. Para macOS, el binario más reciente se denomina minikube-darwin-amd64 y,
para Windows, es minikube-windows-amd64.

Prueba de Minikube y kubectl


Una vez que Minikube se ha instalado correctamente en la estación de trabajo, abre
un Terminal y prueba la instalación.

1. Primero, tenemos que iniciar Minikube. Escribe minikube start en la línea


de comandos. El resultado debería ser similar al siguiente:

Inicio de Minikube

2. Ahora, escribe kubectl version y pulsa Intro para ver algo parecido a la
siguiente captura de pantalla:

Determinación de la versión del cliente y el servidor de Kubernetes

Si el comando anterior falla, por ejemplo, al agotarse el tiempo de espera,


el problema podría ser que kubectl no está configurado para el contexto
correcto. kubectl se puede utilizar para trabajar con muchos clústeres
diferentes de Kubernetes. Cada clúster se denomina "contexto".

[ 27 ]
Configuración de un entorno de trabajo

3. Para averiguar en qué contexto está configurado actualmente kubectl, utiliza


el siguiente comando:
$ kubectl config current-context minikube

La respuesta debería ser minikube, como se muestra en el resultado anterior.

4. Si no es así, utiliza kubectl config get-contexts para obtener una lista


de todos los contextos que están definidos en tu sistema y luego establece el
contexto actual en minikube de la siguiente forma:
$ kubectl config use-context minikube

La configuración de kubectl, donde almacena los contextos, se encuentra


normalmente en ~/.kube/config, pero se puede cambiar definiendo una
variable de entorno denominada KUBECONFIG. Es posible que tengas que
desactivar esta variable si está configurada en el equipo.
Para obtener información más detallada sobre cómo configurar y utilizar los
contextos de Kubernetes, consulta el enlace en https://kubernetes.io/docs/
concepts/configuration/organize-cluster-access-kubeconfig/.
Suponiendo que Minikube y kubectl funcionen de la forma esperada, ahora
podemos usar kubectl para obtener información sobre el clúster de Kubernetes.

5. Escribe el siguiente comando:


$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
minikube Ready <none> 47d v1.9.0

Evidentemente, tenemos un clúster de un nodo, que en mi caso tiene instalado


Kubernetes v1.9.0.

Clonación del repositorio de código fuente


Este libro viene acompañado del código fuente que está disponible de forma pública en
un repositorio de GitHub en https://github.com/appswithdockerandkubernetes/
labs. Clona ese repositorio en el equipo local.

En primer lugar, crea una nueva carpeta, por ejemplo, en la carpeta de inicio,
como apps-with-docker-and-kubernetes, y desplázate hasta ella:

$ mkdir -p ~/apps-with-docker-and-kubernetes \ cd apps-with-


docker-and-kubernetes

[ 28 ]
Capítulo 2

Y, luego, clona el repositorio con el siguiente comando:


$ git clone https://github.com/appswithdockerandkubernetes/labs.
git

Resumen
En este capítulo, hemos instalado y configurado nuestro entorno personal o de trabajo
para poder trabajar de una forma productiva con contenedores Docker. Esto también
se aplica a los desarrolladores, DevOps e ingenieros de operaciones. En ese contexto,
hemos procurado utilizar un buen editor, hemos instalado Docker para MacOS
o Windows y también podemos utilizar docker-machine para crear máquinas virtuales
en VirtualBox o Hyper-V, que podemos utilizar para ejecutar y probar contenedores.

En el próximo capítulo, vamos a aprender todos los aspectos importantes de los


contenedores. Por ejemplo, veremos cómo se pueden ejecutar, detener, enumerar
y eliminar contenedores, pero también profundizaremos en su anatomía.

Preguntas
Tras leer este capítulo, responde a las siguientes preguntas:

1. ¿Para qué se utiliza docker-machine? Indica tres o cuatro escenarios.


2. ¿Verdadero o falso? Con Docker para Windows, se pueden desarrollar y ejecutar
contenedores de Linux.
3. ¿Por qué es esencial tener buenas herramientas de script (como Bash o
PowerShell) para usar los contenedores de forma productiva?
4. Indica tres o cuatro distribuciones de Linux en las que Docker tiene certificación
para ejecutarse.
5. Indica todas las versiones de Windows en las que puedes ejecutar contenedores
de Windows.

Lectura adicional
En el siguiente enlace, encontrarás bibliografía adicional (puede estar en inglés):

• Ejecutar Docker en Hyper-V con Docker Machine en http://bit.ly/2HGMPiI

[ 29 ]
Trabajar con contenedores
En el capítulo anterior aprendiste a preparar de forma óptima tu entorno de trabajo para
el uso productivo y sin complicaciones de Docker. En este capítulo, vamos a ensuciarnos
las manos y aprender todo lo que es importante para trabajar con los contenedores. Estos
son los temas que vamos a tratar en este capítulo:

• Ejecución del primer contenedor


• Inicio, detención y eliminación de contenedores
• Inspección de contenedores
• Ejecución del comando exec en un contenedor en ejecución
• Conexión a un contenedor en ejecución
• Recuperación de registros de contenedores
• Anatomía de los contenedores

Cuando termines este capítulo serás capaz de hacer lo siguiente:

• Ejecutar, detener y eliminar un contenedor basado en una imagen existente,


como NGINX, BusyBox o Alpine
• Mostrar todos los contenedores del sistema
• Inspeccionar los metadatos de un contenedor en ejecución o detenido
• Recuperar los registros producidos por una aplicación que se ejecuta dentro
de un contenedor
• Ejecutar un proceso como /bin/sh en un contenedor que ya se está ejecutando.
• Conectar un terminal a un contenedor que ya se encuentra en ejecución
• Explicar con tus propias palabras a un lego interesado los fundamentos
de un contenedor

[ 31 ]
Trabajar con contenedores

Requisitos técnicos
Para este capítulo, deberías haber instalado Docker para Mac o Docker para Windows.
Si utilizas una versión anterior de Windows o Windows 10 Home Edition, debes haber
instalado y preparado Docker Toolbox. En macOS, utiliza la aplicación Terminal y en
Windows, una consola de PowerShell para probar los comandos que vas a aprender.

Ejecución del primer contenedor


Antes de empezar, queremos asegurarnos de que Docker está instalado correctamente
en tu sistema y listo para aceptar tus comandos. Abre una nueva ventana de Terminal
y escribe el siguiente comando:
$ docker -v

Si todo funciona correctamente, deberías ver la versión de Docker instalada


en el resultado del Terminal de tu portátil. En el momento de redactar este
documento, el resultado sería el siguiente:
Docker version 17.12.0-ce-rc2, build f9cde63

Si esto no funciona, algo falla en tu instalación. Asegúrate de que has seguido las
instrucciones del capítulo anterior sobre cómo instalar Docker para Mac o Docker
para Windows en tu sistema.

Ahora estás listo para ver algo de acción. Escribe el siguiente comando en tu ventana
de Terminal y pulsa Intro:
$ docker container run alpine echo "Hello World"

Cuando ejecutes el comando anterior por primera vez, deberías ver un resultado
en la ventana de Terminal similar al siguiente:
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
2fdfe1cd78c2: Pull complete
Digest: sha256:ccba511b...
Status: Downloaded newer image for alpine:latest
Hello World

Fácil, ¿no? Vamos a tratar de ejecutar el mismo comando de nuevo:


$ docker container run alpine echo "Hello World"

[ 32 ]
Capítulo 3

La segunda, tercera o enésima vez que ejecutes el comando anterior, solo debes ver
este resultado en tu Terminal:
Hello World

Trata de averiguar por qué la primera vez que ejecutas un comando aparece un resultado
diferente al de todos los intentos posteriores. No te preocupes si no puedes averiguarlo;
te explicaremos de forma detallada las razones en las siguientes secciones del capítulo.

Inicio, detención y eliminación


de contenedores
En la sección anterior has ejecutado correctamente un contenedor. Ahora queremos
investigar en detalle qué ha ocurrido exactamente y por qué. Echemos un vistazo
de nuevo al comando que usamos:
$ docker container run alpine echo "Hello World"

Este comando contiene varias partes. En primer lugar, tenemos la palabra docker.
Este es el nombre de la interfaz de línea de comandos (CLI) de Docker, que estamos
utilizando para interactuar con el motor de Docker encargado de ejecutar los
contenedores. A continuación, tenemos la palabra container, que indica el contexto
en el que estamos trabajando. Como queremos ejecutar un contenedor, nuestro contexto
es la palabra container. A continuación está el comando real que queremos ejecutar en
el contexto dado, que es run.

Recapitulemos: hasta el momento, tenemos docker container run, que significa:


Oye, Docker, queremos ejecutar un contenedor....

Ahora también tenemos que decirle a Docker qué contenedor debe ejecutar. En este caso,
es el contenedor Alpine. Por último, debemos definir qué tipo de proceso o tarea se
ejecutará dentro del contenedor cuando este se encuentre en ejecución. En nuestro caso,
esta es la última parte del comando, echo "Hello World".

La siguiente imagen puede ayudarte a obtener una idea mejor de todo esto:

Anatomía de la expresión docker container run

[ 33 ]
Trabajar con contenedores

Ahora que hemos comprendido las distintas partes de un comando para ejecutar
un contenedor, vamos a tratar de ejecutar otro contenedor con un proceso diferente
que se ejecuta dentro del mismo. Escribe el siguiente comando en tu Terminal:
$ docker container run centos ping -c 5 127.0.0.1

Deberías ver un resultado similar al siguiente en la ventana de tu Terminal:


Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
85432449fd0f: Pull complete
Digest: sha256:3b1a65e9a05...
Status: Downloaded newer image for centos:latest
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.022 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.019 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.029 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.030 ms
64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.029 ms

--- 127.0.0.1 ping statistics ---


5 packets transmitted, 5 received, 0% packet loss, time 4103ms
rtt min/avg/max/mdev = 0.021/0.027/0.029/0.003 ms

Lo que ha cambiado es que, esta vez, la imagen de contenedor que estamos usando es
centos y el proceso que estamos ejecutando dentro del contenedor de centos es ping
-c 5 127.0.0.1, que intenta conectar con la dirección de bucle de retorno cinco veces
hasta que se detiene.

Analicemos el resultado en detalle:

• La primera línea es la siguiente:


Unable to find image 'centos:latest' locally

Esto nos dice que Docker no ha encontrado ninguna imagen llamada


centos:latest en la caché local del sistema. Por lo tanto, Docker sabe que
tiene que obtener la imagen de algún registro donde se almacenen las imágenes
del contenedor. De forma predeterminada, el entorno del Docker se configura
de forma tal que las imágenes se extraen de Docker Hub en docker.io.
Esto se expresa en la segunda línea de la siguiente manera:
latest: Pulling from library/centos

[ 34 ]
Capítulo 3

• Las siguientes tres líneas de resultado son las siguientes:


85432449fd0f: Pull complete
Digest: sha256:3b1a65e9a05...
Status: Downloaded newer image for centos:latest

Esto nos dice que Docker ha encontrado con éxito la imagen centos:latest
de Docker Hub.

Todas las líneas siguientes del resultado las genera el proceso que ejecutamos dentro
del contenedor, que, en este caso, es la herramienta ping. Si has estado atento, puede que
hayas visto la palabra clave latest que aparece unas cuantas veces. Cada imagen tiene
una versión (también denominada etiqueta) y si no especificamos explícitamente una
versión, Docker supone automáticamente que es la última.

Si volvemos a ejecutar el contenedor anterior en nuestro sistema, las primeras cinco líneas
de resultados faltarán ya que, esta vez, Docker encontrará la imagen del contenedor
almacenada localmente en la caché y por lo tanto no tendrás que descargarla primero.
Para comprobarlo, inténtalo.

Ejecutar un contenedor de citas aleatorias


Para las siguientes secciones de este capítulo, necesitamos un contenedor que se ejecute
continuamente en segundo plano y que genere un resultado interesante. Por eso hemos
elegido un algoritmo que produce citas aleatorias. La API que produce esas citas aleatorias
gratuitas se puede encontrar en https://talaikis.com/random_quotes_api/.

Ahora el objetivo es tener un proceso que se ejecute dentro de un contenedor que


produzca una nueva cita aleatoria cada cinco segundos y muestre la cita en STDOUT.
El siguiente script hará exactamente eso:
while :
do
wget -qO- https://talaikis.com/api/quotes/random
printf 'n'
sleep 5
done

Inténtalo en una ventana de Terminal. Detén el script presionando Ctrl+ C. El resultado


debe tener un aspecto parecido al siguiente:
{"quote":"Martha Stewart is extremely talented. Her designs are
picture perfect. Our philosophy is life is messy, and rather than
being afraid of those messes we design products that work the way we
live.","author":"Kathy Ireland","cat":"design"}

[ 35 ]
Trabajar con contenedores

{"quote":"We can reach our potential, but to do so, we must


reach within ourselves. We must summon the strength, the will,
and the faith to move forward - to be bold - to invest in our
future.","author":"John Hoeven","cat":"faith"}

Cada respuesta es una cadena con formato JSON con la cita, su autor y su categoría.

Ahora, vamos a ejecutarlo en un contenedor Alpine como un daemon en segundo plano.


Para ello, necesitamos compactar el script anterior en una línea y ejecutarlo usando
la sintaxis /bin/sh-c "...". Nuestra expresión Docker tendrá este aspecto:
$ docker container run -d --name quotes alpine \
/bin/sh -c "while :; do wget -qO- https://talaikis.com/api/quotes/
random; printf '\n'; sleep 5; done"

En la expresión anterior, hemos utilizado dos nuevos parámetros de línea de comandos,


-d y --name. La -d le dice a Docker que ejecute el proceso que se ejecuta en el
contenedor como un daemon de Linux. El parámetro --name, a su vez, puede utilizarse
para asignar al contenedor un nombre explícito. En el ejemplo anterior, el nombre
que elegimos fue quotes.

Si no especificamos un nombre de contenedor explícito cuando ejecutamos un


contenedor, Docker asignará automáticamente al contenedor un nombre aleatorio
pero único. Este nombre estará compuesto por el nombre de un científico famoso y un
adjetivo. Estos nombres podrían ser boring_borg o angry_goldberg. Qué graciosos
son nuestros ingenieros de Docker, ¿verdad?

Un aspecto importante es que el nombre del contenedor tiene que ser único en el sistema.
Vamos a asegurarnos de que el contenedor de citas está en funcionamiento:
$ docker container ls -l

El resultado debería ser parecido al siguiente:

Listado del último contenedor en ejecución

La parte importante del resultado anterior es la columna STATUS, que en este caso
es Up 16 seconds (Hasta 16 segundos). Esto significa que el contenedor ha estado
funcionando durante 16 segundos hasta ahora.

No te preocupes si no estás familiarizado con este último comando de Docker;


volveremos a explicarlo en la siguiente sección.

[ 36 ]
Capítulo 3

Listado de contenedores
A medida que continuamos ejecutando contenedores con el tiempo, almacenamos
muchos de ellos en nuestro sistema. Para saber qué se está ejecutando actualmente
en nuestro host, podemos utilizar el comando list de la siguiente manera:
$ docker container ls

Este comando enumerará todos los contenedores que se están ejecutando actualmente.
Esta lista podría ser similar a la siguiente:

Enumerar todos los contenedores del sistema

De forma predeterminada, Docker genera siete columnas con los siguientes significados:

Columna Descripción
Container ID El identificador único del contenedor. Es un SHA-256.
Nombre de la imagen del contenedor desde la que se crea una
Image
instancia de este contenedor.
Comando que se utiliza para ejecutar el proceso principal en el
Command
contenedor.
Created Fecha y hora en que se creó el contenedor.
El estado del contenedor (creado, reiniciando, ejecutando, eliminando,
Status
pausado, abandonado o inactivo).
Ports La lista de puertos de contenedores que se han asignado al host.
Nombre asignado a este contenedor (es posible asignar
Names
varios nombres).

Si queremos enumerar no solo los contenedores actualmente en ejecución sino todos


los contenedores definidos en nuestro sistema, podemos usar el parámetro de la línea
de comandos -a o --all de la siguiente manera:
$ docker container ls -a

Este comando enumerará los contenedores que tengan cualquier estado, como created,
running o exited.

A veces, solo queremos enumerar los identificadores de todos los contenedores. Para ello,
tenemos el parámetro -q:
$ docker container ls -q

[ 37 ]
Trabajar con contenedores

Puede que te preguntes dónde es útil este parámetro. El siguiente comando muestra
dónde puede ser muy útil:
$ docker container rm -f $(docker container ls -a -q)

Reclínate y respira hondo. Después, trata de averiguar la función del comando anterior.
No leas más hasta que encuentres la respuesta o te des por vencido.

Correcto: el comando anterior borra todos los contenedores que están definidos
actualmente en el sistema, incluidos los detenidos. El comando rm significa "eliminar"
y se explicará más adelante.

En la sección anterior, usamos el parámetro -l en el comando list. Intenta utilizar


la ayuda de Docker para averiguar qué significa el parámetro -l. Puedes abrir el panel
de ayuda del comando list de la siguiente manera:
$ docker container ls -h

Detención e inicio de contenedores


A veces queremos detener (temporalmente) un contenedor en ejecución. Vamos a
intentarlo con el contenedor de citas que utilizamos anteriormente. Vuelve a ejecutar
el contenedor con este comando:
$ docker container run -d --name quotes alpine \
/bin/sh -c "while :; do wget -qO- https://talaikis.com/api/quotes/
random; printf '\n'; sleep 5; done"

Ahora, si queremos detener este contenedor , podemos hacerlo con este comando:
$ docker container stop quotes

Cuando intentes detener el contenedor de citas, probablemente tendrás que esperar un


tiempo hasta que este comando se ejecute. Para ser más precisos, unos 10 segundos.
¿Por qué es así?

Docker envía una señal SIGTERM de Linux al proceso principal que se ejecuta dentro del
contenedor. Si el proceso no reacciona a esta señal y finaliza, Docker espera 10 segundos
y envía SIGKILL, que terminará el proceso a la fuerza y finalizará el contenedor.

En el comando anterior, hemos utilizado el nombre del contenedor para especificar


el contenedor que queremos detener. Pero también podríamos haber usado el ID del
contenedor en su lugar.

[ 38 ]
Capítulo 3

¿Cómo conseguimos el identificador de un contenedor? Hay varias maneras de hacerlo.


El modo manual consiste en enumerar todos los contenedores en ejecución y encontrar
el que estamos buscando en la lista. A partir de ahí, copiamos su identificador. Una
forma más automatizada es utilizar algunas variables de entorno y scripting del shell.
Si, por ejemplo, queremos obtener el identificador del contenedor de citas, podemos usar
esta expresión:
$ export CONTAINER_ID=$(docker container ls | grep quotes | awk
'{print $1}')

Ahora, en lugar de utilizar el nombre del contenedor, podemos utilizar la variable


$CONTAINER_ID en nuestra expresión:

$ docker container stop $CONTAINER_ID

Una vez que hemos detenido el contenedor, su estado cambia a Exited (abandonado).

Si se detiene un contenedor, puede volver a iniciarse utilizando el comando docker


container start. Vamos a hacerlo con nuestro contenedor de citas. Es bueno tenerlo
funcionando de nuevo, ya que lo necesitaremos en las secciones siguientes de este capítulo:
$ docker container start quotes

Eliminación de contenedores
Cuando ejecutamos el comando docker container ls-a, podemos ver bastantes
contenedores que están en estado Exited. Si ya no necesitamos estos contenedores,
es una buena idea eliminarlos de la memoria, ya que, de lo contrario, consumirán
innecesariamente nuestros valiosos recursos. El comando para eliminar un contenedor es:
$ docker container rm <container ID>

Otro comando para eliminar un contenedor es:


$ docker container rm <container name>

Intenta eliminar uno de tus contenedores abandonados usando su ID.

A veces, la eliminación de un contenedor no funciona, ya que todavía se está ejecutando.


Si queremos forzar una eliminación sea cual sea el estado actual del mismo, podemos
usar los parámetros de línea de comandos -f o --force.

[ 39 ]
Trabajar con contenedores

Inspección de contenedores
Los contenedores son instancias del runtime de una imagen y tienen muchos datos
asociados que caracterizan su comportamiento. Para obtener más información sobre
un contenedor específico, podemos utilizar el comando inspect. Como de costumbre,
debemos proporcionar el ID o el nombre del contenedor para identificar el contenedor
del que queremos obtener los datos. Así pues, examinemos nuestro contenedor
de prueba:
$ docker container inspect quotes

La respuesta es un gran objeto JSON lleno de detalles. El resultado es similar al siguiente:


[
{
"Id": "c5c1c68c87...",
"Created": "2017-12-30T11:55:51.223271182Z",
"Path": "/bin/sh",
"Args": [
"-c",
"while :; do wget -qO- https://talaikis.com/api/
quotes/random; printf '\n'; sleep 5; done"
],
"State": {
"Status": "running",
"Running": true,
...
},
"Image": "sha256:e21c333399e0...",
...
"Mounts": [],
"Config": {
"Hostname": "c5c1c68c87dd",
"Domainname": "",
...
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "2fd6c43b6fe5...",
...
}
}
]

[ 40 ]
Capítulo 3

El resultado se ha acortado para que sea más fácil de leer.

Detente un momento para analizar lo que contiene. Deberías ver información como:

• El ID del contenedor
• La fecha y hora de creación del contenedor
• Desde qué imagen se ha creado el contenedor y otros detalles

Muchas secciones del resultado como Mounts o NetworkSettings no tienen mucho


sentido en este momento, pero las explicaremos en los próximos capítulos del libro.
Los datos que ves aquí también se denominan metadatos de un contenedor. Utilizaremos
el comando inspect con bastante frecuencia en el resto del libro como fuente
de información.

A veces, solo necesitamos una pequeña parte de la información general, y para lograrlo,
podemos usar la herramienta grep tool o un filtro. El método anterior no siempre
produce la respuesta esperada, así que analicemos este último enfoque:
$ docker container inspect -f "{{json .State}}" quotes | jq

Los parámetros -f o --filter se utilizan para definir el filtro. La propia expresión


del filtro utiliza la sintaxis go template. En este ejemplo, solo queremos ver la parte
del estado de todo el resultado en formato JSON.

Para formatear el resultado, enviamos el resultado a la herramienta jq:


{
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 6759,
"ExitCode": 0,
"Error": "",
"StartedAt": "2017-12-31T10:31:51.893299997Z",
"FinishedAt": "0001-01-01T00:00:00Z"
}

[ 41 ]
Trabajar con contenedores

Ejecución del comando exec


en un contenedor en ejecución
A veces, queremos ejecutar otro proceso dentro de un contenedor en ejecución,
normalmente para intentar depurar un contenedor que funciona mal. ¿Cómo podemos
hacerlo? En primer lugar, necesitamos saber el ID o el nombre del contenedor
y, a continuación, podemos definir qué proceso queremos ejecutar y cómo queremos que
se ejecute. Una vez más, usamos nuestro contenedor de citas actualmente en ejecución
y ejecutamos un shell de manera interactiva dentro del mismo con el siguiente comando:
$ docker container exec -i -t quotes /bin/sh

La etiqueta -i significa que queremos ejecutar el proceso adicional de forma interactiva


y -t le dice a Docker que queremos que nos proporcione un TTY (un emulador
de terminal) para el comando. Finalmente, el proceso que ejecutamos es /bin/sh.

Si ejecutamos el comando anterior en nuestro Terminal, aparecerá un nuevo símbolo del


sistema. Ahora estamos en un shell dentro del contenedor de citas. Podemos probarlo
fácilmente, por ejemplo, ejecutando el comando ps, que enumerará todos los procesos
en ejecución en el contexto:
# / ps

El resultado debe tener un aspecto parecido al siguiente:

Lista de procesos que se ejecutan dentro del contenedor de citas

Podemos ver claramente que el proceso con PID 1 es el comando que hemos
definido para que se ejecute dentro del contenedor de citas. El proceso con PID 1
también se denomina "proceso principal".

Sal del contenedor introduciendo exit en el símbolo del sistema. No podemos


ejecutar procesos adicionales interactivos en un contenedor exclusivamente. Observa
el siguiente comando:
$ docker container exec quotes ps

[ 42 ]
Capítulo 3

Evidentemente, el resultado es muy similar al anterior:

Lista de procesos que se ejecutan dentro del contenedor de citas

Incluso podemos ejecutar procesos como daemon usando la marca -d y definir variables
de entorno usando las variables de marca -e de la siguiente manera:
$ docker container exec -it \
-e MY_VAR="Hello World" \
quotes /bin/sh
# / echo $MY_VAR
Hello World
# / exit

Conexión a un contenedor en ejecución


Podemos utilizar el comando attach para conectar la entrada estándar de nuestro
Terminal, la salida o resultado y el error (o cualquier combinación de los tres) a un
contenedor en ejecución utilizando el ID o el nombre del contenedor. Vamos a hacerlo
con nuestro contenedor de citas:
$ docker container attach quotes

En este caso, vamos a ver cada cinco segundos aproximadamente una nueva cita
en el resultado.

Para salir del contenedor sin pararlo o eliminarlo, podemos pulsar la combinación
de teclas Ctrl + P Ctrl+ Q. Esto nos desconecta del contenedor mientras lo dejamos
funcionando en segundo plano. Por otro lado, si queremos separar y detener
el contenedor al mismo tiempo, podemos pulsar Ctrl + C.

Vamos a ejecutar otro contenedor, esta vez un servidor web nginx:


$ docker run -d --name nginx -p 8080:80 nginx:alpine

[ 43 ]
Trabajar con contenedores

Aquí, ejecutamos la versión Alpine de Nginx como daemon en un contenedor llamado


nginx. El parámetro -p 8080:80 de la línea de comandos abre el puerto 8080
en el host para tener acceso al servidor web nginx que se ejecuta dentro del contenedor.
No te preocupes por la sintaxis, ya que explicaremos esta característica con más detalle
en el Capítulo 7, Conexión en red con un solo host.

Veamos si podemos acceder a nginx usando la herramienta curl y ejecutando este


comando:
$ curl -4 localhost:8080

Si todo funciona correctamente, debes recibir un mensaje de la página de bienvenida


de Nginx:
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully
installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to


<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>


</body>
</html>

Ahora, vamos a asociar nuestro Terminal al contenedor nginx para observar lo que ocurre:
$ docker container attach nginx

[ 44 ]
Capítulo 3

Una vez que estés conectado al contenedor, al principio no verás nada. Pero ahora abre
otro Terminal, y en esta nueva ventana de Terminal, repite el comando curl varias veces
usando el siguiente script:
$ for n in {1..10}; do curl -4 localhost:8080; done

Deberías ver el resultado del registro de Nginx, que es parecido al siguiente:


172.17.0.1 - - [06/Jan/2018:12:20:00 +0000] "GET / HTTP/1.1" 200 612
"-" "curl/7.54.0" "-"
172.17.0.1 - - [06/Jan/2018:12:20:03 +0000] "GET / HTTP/1.1" 200 612
"-" "curl/7.54.0" "-"
172.17.0.1 - - [06/Jan/2018:12:20:05 +0000] "GET / HTTP/1.1" 200 612
"-" "curl/7.54.0" "-"

Sal del contenedor pulsando Ctrl + C. Esto te desconectará del Terminal y, al mismo
tiempo, detendrá el contenedor nginx.

Para limpiar, elimina el contenedor nginx con el siguiente comando:


$ docker container rm nginx

Recuperación de registros
de contenedores
Es una práctica recomendada para cualquier aplicación generar información de registro
que los desarrolladores y operadores puedan utilizar para averiguar lo que la aplicación
está haciendo en un momento dado, y si hay algún problema, determinar su causa.

Cuando se ejecuta dentro de un contenedor, la aplicación debe enviar preferentemente


los elementos de registro a STDOUT y STDERR, y no a un archivo. Si el resultado
del registro se dirige a STDOUT y STDERR, Docker puede recopilar esta información
y mantenerla lista para su uso por un usuario o cualquier otro sistema externo.

Para acceder a los registros de un contenedor determinado, podemos utilizar el comando


docker container logs. Si, por ejemplo, queremos obtener los registros de nuestro
contenedor de citas, podemos usar la siguiente expresión:
$ docker container logs quotes

Esto recuperará todo el registro producido por la aplicación desde que existe.

[ 45 ]
Trabajar con contenedores

Un momento. Lo que acabo de decir sobre la disponibilidad del registro


completo desde la existencia de los contenedores no es del todo cierto.
De forma predeterminada, Docker utiliza el controlador de registro json-
file. Este controlador almacena la información de registro en un archivo.
Y si se define una política de importación de archivos, los registros de
contenedor de Docker solo recuperan lo que está en el archivo de
registro activo actual y no lo que se encuentra en los archivos importados
anteriores que aún podrían estar disponibles en el host.

Si queremos obtener solo algunas de las últimas entradas, podemos usar el


parámetro -t o --tail, de la siguiente manera:
$ docker container logs --tail 5 quotes

Este comando solo recuperará los últimos cinco elementos del proceso que se ejecuta
dentro del contenedor producido.

A veces, queremos seguir el registro producido por un contenedor. Esto es posible


cuando se usa el parámetro -f- o --follow. La siguiente expresión mostrará los últimos
cinco elementos de registro y, a continuación, seguirá el registro tal como lo produce
el proceso incluido en el contenedor:
$ docker container logs --tail 5 --follow quotes

Controladores de registro
Docker incluye varios mecanismos de registro que nos ayudan a obtener información
de los contenedores en ejecución. Estos mecanismos se denominan controladores
de registro. El controlador de registro que se utiliza se puede configurar en el nivel de
daemon de Docker. El controlador de registro predeterminado es json-file. Algunos
de los controladores que actualmente son compatibles de forma nativa son:

Controlador Descripción
none No se produce ningún resultado de registro para el contenedor específico.
Este es el controlador predeterminado. La información de registro
json-file
se almacena en archivos, formateados como JSON.
Si el daemon del diario se está ejecutando en el equipo host, podemos
journald
utilizar este controlador. Reenvía el registro al daemon journald.
Si el daemon syslog se está ejecutando en el equipo host, podemos configurar
syslog
este controlador, que reenviará los mensajes de registro al daemon syslog.
Cuando se utiliza este controlador, los mensajes de registro se escriben en
gelf un punto de conexión Graylog Extended Log Format (GELF). Los ejemplos
más conocidos de estos puntos de conexión son Graylog y Logstash.
Suponiendo que el daemon fluentd esté instalado en el sistema host, este
fluentd
controlador escribe en él los mensajes de registro.
[ 46 ]
Capítulo 3

Si cambias el controlador de registro, ten en cuenta que el


comando docker container logs solo está disponible para los
controladores json-file y journald.

Uso de un controlador de registro específico


del contenedor
Hemos visto que el controlador de registro se puede configurar globalmente en el archivo
de configuración del daemon de Docker. Pero también podemos definir el controlador de
registro para cada contenedor. En el siguiente ejemplo, ejecutamos un contenedor busybox
y usamos el parámetro --log driver para configurar el controlador de registro none:
$ docker container run --name test -it \
--log-driver none \
busybox sh -c 'for N in 1 2 3; do echo "Hello $N"; done'

Debemos ver lo siguiente:


Hello 1
Hello 2
Hello 3

Ahora, vamos a tratar de obtener los registros del contenedor anterior:


$ docker container logs test

El resultado es el siguiente:
Error response from daemon: configured logging driver does not support
reading

Esto es de esperar, ya que el controlador none no genera ningún resultado de registro.


Vamos a limpiar y eliminar el contenedor de prueba:
$ docker container rm test

Tema avanzado: cambiar el controlador


de registro predeterminado
Vamos a cambiar el controlador de registro predeterminado de un host Linux. La forma
más fácil de hacerlo es en un host de Linux real. Para ello, vamos a utilizar Vagrant con
una imagen de Ubuntu:
$ vagrant init bento/ubuntu-17.04
$ vagrant up
$ vagrant ssh
[ 47 ]
Trabajar con contenedores

Una vez dentro de la máquina virtual de Ubuntu, queremos editar el archivo


de configuración del daemon de Docker. Desplázate hasta la carpeta /etc/docker
y ejecuta vi de la siguiente manera:
$ vi daemon.json

Introduce el siguiente contenido:


{
"Log-driver": "json-log",
"log-opts": {
"max-size": "10m",
"max-file": 3
}
}

Guarda y sal de Vi pulsando primero Esc, escribiendo :w:q y pulsando la tecla Intro.

En la definición anterior se indica al daemon de Docker que utilice el controlador


json-file con un tamaño máximo de archivo de registro de 10 MB antes de que
se implemente, y un número máximo de tres archivos de registro en el sistema antes
de purgar el archivo más antiguo.

Ahora debemos enviar una señal SIGHUP al daemon de Docker para que recupere
los cambios del archivo de configuración:
$ sudo kill -SIGHUP $(pidof dockerd)

Ten en cuenta que el comando anterior solo vuelve a cargar el archivo


de configuración y no reinicia el daemon.

Anatomía de los contenedores


Muchas personas comparan erróneamente contenedores con máquinas virtuales. Sin embargo,
esta es una comparación que puede cuestionarse. Los contenedores no son solo máquinas
virtuales ligeras. De acuerdo, entonces, ¿cuál es la descripción correcta de un contenedor?

Los contenedores son procesos especialmente encapsulados y protegidos que se ejecutan


en el sistema host.

Los contenedores aprovechan una gran cantidad de características y funciones primitivas


disponibles en el sistema operativo Linux. Las más importantes son los espacios
de nombres y cgroups. Todos los procesos que se ejecutan en contenedores comparten
el mismo kernel de Linux del sistema operativo del host subyacente. Esta es una
diferencia fundamental con respecto a las máquinas virtuales, ya que cada máquina
virtual contiene su propio sistema operativo completo.
[ 48 ]
Capítulo 3

Los tiempos de inicio de un contenedor típico se pueden medir en milisegundos,


mientras que una máquina virtual normalmente necesita varios segundos o minutos
para iniciarse. Las máquinas virtuales están diseñadas para durar mucho tiempo. Es un
objetivo primordial de cada ingeniero de operaciones maximizar el tiempo de actividad
de sus máquinas virtuales. Por el contrario, los contenedores están diseñados para ser
efímeros. Vienen y van en una cadencia rápida.

Primero vamos a ver una descripción general de la arquitectura que nos permite
ejecutar contenedores.

Arquitectura
Aquí tenemos un diagrama arquitectónico de todas las piezas:

Arquitectura general de Docker

En la parte inferior de la imagen anterior, tenemos el sistema operativo Linux con


sus cgroups, espacios de nombres y funciones de capa, así como otras funcionalidades
que no necesitamos mencionar explícitamente aquí. Luego, hay una capa intermediaria
compuesta por containerd y runc. Encima de todo está el motor de Docker. El motor
de Docker ofrece una interfaz RESTful para el mundo exterior a la que puede acceder
cualquier herramienta, como Docker CLI, Docker para Mac y Docker para Windows
o Kubernetes, por nombrar algunas.

Ahora vamos a describir los componentes principales más detalladamente.

[ 49 ]
Trabajar con contenedores

Espacios de nombres
Los espacios de nombres de Linux habían existido durante años antes de que Docker los
utilizara para sus contenedores. Un espacio de nombres es una abstracción de recursos
globales como sistemas de archivos, acceso a la red, árbol de procesos (también denominado
espacio de nombres PID) o los identificadores de grupo de sistema e identificadores
de usuario. Un sistema Linux se inicia con una sola instancia de cada tipo de espacio de
nombres. Después del inicio, se pueden crear o unir espacios de nombres adicionales.

Los espacios de nombres de Linux se originaron en 2002 en el kernel 2.4.19. En la versión


3.8 del kernel, se introdujeron espacios de nombres de usuario y, con ella, los espacios
de nombres estaban listos para su uso por parte de los contenedores.

Si empaquetamos un proceso en ejecución, por ejemplo, en un espacio de nombres


del sistema de archivos, este proceso tiene la ilusión de que tiene su propio sistema de
archivos completo. Esto, por supuesto, no es cierto; solo es un sistema de archivos virtual.
Desde la perspectiva del host, el proceso contenido obtiene una subsección blindada del
sistema de archivos general. Es como un sistema de archivos en un sistema de archivos:

Lo mismo se aplica a todos los demás recursos globales para los que existan espacios
de nombres. El espacio de nombres de ID de usuario es otro ejemplo. Al tener un espacio
de nombres de usuario, ahora podemos definir un jdoe de usuario muchas veces
en el sistema, ya que existe dentro de su propio espacio de nombres.

[ 50 ]
Capítulo 3

El espacio de nombres PID es lo que impide que los procesos de un contenedor vean
procesos de otro contenedor o interactúen con ellos. Un proceso podría tener el PID
aparente 1 dentro de un contenedor, pero si lo examinamos desde el sistema host, tendría
un PID ordinario, como 334:

Árbol de procesos en un host de Docker

En un espacio de nombres determinado, podemos ejecutar uno o varios procesos.


Eso es importante cuando hablamos de contenedores, y lo hemos experimentado
cuando ejecutamos otro proceso en un contenedor en ejecución.

Grupos de control (cgroups)


Los cgroups de Linux se utilizan para limitar, administrar y aislar el uso de recursos de las
colecciones de procesos que se ejecutan en un sistema. Los recursos son tiempo de CPU,
memoria del sistema, ancho de banda de red o combinaciones de estos recursos, etcétera.

Los ingenieros de Google implementaron originalmente esta función a partir de 2006.


La funcionalidad de cgroups se fusionó en la línea principal del kernel de Linux en la
versión del kernel 2.6.24 lanzada en enero de 2008.

Mediante el uso de cgroups, los administradores pueden limitar los recursos que
pueden consumir los contenedores. Con esto, se puede evitar, por ejemplo, el clásico
problema del vecino ruidoso, donde un proceso malintencionado que se ejecuta en un
contenedor consume todo el tiempo de la CPU o reserva cantidades masivas de RAM y,
como tal, priva de recursos a todos los demás procesos que se ejecutan en el host, estén
en contenedor o no.

[ 51 ]
Trabajar con contenedores

Sistema de archivos Union (UnionFS)


UnionFS es el componente principal de lo que se conoce como "imágenes de contenedor".
Explicaremos las imágenes de contenedor en detalle en el capítulo siguiente. Por ahora,
simplemente queremos entender un poco mejor lo que es un UnionFS y cómo funciona.
UnionFS se utiliza principalmente en Linux, y permite que los archivos y directorios
de distintos sistemas de archivos se superpongan y con él formen un único sistema
de archivos coherente. En este contexto, los sistemas de archivos individuales
se denominan "ramificaciones". El contenido de los directorios que tienen la misma ruta
dentro de las ramificaciones fusionadas se verá junto en un único directorio fusionado,
dentro del nuevo sistema de archivos virtual. Al fusionar las ramificaciones, se especifica
la prioridad entre ramificaciones. De esta manera, cuando dos ramificaciones contienen
el mismo archivo, la que tiene mayor prioridad se muestra en el sistema de archivos final.

Código de contenedores
La base sobre la que se construye el motor del Docker también se denomina código del
contenedor, y está formada por los dos componentes: runc y containerd.

Originalmente, Docker se construyó de forma monolítica y contenía todas las funciones


necesarias para ejecutar contenedores. Con el tiempo, esto era demasiado rígido
y Docker comenzó a extraer partes de la funcionalidad a sus propios componentes.
Dos componentes importantes son runc y containerd.

Runc
Runc es un runtime de contenedor portátil y ligero. Proporciona compatibilidad
completa con los espacios de nombres de Linux, así como compatibilidad nativa con
todas las funciones de seguridad disponibles en Linux, como SELinux, AppArmor,
seccomp y cgroups.

Runc es una herramienta para la generación y la ejecución de contenedores según las


especificaciones de la Open Container Initiative (OCI). Es un formato de configuración
con una especificación formal, que se rige por Open Container Project (OCP) de acuerdo
con las indicaciones de Linux Foundation.

Containerd
Runc es una implementación de bajo nivel de un runtime de contenedor; containerd
se ejecuta encima y agrega características de nivel superior, como transferencia
y almacenamiento de imágenes, ejecución de contenedores y supervisión, así como
archivos adjuntos de red y almacenamiento de información. Con esto se gestiona el ciclo
de vida completo de los contenedores. Containerd es la implementación de referencia
de las especificaciones de OCI y es, de lejos, el runtime de contenedor más popular
y utilizado.

[ 52 ]
Capítulo 3

Containerd fue donado y aceptado por la CNCF en 2017. Existen implementaciones


alternativas de la especificación de OCI. Algunas de ellas son RKT de CoreOS, CRI-O
de RedHat y LXD de Linux Containers. Sin embargo, containerd en este momento
es, de lejos, el runtime de contenedor más popular y es el runtime predeterminado
de Kubernetes 1.8 o posterior y la plataforma Docker.

Resumen
En este capítulo, has aprendido a trabajar con contenedores basados en imágenes
existentes. Te hemos mostrado cómo ejecutar, parar, arrancar y eliminar un contenedor.
Luego, hemos examinado los metadatos de un contenedor, hemos extraído registros
del mismo y hemos aprendido a ejecutar un proceso arbitrario en un contenedor
en ejecución. Por último, pero no menos importante, hemos profundizado e investigado
sobre cómo funcionan los contenedores y qué características del sistema operativo
Linux subyacente utilizan.

En el siguiente capítulo, aprenderás qué son las imágenes de contenedor y cómo


podemos crear y compartir nuestras propias imágenes personalizadas. También
analizaremos las prácticas recomendadas más utilizadas cuando se crean imágenes
personalizadas, como minimizar su tamaño y aprovechar la caché de imágenes.
¡Sigue leyendo!

Preguntas
Para evaluar el progreso de tu aprendizaje, responde a las siguientes preguntas:

1. ¿Cuáles son los estados de un contenedor?


2. ¿Qué comando nos ayuda a averiguar qué se está ejecutando actualmente
en nuestro host?
3. ¿Qué comando se utiliza para enumerar los ID de todos los contenedores?

Lectura adicional
Los siguientes artículos ofrecen más información relacionada con los temas que hemos
explicado en este capítulo (pueden estar en inglés):

• Contenedor Docker en http://dockr.ly/2iLBV2I


• Introducción a los contenedores en http://dockr.ly/2gmxKWB
• Aislar contenedores con un espacio de nombres de usuario en
http://dockr.ly/2gmyKdf
• Limitar los recursos del contenedor en http://dockr.ly/2wqN5Nn

[ 53 ]
Creación y gestión de
imágenes de contenedores
En el capítulo anterior, aprendimos qué son los contenedores y cómo ejecutarlos,
detenerlos, eliminarlos, enumerarlos e inspeccionarlos. Extrajimos la información
de registro de algunos contenedores, ejecutamos otros procesos dentro de un contenedor
en ejecución y finalmente explicamos en detalle la anatomía de los contenedores.
Cada vez que ejecutamos un contenedor, lo creamos usando una imagen de contenedor.
En este capítulo, nos familiarizaremos con estas imágenes de contenedores.
Aprenderemos en detalle lo que son, cómo crearlas y cómo distribuirlas.

En este capítulo, abordaremos los siguientes temas:

• ¿Qué son las imágenes?


• Creación de imágenes
• Compartir o enviar imágenes

Después de terminar este capítulo, serás capaz de hacer lo siguiente:

• Nombrar tres de las características más importantes de una imagen


de contenedor
• Crear una imagen personalizada de forma interactiva cambiando la capa
del contenedor y confirmándola
• Escribir un archivo de Docker simple usando palabras clave como FROM,
COPY, RUN, CMD y ENTRYPOINT para generar una imagen personalizada
• Exportar una imagen existente mediante docker image save e importarla
a otro host de Docker usando docker image load
• Escribir un archivo de Docker de dos pasos que reduzca al mínimo el tamaño
de la imagen resultante incluyendo solo los artefactos resultantes (binarios)
en la imagen final

[ 55 ]
Creación y gestión de imágenes de contenedores

¿Qué son las imágenes?


En Linux, todo se integra en un solo archivo. Todo el sistema operativo es básicamente
un sistema de archivos con archivos y carpetas almacenados en el disco local. Esto
es algo importante que debemos recordar cuando examinamos el concepto de "imagen
de contenedor". Como veremos, una imagen es básicamente un gran archivo que
contiene un sistema de archivos. En concreto, contiene un sistema de archivos en capas.

El sistema de archivos en capas


Las imágenes de contenedor son plantillas desde las que se crean los contenedores. Estas
imágenes no son solo un bloque monolítico, sino que se componen de muchas capas.
La primera capa de la imagen se denomina "capa base":

La imagen como una pila de capas

Cada capa individual contiene archivos y carpetas. Cada capa solo contiene los cambios
del sistema de archivos con respecto a las capas subyacentes. Docker utiliza un sistema
de archivos de unión, como se explica en el Capítulo 3, Trabajar con contenedores, para
crear un sistema de archivos virtual fuera del conjunto de capas. Un controlador
de almacenamiento gestiona los detalles sobre la forma en que estas capas interactúan
entre sí. Existen diferentes controladores de almacenamiento disponibles que tienen
ventajas y desventajas en función de cada situación.

Las capas de una imagen de contenedor son inmutables. "Inmutable" significa


que una vez generada, la capa no se puede cambiar nunca. La única operación posible
que afecta a la capa es la eliminación física de la misma. Esta inmutabilidad de las capas
es importante porque ofrece una gran cantidad de oportunidades, como veremos.

[ 56 ]
Capítulo 4

En la siguiente imagen, podemos ver a lo que podría asemejarse una imagen


personalizada para una aplicación web que utilice nginx como servidor web:

Una imagen personalizada de ejemplo basada en Alpine y Nginx

Nuestra capa base aquí consta de la distribución Linux Alpine. Luego tenemos una capa
donde Nginx se añade encima de Alpine. Por último, la tercera capa contiene todos los
archivos que componen la aplicación web, como archivos HTML, CSS y JavaScript.

Como se ha dicho anteriormente, cada imagen comienza con una imagen base.
Normalmente, esta imagen base es una de las imágenes oficiales que se encuentran
en Docker Hub, como Linux distro, Alpine, Ubuntu o CentOS. Sin embargo, también
es posible crear una imagen desde cero.

Docker Hub es un registro público para imágenes de contenedores.


Es un lugar central adecuado para compartir imágenes de contenedores
públicas.

Cada capa solo contiene el delta de los cambios en relación con el conjunto anterior
de capas. El contenido de cada capa se asigna a una carpeta especial del sistema host,
que suele ser una subcarpeta de /var/lib/docker/.

Como las capas son inmutables, se pueden almacenar en caché sin que se queden
obsoletas. Esta es una gran ventaja, como veremos más adelante.

[ 57 ]
Creación y gestión de imágenes de contenedores

La capa de contenedor grabable


Como hemos comentado, una imagen de contenedor se compone de un conjunto
de capas inmutables o de solo lectura. Cuando el motor de Docker crea un contenedor
a partir de dicha imagen, añade una capa de contenedor grabable sobre este conjunto
de capas inmutables. Ahora nuestra pila tiene el siguiente aspecto:

La capa de contenedor grabable

La capa de contenedor está marcada como de lectura/escritura. Otra ventaja


de la inmutabilidad de las capas de imagen es que se pueden compartir entre muchos
contenedores creados a partir de esta imagen. Todo lo que necesitamos es una capa
de contenedor pequeña y grabable para cada contenedor:

Varios contenedores que comparten las mismas capas de imagen

Con esta técnica, lo que se consigue es una gran reducción de los recursos que
se consumen. Además, ayuda a disminuir el tiempo de carga de un contenedor, ya que
solo se tiene que crear una capa de contenedor pequeña una vez que las capas de imagen
se han cargado en la memoria, lo que solo ocurre para el primer contenedor.

[ 58 ]
Capítulo 4

Copy-on-write (copiar al escribir)


Docker utiliza la técnica "copy-on-write" cuando se gestionan imágenes. "Copy-on-write"
es una estrategia para compartir y copiar archivos con la máxima eficiencia. Si una capa
utiliza un archivo o carpeta que está disponible en una de las capas inferiores,
simplemente lo utiliza. Si, por el contrario, una capa quiere modificar, por ejemplo,
un archivo de una capa inferior, primero copia este archivo en la capa de destino
y luego lo modifica. En la siguiente imagen, podemos ver lo que esto significa:

Copy-on-write (copiar al escribir)

La segunda capa quiere modificar Archivo 2, que está presente en la capa base. Por tanto,
lo ha copiado y después lo ha modificado. Supongamos ahora que estamos en la capa
superior de la imagen anterior. Esta capa usará Archivo 1 de la capa base y Archivo 2
y Archivo 3 de la segunda capa.

Controladores de gráficos
Los controladores de gráficos son los que habilitan el sistema de archivos de unión.
Los controladores de gráficos también se denominan "controladores de almacenamiento"
y se utilizan al gestionar las imágenes de contenedor en capas. Un controlador de gráficos
consolida las múltiples capas de una imagen en un sistema de archivos raíz para el
espacio de nombres de montaje del contenedor. O, dicho de otra forma, el controlador
controla cómo se almacenan y administran las imágenes y los contenedores en el host
de Docker.

Docker admite varios controladores de gráficos diferentes con una arquitectura


conectable. El controlador preferido es overlay2 seguido de overlay.

[ 59 ]
Creación y gestión de imágenes de contenedores

Creación de imágenes
Hay tres formas de crear una nueva imagen de contenedor en el sistema. La
primera es mediante la creación interactiva de un contenedor que contenga todas
las incorporaciones y cambios que queramos y la aplicación de esos cambios a una
nueva imagen. La segunda y la más importante es utilizar un archivo de Docker para
describir lo que hay en la nueva imagen y luego construir esta imagen usando ese
archivo de Docker como manifiesto. Por último, la tercera forma de crear una imagen
es importarla en el sistema desde un "tarball".

Veamos ahora estos tres procedimientos en detalle.

Creación de imágenes interactivas


La primera forma de crear una imagen personalizada es creando de forma interactiva
un contenedor. Es decir, empezamos con una imagen base que queremos utilizar como
plantilla y ejecutamos un contenedor de ella de forma interactiva. Supongamos que esta
es la imagen alpine. El comando para ejecutar el contenedor sería el siguiente:
$ docker container run -it --name sample alpine /bin/sh

De forma predeterminada, el contenedor alpine no tiene instalada la herramienta


ping. Supongamos que queremos crear una nueva imagen personalizada que tenga
ping instalado. Dentro del contenedor, podemos ejecutar el siguiente comando:

/ # apk update && apk add iputils

Este comando utiliza el administrador de paquetes de Alpine apk para instalar


la biblioteca iputils, de la que ping forma parte. El resultado del comando anterior
debe ser el siguiente:
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/APKINDEX.
tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/community/x86_64/
APKINDEX.tar.gz
v3.7.0-50-gc8da5122a4 [http://dl-cdn.alpinelinux.org/alpine/v3.7/main]
v3.7.0-49-g06d6ae04c3 [http://dl-cdn.alpinelinux.org/alpine/v3.7/
community]
OK: 9046 distinct packages available
(1/2) Installing libcap (2.25-r1)
(2/2) Installing iputils (20121221-r8)
Executing busybox-1.27.2-r6.trigger
OK: 4 MiB in 13 packages

[ 60 ]
Capítulo 4

Ahora podemos usar ping, como se muestra en el siguiente fragmento de código:


/ # ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.028 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.044 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.049 ms
^C
--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2108ms
rtt min/avg/max/mdev = 0.028/0.040/0.049/0.010 ms

Una vez terminada nuestra personalización, podemos salir del contenedor escribiendo
exit en el símbolo del sistema. Si ahora enumeramos todos los contenedores con docker
container ls -a, podemos ver que nuestro contenedor de ejemplo tiene un estado
Exited (Abandonado), pero aún existe en el sistema:

$ docker container ls -a | grep sample


eff7c92a1b98 alpine "/bin/sh" 2 minutes ago Exited (0)
...

Si queremos ver lo que ha cambiado en nuestro contenedor en relación con la imagen base,
podemos utilizar el comando docker container diff de Docker de la siguiente manera:
$ docker container diff sample

El resultado debe presentar una lista de todas las modificaciones realizadas en el sistema
de archivos del contenedor:
C /bin
C /bin/ping
C /bin/ping6
A /bin/traceroute6
C /etc/apk
C /etc/apk/world
C /lib/apk/db
C /lib/apk/db/installed
C /lib/apk/db/lock
C /lib/apk/db/scripts.tar
C /lib/apk/db/triggers
C /root
A /root/.ash_history
C /usr/lib
A /usr/lib/libcap.so.2
A /usr/lib/libcap.so.2.25
C /usr/sbin

[ 61 ]
Creación y gestión de imágenes de contenedores

C /usr/sbin/arping
A /usr/sbin/capsh
A /usr/sbin/clockdiff
A /usr/sbin/getcap
A /usr/sbin/getpcaps
A /usr/sbin/ipg
A /usr/sbin/rarpd
A /usr/sbin/rdisc
A /usr/sbin/setcap
A /usr/sbin/tftpd
A /usr/sbin/tracepath
A /usr/sbin/tracepath6
C /var/cache/apk
A /var/cache/apk/APKINDEX.5022a8a2.tar.gz
A /var/cache/apk/APKINDEX.70c88391.tar.gz
C /var/cache/misc

En la lista anterior, A significa añadido y C significa modificado. Si tuviéramos algún archivo


eliminado, entonces tendría el prefijo D.

Ahora podemos utilizar el comando docker container commit para guardar nuestras
modificaciones y crear una nueva imagen a partir de ellas:
$ docker container commit sample my-alpine
sha256:44bca4141130ee8702e8e8efd1beb3cf4fe5aadb62a0c69a6995afd49c
2e7419

Con el comando anterior, hemos especificado que la nueva imagen se llamará


my-alpine. El resultado generado por el comando anterior corresponde al ID
de la imagen recién generada. Podemos comprobarlo enumerando todas las imágenes
de nuestro sistema, de la siguiente manera:
$ docker image ls

Podemos ver este ID de imagen (acortado) de la siguiente manera:


REPOSITORY TAG IMAGE ID CREATED SIZE
my-alpine latest 44bca4141130 About a minute ago 5.64MB
...

Podemos ver que la imagen llamada my-alpine, tiene el ID esperado 44bca4141130


y que automáticamente se le asigna la etiqueta latest. Esto es así porque no hemos
definido explícitamente una etiqueta. En este caso, Docker siempre asigna de forma
predeterminada la etiqueta latest.

[ 62 ]
Capítulo 4

Si queremos ver cómo se ha creado nuestra imagen personalizada, podemos utilizar


el comando history de la siguiente manera:
$ docker image history my-alpine

De esta forma se imprimirá la lista de capas de las que consta nuestra imagen:
IMAGE CREATED CREATED BY SIZE
COMMENT
44bca4141130 3 minutes ago /bin/sh 1.5MB
e21c333399e0 6 weeks ago /bin/sh -c #... 0B
<missing> 6 weeks ago /bin/sh -c #... 4.14MB

La primera capa de la lista anterior es la que acabamos de crear añadiendo el paquete


iputils.

Uso de Dockerfiles
La creación manual de imágenes personalizadas mostrada en la sección anterior
de este capítulo es muy útil para la exploración, la creación de prototipos o la realización
de estudios de viabilidad. Pero tiene un gran inconveniente: es un proceso manual y,
por lo tanto, no se puede repetir ni escalar. También es muy propensa a errores como
cualquier tarea ejecutada manualmente por una persona. Tiene que haber una mejor
forma de hacerlo.

Aquí es donde entra en juego lo que se denomina "Dockerfile". Dockerfile es un archivo


de texto que normalmente se llama Dockerfile, literalmente, según su denominación
en inglés. Contiene instrucciones sobre cómo crear una imagen de contenedor
personalizada. Es una forma declarativa de crear imágenes.

Enfoque declarativo frente a imperativo


En informática, en general y con Docker específicamente, se utiliza
a menudo una manera declarativa de definir una tarea. Se describe
el resultado esperado y se permite que el sistema averigüe cómo lograr
este objetivo, en lugar de dar instrucciones paso a paso al sistema sobre
cómo lograr este resultado deseado. Este último es el enfoque imperativo.

Echemos un vistazo a un Dockerfile de ejemplo:


FROM python:2.7
RUN mkdir -p /app
WORKDIR /app
COPY ./requirements.txt /app/
RUN pip install -r requirements.txt
CMD ["python", "main.py"]

[ 63 ]
Creación y gestión de imágenes de contenedores

Se trata de Dockerfile, ya que se utiliza para incluir una aplicación Python 2.7 en
contenedores. Como vemos, el archivo tiene seis líneas y cada una de ellas comienza
por una palabra clave como FROM, RUN o COPY. Es una convención escribir las palabras
clave en mayúsculas, pero no es obligatorio.

Cada línea del Dockerfile da lugar a una capa en la imagen resultante. En la siguiente
imagen, la imagen se dibuja al revés en comparación con las ilustraciones anteriores
de este capítulo, que muestran una imagen como un conjunto de capas. Aquí, la capa
base se muestra en la parte superior. No dejes que esto te confunda. En realidad, la capa
base siempre es la capa inferior de la pila:

La relación de Dockerfile y las capas en una imagen

Ahora examinemos las palabras clave individuales con más detalle.

La palabra clave FROM


La mayoría de los Dockerfiles comienzan con la palabra clave FROM. Con ella definimos
a partir de qué imagen base queremos empezar a crear nuestra imagen personalizada.
Si queremos crear la imagen a partir de CentOS 7, por ejemplo, tendríamos la siguiente
línea en el Dockerfile:
FROM centos:7

En Docker Hub, hay imágenes adaptadas u oficiales para todas las distribuciones
Linux más importantes, así como para todas las plataformas de desarrollo o lenguajes
importantes, como Python, Node JS, Ruby y Go, entre otros muchos. En función
de lo que necesitemos, debemos seleccionar la imagen base más adecuada.

Por ejemplo, si quiero incluir una aplicación Python 2.7 en un contenedor, tal vez quiera
seleccionar la imagen oficial python:2.7 correspondiente.

Si queremos empezar desde cero, también podemos usar la siguiente instrucción:


FROM scratch
[ 64 ]
Capítulo 4

Esto es útil en el contexto de la creación de imágenes súper mínimas que solo contienen, por
ejemplo, un único binario, el ejecutable real vinculado estáticamente, como Hello-World.
La imagen nueva es literalmente una imagen base vacía.

FROM scratch se corresponde a un no-op (ninguna operación) en el Dockerfile y como


tal no genera una capa en la imagen de contenedor resultante.

La palabra clave RUN


La siguiente palabra clave importante es RUN. El argumento para RUN es cualquier
comando Linux válido, como el siguiente:
RUN yum install -y wget

El comando anterior utiliza el administrador de paquetes de CentOS yum para instalar


el paquete wget en el contenedor en ejecución. Este comando presupone que nuestra
imagen base es CentOS o RHEL. Si tuviéramos Ubuntu como nuestra imagen base,
el comando tendría un aspecto similar al siguiente:
RUN apt-get update && apt-get install -y wget

Tendría este aspecto porque Ubuntu utiliza apt-get como administrador de paquetes.
Del mismo modo, podríamos definir una línea con RUN de este modo:
RUN mkdir -p /app && cd /app

También podríamos hacer esto:


RUN tar -xJC /usr/src/python --strip-components=1 -f python.tar.xz

En este caso, la primera línea crea una carpeta /app en el contenedor y se desplaza hasta
ella, y la última guarda un archivo en una ubicación determinada. Está bien, e incluso es
recomendable, formatear un comando Linux usando más de una línea física, de este modo:
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
libexpat1 \
libffi6 \
libgdbm3 \
libreadline7 \
libsqlite3-0 \
libssl1.1 \
&& rm -rf /var/lib/apt/lists/*

[ 65 ]
Creación y gestión de imágenes de contenedores

Si usamos más de una línea, necesitamos añadir una barra diagonal inversa (\) al final
de las líneas para indicar al shell que el comando continúa en la línea siguiente.

Intenta averiguar qué hace el comando anterior.

Las palabras clave COPY y ADD


Las palabras clave COPY y ADD son muy importantes ya que, al final, queremos
añadir algo de contenido a una imagen base existente para convertirla en una imagen
personalizada. La mayoría de las veces, hay algunos archivos fuente de, por ejemplo, una
aplicación web o algunos binarios de una aplicación compilada.

Estas dos palabras clave se utilizan para copiar archivos y carpetas desde el host a la
imagen que estamos creando. Las dos palabras clave son muy similares, con la salvedad
de que la palabra clave ADD también nos permite copiar y descomprimir archivos TAR, así
como proporcionar una URL como origen para los archivos y carpetas que se van a copiar.

Echemos un vistazo a algunos ejemplos de cómo se pueden utilizar estas dos


palabras clave:
COPY . /app
COPY ./web /app/web
COPY sample.txt /data/my-sample.txt
ADD sample.tar /app/bin/
ADD http://example.com/sample.txt /data/

En las líneas anteriores de código:

• La primera línea copia todos los archivos y carpetas del directorio actual
de forma recursiva a la carpeta /app dentro de la imagen del contenedor
• La segunda línea copia todo lo que hay en la subcarpeta web en la carpeta
de destino, /app/web.
• La tercera línea copia un solo archivo, sample. txt, en la carpeta de destino,
/data, y al mismo tiempo, lo renombra como My-sample. txt
• La cuarta instrucción descomprime el archivo sample.tar en la carpeta
de destino, /app/bin.
• Finalmente, la última instrucción copia el archivo remoto, sample. txt,
en el archivo de destino, /data.

Se permiten caracteres comodín en la ruta de origen. Por ejemplo, la siguiente


instrucción copia todos los archivos que empiezan por sample en la carpeta mydir
dentro de la imagen:
COPY ./sample* /mydir/

[ 66 ]
Capítulo 4

Desde el punto de vista de la seguridad, es importante saber que, de forma


predeterminada, todos los archivos y carpetas dentro de la imagen tendrán un ID de
usuario (UID) y un ID de grupo (GID) de 0. Lo bueno es que tanto para ADD como
para COPY, podemos cambiar la propiedad que los archivos tendrán dentro de la imagen
usando la etiqueta opcional --chown, de la siguiente manera:
ADD --chown=11:22 ./data/files* /app/data/

La instrucción anterior copiará todos los archivos que comiencen con el nombre web
y los guardará en la carpeta /app/data de la imagen, y al mismo tiempo asignará
el usuario 11 y el grupo 22 a estos archivos.

En lugar de números, también se podrían utilizar nombres para el usuario y el


grupo, pero entonces estas entidades tendrían que estar ya definidas en el sistema
de archivos raíz de la imagen en /etc/passwd y /etc/group respectivamente, ya que,
de lo contrario, la creación de la imagen fallaría.

La palabra clave WORKDIR


La palabra clave WORKDIR define el directorio de trabajo o el contexto que se utiliza
cuando se ejecuta un contenedor desde nuestra imagen personalizada. Por lo tanto,
si quiero establecer el contexto en la carpeta /app/bin dentro de la imagen, mi expresión
en Dockerfile tendría que ser similar a la siguiente:
WORKDIR /app/bin

Cualquier actividad que ocurra dentro de la imagen después de la línea anterior usará
este directorio como directorio de trabajo. Es muy importante tener en cuenta que los
siguientes dos fragmentos de un Dockerfile no son los mismos:
RUN cd /app/bin
RUN touch sample.txt

Compara el código anterior con el código siguiente:


WORKDIR /app/bin
RUN touch sample.txt

El primero creará el archivo en la raíz del sistema de archivos de imagen, mientras


que el último creará el archivo en la ubicación prevista en la carpeta /app/bin. Sola
la palabra clave WORKDIR establece el contexto en las capas de la imagen. El comando
cd solo no es persistente de una capa a otra.

[ 67 ]
Creación y gestión de imágenes de contenedores

Las palabras clave CMD y ENTRYPOINT


Las palabras clave CMD y ENTRYPOINT son especiales. Mientras que todas las demás
palabras clave definidas para un Dockerfile se ejecutan en el momento en que Docker
Builder crea la imagen, estas dos son en realidad definiciones de lo que ocurrirá cuando
se inicie un contenedor a partir de la imagen que definimos. Cuando el runtime de
contenedor inicia un contenedor, necesita saber cuál será el proceso o aplicación que tiene
que ejecutarse dentro de este contenedor. Esto es exactamente para lo que se utilizan CMD
y ENTRYPOINT: para decirle a Docker cuál es el proceso de inicio y cómo iniciar ese proceso.

Ahora bien, las diferencias entre CMD y ENTRYPOINT son sutiles y, sinceramente,
la mayoría de los usuarios no entienden completamente estas palabras clave ni
las utilizan de la manera prevista. Afortunadamente, en la mayoría de los casos,
esto no supone un problema y el contenedor se ejecutará de todos modos; solo que
la gestión del mismo no es tan sencilla como podría llegar a ser.

Para entender mejor cómo utilizar las dos palabras clave, veamos qué aspecto tiene
un comando o una expresión típica de Linux; tomemos, por ejemplo, la utilidad ping
como ejemplo, de la siguiente manera:
$ ping 8.8.8.8 -c 3

En la expresión anterior, ping es el comando y 8.8.8.8-c 3 son los parámetros de este


comando. Echemos un vistazo a otra expresión:
$ wget -O - http://example.com/downloads/script.sh

De nuevo, en la expresión anterior wget es el comando y -O - http://example.com/


downloads/script.sh son los parámetros.

Ahora que hemos lidiado con esto, podemos volver a CMD y ENTRYPOINT. ENTRYPOINT
se utiliza para definir el comando de la expresión, mientras que CMD se utiliza para
definir los parámetros del comando. Así, un Dockerfile que utilizara alpine como
imagen base y definiera ping como el proceso que se debe ejecutar en el contenedor,
tendría el siguiente aspecto:
FROM alpine:latest
ENTRYPOINT ["ping"]
CMD ["8.8.8.8", "-c", "3"]

Tanto para ENTRYPOINT como para CMD, los valores se formatean como una matriz
de cadenas JSON, donde los elementos individuales corresponden a los tokens de la
expresión separados por espacios en blanco. Esta es la forma preferida de definir CMD
y ENTRYPOINT. También recibe el nombre de formato exec.

Otra opción consiste en utilizar lo que se denomina forma shell, por ejemplo:
CMD command param1 param2

[ 68 ]
Capítulo 4

Ahora podemos crear una imagen del Dockerfile de la siguiente manera:


$ docker image build -t pinger .

A continuación, podemos ejecutar un contenedor desde la imagen pinger que acabamos


de crear:
$ docker container run --rm -it pinger
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=37 time=19.298 ms
64 bytes from 8.8.8.8: seq=1 ttl=37 time=27.890 ms
64 bytes from 8.8.8.8: seq=2 ttl=37 time=30.702 ms

Lo mejor de esto es que ahora puedo invalidar la parte CMD que he definido en el Dockerfile
(recuerda que era ["8.8.8.8", "-c", "3"]) cuando cree un nuevo contenedor
añadiendo los valores nuevos al final de la expresión docker container run:
$ docker container run --rm -it pinger -w 5 127.0.0.1

Esto hará que el contenedor haga ping al bucle invertido durante 5 segundos.

Si queremos invalidar lo que se define en ENTRYPOINT en el Dockerfile, necesitamos usar


el parámetro --ENTRYPOINT en la expresión docker container run . Supongamos
que queremos ejecutar un shell en el contenedor en lugar del comando ping. Podríamos
hacerlo utilizando el siguiente comando:
$ docker container run --rm -it --entrypoint /bin/sh pinger

Enseguida nos encontraremos dentro del contenedor. Escribe exit para salir del contenedor.

Como ya he mencionado, no tenemos que seguir necesariamente las prácticas


recomendadas y definir el comando a través de ENTRYPOINT y los parámetros a través
de CMD; en su lugar podemos introducir la expresión completa como un valor de CMD y
funcionará igualmente:
FROM alpine:latest
CMD wget -O - http://www.google.com

Aquí, incluso he utilizado la forma shell para definir la palabra clave CMD. Pero ¿qué
sucede realmente en esta situación, donde ENTRYPOINT no está definida? Si dejas
ENTRYPOINT sin definir, tendrá el valor predeterminado /bin/sh-c, y el valor de CMD
se pasará como cadena al comando del shell. De este modo, la definición anterior tendría
como resultado la introducción del siguiente proceso para su ejecución dentro del
contenedor:
/bin/sh -c "wget -O - http://www.google.com"

[ 69 ]
Creación y gestión de imágenes de contenedores

Por consiguiente, /bin/sh es el proceso principal que se ejecuta dentro del contenedor,
e iniciará un nuevo proceso secundario para ejecutar la utilidad wget.

Un Dockerfile complejo
Hemos explicado las palabras clave más importantes usadas en los Dockerfiles. Echemos
un vistazo a un ejemplo realista y un tanto complejo de un Dockerfile. El lector atento
podría darse cuenta de que tiene un aspecto muy similar al primer Dockerfile que
presentamos en este capítulo. Este es el contenido:
FROM node:9.4
RUN mkdir -p /app
WORKDIR /app
COPY package.json /app/
RUN npm install
COPY . /app
ENTRYPOINT ["npm"]
CMD ["start"]

Vale, ¿qué está ocurriendo aquí? Evidentemente, se trata de un Dockerfile que se utiliza
para crear una imagen para una aplicación Node.js; podemos deducirlo porque se utiliza
el node:9.4 de la imagen base. A continuación, la segunda línea es una instrucción para
crear una carpeta /app en el sistema de archivos de la imagen. La tercera línea define
el directorio de trabajo o el contexto de la imagen como esta nueva carpeta /app. Luego,
en la línea cuatro, copiamos un archivo package.json en la carpeta /app de la imagen.
Después, en la quinta línea, ejecutamos el comando npm install dentro del contenedor;
recuerda que nuestro contexto es la carpeta /app y, por lo tanto, npm encontrará allí
el archivo package.json que copiamos en la cuarta línea.

Después de instalar todas las dependencias de Node.js, copiamos el resto de los archivos
de la aplicación de la carpeta actual del host a la carpeta /app de la imagen.

Finalmente, en las dos últimas líneas, definimos cuál será el comando de inicio cuando
se ejecute un contenedor a partir de esta imagen. En nuestro caso, es npm start, que
iniciará la aplicación Node.

[ 70 ]
Capítulo 4

Creación de una imagen


Realiza los siguientes pasos para crear una imagen:

1. En el directorio principal, crea una carpeta FundamentalsOfDocker


y desplázate hasta ella:
$ mkdir ~/FundamentalsOfDocker
$ cd ~/FundamentalsOfDocker

2. En la carpeta anterior, crea una subcarpeta sample1 y desplázate hasta ella:


$ mkdir sample1 && cd sample1

3. Utiliza tu editor favorito para crear un archivo llamado Dockerfile dentro


de esta carpeta de ejemplo con el siguiente contenido:
FROM centos:7
RUN yum install -y wget

4. Guarda el archivo y sal de tu editor. De vuelta en el Terminal, ahora podemos


crear una nueva imagen de contenedor usando el Dockerfile anterior como
manifiesto o plan de construcción:
$ docker image build -t my-centos .

Ten en cuenta que hay un punto al final del comando anterior. Este comando significa que
el constructor de Docker está creando una nueva imagen llamada my-centos usando el
Dockerfile del directorio actual. Aquí, el punto al final del comando significa el directorio
actual. También podríamos escribir el comando anterior como sigue, con el mismo resultado:
$ docker image build -t my-centos -f Dockerfile .

Pero podemos omitir el parámetro -f, ya que el constructor supone que el Dockerfile
se llama literalmente Dockerfile. Solo necesitamos el parámetro -f si nuestro
Dockerfile tiene un nombre diferente o no está ubicado en el directorio actual.

El comando anterior nos ofrece este resultado (acortado):


Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM centos:7
7: Pulling from library/centos
af4b0a2388c6: Pull complete
Digest: sha256:2671f7a3eea36ce43609e9fe7435ade83094291055f1c96d9d1d1d
7c0b986a5d
Status: Downloaded newer image for centos:7
---> ff426288ea90

[ 71 ]
Creación y gestión de imágenes de contenedores

Step 2/2 : RUN yum install -y wget


---> Running in bb726903820c
Loaded plugins: fastestmirror, ovl
Determining fastest mirrors
* base: mirror.dal10.us.leaseweb.net
* extras: repos-tx.psychz.net
* updates: pubmirrors.dal.corespace.com
Resolving Dependencies
--> Running transaction check
---> Package wget.x86_64 0:1.14-15.el7_4.1 will be installed
...
Installed:
wget.x86_64 0:1.14-15.el7_4.1
Complete!
Removing intermediate container bb726903820c
---> bc070cc81b87
Successfully built bc070cc81b87
Successfully tagged my-centos:latest

Analicemos este resultado:

1. Primero, tenemos la siguiente línea:


Sending build context to Docker daemon 2.048kB

Lo primero que hace el constructor es empaquetar los archivos en el contexto


de la compilación actual, excluyendo los archivos y la carpeta mencionados
en el archivo .dockerignore, si existe, y enviando el archivo .tar resultante
al daemon de Docker.

2. A continuación, tenemos la siguiente línea:


Step 1/2 : FROM centos:7
7: Pulling from library/centos
af4b0a2388c6: Pull complete
Digest: sha256:2671f7a...
Status: Downloaded newer image for centos:7
---> ff426288ea90

[ 72 ]
Capítulo 4

La primera línea nos indica qué paso del Dockerfile está ejecutando actualmente
el constructor. Aquí, solo tenemos dos instrucciones en el Dockerfile y estamos en
el paso 1 de 2. También podemos ver cuál es el contenido de esa sección. Aquí está
la declaración de la imagen base sobre la que queremos crear nuestra imagen
personalizada. Lo que hace el constructor es extraer esta imagen de Docker
Hub si no está aún disponible en la caché local. La última línea del fragmento
de código anterior indica qué ID de la capa recién creada asigna el constructor.

3. Este es el siguiente paso. Lo he acortado aún más que el anterior para centrarnos
en la parte esencial:
Step 2/2 : RUN yum install -y wget
---> Running in bb726903820c
...
...
Removing intermediate container bb726903820c
---> bc070cc81b87

Aquí, de nuevo, la primera línea nos indica que estamos en el paso 2 de 2.


También nos muestra la entrada respectiva del Dockerfile. En la línea dos,
podemos ver Running in bb726903820c, que nos dice que el constructor
ha creado un contenedor con ID bb726903820c dentro, que ejecuta el comando
RUN. Hemos omitido el resultado del comando yum install-y wget en el
fragmento de código, ya que no es importante en esta sección. Una vez finalizado
el comando, el constructor detiene el contenedor, lo asigna a una nueva capa y,
a continuación, elimina el contenedor. La nueva capa tiene el ID bc070cc81b87,
en este caso concreto.

4. Al final del resultado, encontramos las siguientes dos líneas:


Successfully built bc070cc81b87
Successfully tagged my-centos:latest

Esto nos dice que a la imagen personalizada resultante se le ha asignado el ID


bc070cc81b87 y que se ha etiquetado con el nombre my-centos:latest.

[ 73 ]
Creación y gestión de imágenes de contenedores

Entonces, ¿cómo funciona el constructor exactamente? Comienza con la imagen base. A partir
de esta imagen base, una vez descargada en la caché local, crea un contenedor y ejecuta
la primera instrucción del Dockerfile dentro de este contenedor. A continuación, detiene
el contenedor y guarda los cambios realizados en el contenedor en una nueva capa
de imagen. A continuación, el constructor crea un nuevo contenedor a partir de la imagen
base y la nueva capa, y ejecuta la segunda instrucción dentro de este nuevo contenedor.
Una vez más, el resultado se guarda en una nueva capa. Este proceso se repite hasta que
se encuentra la última instrucción del Dockerfile. Después de haber guardado la última
capa de la nueva imagen, el constructor crea un identificador para esta imagen y etiqueta
la imagen con el nombre proporcionado en el comando build:

Visualización del proceso de generación de imágenes

[ 74 ]
Capítulo 4

Creación de imágenes en varios pasos


Para demostrar por qué un Dockerfile con varios pasos de creación es útil, veamos
un ejemplo de Dockerfile. Vamos a utilizar una aplicación Hello World escrita en C.
Este es el código que se encuentra dentro del archivo hello.c:
#include <stdio.h>
int main (void)
{
printf ("Hello, world!\n");
return 0;
}

Ahora, queremos incluir esta aplicación en un contenedor y escribir este Dockerfile:


FROM alpine:3.7
RUN apk update &&
apk add --update alpine-sdk
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN mkdir bin
RUN gcc -Wall hello.c -o bin/hello
CMD /app/bin/hello

Ahora, vamos a crear esta imagen:


$ docker image build -t hello-world .

Esto nos da un resultado bastante largo, ya que el constructor tiene que instalar el SDK
de Alpine, que, entre otras herramientas, contiene el compilador de C++ que necesitamos
para compilar la aplicación.

Una vez realizada la compilación, podemos mostrar la imagen y ver su tamaño como
se muestra a continuación:
$ docker image ls | grep hello-world
hello-world latest e9b... 2 minutes ago 176MB

Con un tamaño de 176 MB, la imagen resultante es demasiado grande. Al final, solo


es una aplicación Hello World. La razón de que sea tan grande es que la imagen no solo
contiene el binario Hello World, sino también todas las herramientas para compilar
y vincular la aplicación desde el código fuente. Pero esto no es muy deseable cuando
se ejecuta la aplicación, por ejemplo, en producción. Idealmente, solo queremos tener
el binario resultante en la imagen y no todo un SDK.

[ 75 ]
Creación y gestión de imágenes de contenedores

Es precisamente por esta razón que debemos definir Dockerfiles como multifase. Tenemos
algunas etapas que se utilizan para compilar los artefactos finales y luego una etapa
final donde usamos la mínima imagen base necesaria y copiamos en ella los artefactos.
Esto da como resultado imágenes muy pequeñas. Mira este Dockerfile revisado:
FROM alpine:3.7 AS build
RUN apk update && \
apk add --update alpine-sdk
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN mkdir bin
RUN gcc hello.c -o bin/hello
FROM alpine:3.7
COPY --from=build /app/bin/hello /app/hello
CMD /app/hello

Aquí, tenemos una primera etapa con una compilación de alias que se utiliza para
compilar la aplicación, y luego la segunda etapa utiliza la misma imagen base
alpine:3.7, pero no instala el SDK, y solo copia el binario de la etapa de compilación
utilizando el parámetro --from en esta imagen final.

Vamos a crear la imagen de nuevo del siguiente modo:


$ docker image build -t hello-world-small .

Cuando comparamos los tamaños de las imágenes, obtenemos el siguiente resultado:


$ docker image ls | grep hello-world
hello-world-small latest f98... 20 seconds ago 4.16MB
hello-world latest 469... 10 minutes ago 176MB

Hemos podido reducir el tamaño de 176 MB a 4 MB. Esta es una reducción de tamaño
por un factor de 40. Un tamaño de imagen más pequeño tiene muchas ventajas, como
una superficie de ataque más pequeña para hackers, menor consumo de memoria y disco,
tiempos de inicio más rápidos de los contenedores correspondientes y una reducción del
ancho de banda necesario para descargar la imagen de un registro, como Docker Hub.

[ 76 ]
Capítulo 4

Prácticas recomendadas de Dockerfiles


Existen algunas prácticas recomendadas que deben tenerse en cuenta cuando se crea
un Dockerfile, que son las siguientes:

• Recuerda que los contenedores están diseñados para ser efímeros. Por efímero
queremos decir que un contenedor se puede detener y destruir, y crearse
e implementarse uno nuevo con los ajustes mínimos y la configuración necesaria.
Esto significa que debemos esforzarnos por mantener al mínimo el tiempo
necesario para iniciar la aplicación que se ejecuta dentro del contenedor,
así como el tiempo necesario para finalizar o limpiar la aplicación.
• Ordena los comandos individuales en el Dockerfile de modo que
aprovechemos el almacenamiento en caché tanto como sea posible. La creación
de una capa de una imagen puede llevar una cantidad considerable de tiempo,
a veces varios minutos. Mientras desarrollamos una aplicación, tendremos que
crear la imagen de contenedor para nuestra aplicación varias veces. Queremos
mantener los tiempos de compilación al mínimo.
Cuando estamos reconstruyendo una imagen construida previamente, las únicas
capas que se reconstruyen son las que han cambiado, pero si una capa necesita
ser reconstruida, todas las capas siguientes también necesitan ser reconstruidas.
Es muy importante que recordemos esto. Observa el siguiente ejemplo:
FROM node:9.4
RUN mkdir -p /app
WORKIR /app
COPY . /app
RUN npm install
CMD ["npm", "start"]

En este ejemplo, el comando npm install de la línea cinco del Dockerfile


generalmente es el que lleva más tiempo. Una aplicación clásica de Node.
js tiene muchas dependencias externas, y todas se descargan e instalan en este
paso. Esto puede llevar unos minutos. Por lo tanto, queremos evitar ejecutar
npm install cada vez que reconstruyamos la imagen, pero un desarrollador
cambia su código fuente todo el tiempo durante el desarrollo de la aplicación.
Esto significa que la línea cuatro, el resultado del comando COPY, cambia todo
el tiempo y esta capa tiene que reconstruirse cada vez. Pero como ya comentamos
anteriormente, eso también significa que todas las capas siguientes deben
reconstruirse, que en este caso incluye el comando npm install. Para evitarlo,
podemos modificar ligeramente el Dockerfile del siguiente modo:
FROM node:9.4
RUN mkdir -p /app
WORKIR /app

[ 77 ]
Creación y gestión de imágenes de contenedores

COPY package.json /app/


RUN npm install
COPY . /app
CMD ["npm", "start"]

Aquí, en la línea cuatro, solo hemos copiado el archivo único que el comando
npm install necesita como origen, que es el archivo package.json. Este
archivo no suele cambiar en un proceso de desarrollo típico. Por lo tanto, el
comando npm install también debe ejecutarse cuando cambie el archivo
package.json. Todo el contenido restante, modificado frecuentemente, se
añade a la imagen después del comando npm install.

• Es necesario mantener el número de capas que componen tu imagen


relativamente pequeño. Cuantas más capas tenga una imagen, más debe trabajar
el controlador de gráficos para consolidar las capas en un sistema de archivos
raíz único para el contenedor correspondiente. Por supuesto, esto lleva tiempo,
y por lo tanto cuantas menos capas de una imagen tengamos, más breve será el
tiempo de inicio del contenedor.
Pero ¿cómo podemos asegurar que nuestro número de capas se mantiene bajo? Recuerda
que en un Dockerfile cada línea que comienza con una palabra clave, como
FROM, COPY o RUN, crea una nueva capa. La forma más sencilla de reducir el
número de capas es combinar varios comandos RUN individuales en uno solo.
Supongamos que tenemos un Dockerfile como el siguiente:
RUN apt-get update
RUN apt-get install -y ca-certificates
RUN rm -rf /var/lib/apt/lists/*

Podríamos combinarlos en una sola expresión concatenada, de la siguiente


manera:
RUN apt-get update \
&& apt-get install -y ca-certificates \
&& rm -rf /var/lib/apt/lists/*

El primero generará tres capas en la imagen resultante, mientras que el último


solo crea una sola capa.

Las siguientes tres prácticas recomendadas sirven para reducir el tamaño de las
imágenes. ¿Por qué esto es importante? Las imágenes más pequeñas reducen el tiempo y
el ancho de banda necesarios para descargar la imagen de un registro. También reducen
la cantidad de espacio en disco necesario para almacenar una copia local en el host de
Docker y la memoria necesaria para cargar la imagen. Finalmente, las imágenes más
pequeñas también significan una superficie de ataque más pequeña para los hackers.
Estas son las prácticas recomendadas para reducir el tamaño de la imagen:

[ 78 ]
Capítulo 4

• Utiliza un archivo .dockerignore. Queremos evitar copiar archivos


y carpetas innecesarios en una imagen para mantenerla lo más ligera posible.
Un archivo .dockerignore funciona exactamente de la misma manera que
un archivo .gitignore, para aquellos que estén familiarizados con Git.
En un archivo .dockerignore, podemos configurar patrones para excluir
determinados archivos o carpetas del contexto al crear la imagen.
• Evita instalar paquetes innecesarios en el sistema de archivos de la imagen. Una
vez más, esto sirve para mantener la imagen tan ligera como sea posible.
• Utiliza compilaciones multietapa para que la imagen resultante sea lo más
pequeña posible y solo contenga lo mínimo necesario para ejecutar la aplicación
o el servicio de la aplicación.

Guardar y cargar imágenes


La tercera forma de crear una nueva imagen de contenedor es importándola o cargándola
desde un archivo. Una imagen de contenedor no es más que un tarball. Para demostrarlo,
podemos utilizar el comando docker image save para exportar una imagen existente a
un tarball:
$ docker image save -o ./backup/my-alpine.tar my-alpine

El comando anterior utiliza nuestra imagen my-alpine que hemos creado previamente y
la exporta a un archivo ./backup/my-alpine.tar.

Si, por otro lado, disponemos de un tarball existente y queremos importarlo como
imagen a nuestro sistema, podemos utilizar el comando docker image load de la
siguiente manera:
$ docker image load -i ./backup/my-alpine.tar

Compartir o enviar imágenes


Para poder enviar nuestra imagen personalizada a otros entornos, primero necesitamos
asignarle un nombre único a escala global. Esta acción a menudo recibe el nombre de
"etiquetar una imagen". Entonces necesitamos publicar la imagen en una ubicación
central desde la que otras partes interesadas puedan extraerla. Estas ubicaciones centrales
se denominan registros de imágenes.

[ 79 ]
Creación y gestión de imágenes de contenedores

Etiquetado de una imagen


Cada imagen tiene una etiqueta. Una etiqueta se utiliza a menudo para las imágenes de
versión, pero tiene un alcance más amplio que simplemente un número de versión. Si no
especificamos explícitamente una etiqueta cuando trabajamos con imágenes, entonces
Docker presupone que nos estamos refiriendo a la etiqueta latest (más reciente). Esto es
importante cuando se extrae una imagen desde Hub Docker, por ejemplo:
$ docker image pull alpine

El comando anterior extraerá la imagen alpine:latest del Hub. Si queremos


especificar explícitamente una etiqueta, lo hacemos así:
$ docker image pull alpine:3.5

Esto extraerá la imagen alpine que se ha etiquetado con 3.5.

Espacios de nombres de imagen


Hasta ahora, has extraído varias imágenes y no has tenido que preocuparte mucho acerca
de dónde se han originado esas imágenes. Tu entorno de Docker se configura de forma
que, de manera predeterminada, las imágenes se extraen de Docker Hub. También hemos
extraído las llamadas imágenes oficiales de Docker Hub, como alpine o busyBox.

Ahora es el momento de ampliar nuestro horizonte un poco y aprender cómo se generan


los espacios de nombres de las imágenes. La forma más genérica de definir una imagen
es por su nombre completo, de la siguiente manera:
<registry URL>/<User or Org>/<name>:<tag>

Analicemos esto más detalladamente:

• <registry URL>: esta es la URL del registro desde la que queremos extraer
la imagen. De forma predeterminada, es docker.io. En general, podría ser
https://registry.acme.com.
Aparte de Docker Hub, hay un gran número de registros públicos de los que
se pueden extraer imágenes. Veamos una lista de algunos de ellos, sin ningún
orden concreto:

°° Google en https://cloud.google.com/container-registry
°° Amazon AWS en https://aws.amazon.com/ecr/
°° Microsoft Azure en https://azure.microsoft.com/en-us/
services/container-registry/

[ 80 ]
Capítulo 4

°° Red Hat en https://access.redhat.com/containers/


°° Artifactory en https://jfrog.com/integration/artifactory-
docker-registry/

• <User or Org>: este es el ID de Docker privado de una persona o de una


organización definido en Docker Hub o cualquier otro registro para ese fin,
como microsoft u oracle.
• <name>: este es el nombre de la imagen que a menudo se llama "repositorio".
• <tag>: esta es la etiqueta de la imagen.

Veamos un ejemplo:
https://registry.acme.com/engineering/web-app:1.0

Aquí tenemos una imagen, web-app, que está etiquetada con la versión 1.0 y pertenece
a la organización engineering en el registro privado de https://registry.acme.com.

Ahora, existen algunas convenciones especiales:

• Si omitimos la URL del registro, se utilizará automáticamente Docker Hub.


• Si omitimos la etiqueta, se utiliza latest (más reciente).
• Si se trata de una imagen oficial de Docker Hub, no es necesario que aparezca
ningún espacio de nombres de usuario u organización.

En la siguiente tabla se muestran algunos ejemplos:

Imagen Descripción
Imagen oficial alpine de Docker Hub con la etiqueta
alpine
latest.
Imagen oficial de ubuntu de Docker Hub con la etiqueta
ubuntu:16.04
o la versión 16.04.
Imagen nanoserver de Microsoft de Docker Hub con
microsoft/nanoserver
la etiqueta latest.
Imagen web-api con la versión 12.0 asociada
acme/web-api:12.0
a la organización acme. La imagen está en Docker Hub.
Imagen sample-app con la etiqueta 1.1 que
gcr.io/gnschenker/
pertenece a un individuo con el ID gnschenker
sample-app:1.1
en el registro de contenedores de Google.

[ 81 ]
Creación y gestión de imágenes de contenedores

Imágenes oficiales
En la tabla anterior, mencionamos la imagen oficial unas cuantas veces. Esto necesita una
explicación. Las imágenes se almacenan en los repositorios del registro de Docker Hub.
Los repositorios oficiales son un conjunto de repositorios alojados en Docker Hub y son
adaptados por individuos u organizaciones que también son responsables del software
que se empaqueta dentro de la imagen. Echemos un vistazo a un ejemplo de lo que eso
significa. Detrás de la distribución Linux Ubuntu hay una organización oficial. Este
equipo también ofrece versiones oficiales de las imágenes de Docker que contienen sus
distribuciones de Ubuntu.

Las imágenes oficiales están diseñadas para proporcionar repositorios básicos del SO,
imágenes para runtimes de lenguajes de programación populares, almacenamiento
de datos de uso frecuente y otros servicios importantes.

Docker apoya a un equipo cuya tarea es revisar y publicar todas aquellas imágenes
adaptadas en repositorios públicos en Docker Hub. Además, Docker analiza todas las
imágenes oficiales para detectar vulnerabilidades.

Enviar imágenes a un registro


La creación de imágenes personalizadas está muy bien, pero en algún momento
querremos compartir o enviar nuestras imágenes a un entorno de destino, como un
sistema de pruebas, control de calidad o producción. Para ello, normalmente usamos
un registro de contenedores. Uno de los registros públicos más populares es Docker Hub.
Está configurado como registro predeterminado en tu entorno de Docker y es el registro
desde el que hemos extraído todas nuestras imágenes hasta ahora.

En un registro, normalmente se pueden crear cuentas personales o de la organización.


Por ejemplo, mi cuenta personal en Docker Hub es gnschenker. Las cuentas personales
son adecuadas para el uso personal. Si queremos utilizar el registro de forma profesional,
probablemente queramos crear una cuenta de organización, como acme, en Docker Hub.
La ventaja de esto último es que las organizaciones pueden tener varios equipos. Los
equipos pueden tener permisos diferentes.

Para poder enviar una imagen a mi cuenta personal en Docker Hub, necesito etiquetarla
en consecuencia. Supongamos que quiero enviar la última versión de alpine a mi cuenta
y asignarle una etiqueta 1.0. Puedo hacerlo de la siguiente manera:
$ docker image tag alpine:latest gnschenker/alpine:1.0

Ahora, para poder enviar la imagen, tengo que iniciar sesión en mi cuenta:
$ docker login -u gnschenker -p <my secret password>

[ 82 ]
Capítulo 4

Después de iniciar sesión, puedo enviar la imagen:


$ docker image push gnschenker/alpine:1.0

Veré algo parecido a esto en el Terminal:


The push refers to repository [docker.io/gnschenker/alpine]
04a094fe844e: Mounted from library/alpine
1.0: digest: sha256:5cb04fce... size: 528

Para cada imagen que enviamos a Docker Hub, creamos automáticamente un repositorio.
Un repositorio puede ser privado o público. Todo el mundo puede extraer una imagen
de un repositorio público. De un repositorio privado, solo se puede extraer una imagen
si se ha iniciado sesión en el registro y se han configurado los permisos necesarios.

Resumen
En este capítulo, hemos explicado en detalle qué son las imágenes de contenedor
y cómo podemos crearlas y distribuirlas. Como hemos visto, hay tres maneras diferentes
de crear una imagen: manualmente, automáticamente o importando un tarball en el
sistema. También hemos aprendido algunas de las prácticas recomendadas comúnmente
utilizadas cuando creamos imágenes personalizadas.

En el capítulo siguiente, presentaremos los volúmenes de Docker que se pueden utilizar


para conservar el estado de un contenedor, y también introduciremos algunos comandos
del sistema útiles que se pueden utilizar para inspeccionar el host de Docker con más
detalle, trabajar con eventos generados por el daemon de Docker y limpiar los recursos
no utilizados.

Preguntas
Intenta responder a las siguientes preguntas para evaluar el progreso de tu aprendizaje:

1. ¿Cómo crearías un Dockerfile que se herede de la versión 17.04 de Ubuntu


y que instale y ejecute ping cuando se inicie un contenedor? La dirección
predeterminada para hacer ping será 127.0.0.1.
2. ¿Cómo crearías una nueva imagen de contenedor que utilice alpine:latest
e instale curl? Renombra la imagen nueva como my-alpine:1.0.
3. Crea un Dockerfile que utilice varios pasos para crear una imagen de una
aplicación Hello World de tamaño mínimo, escrita en C o Go.
4. Nombra tres características esenciales de una imagen de contenedor de Docker.

[ 83 ]
Creación y gestión de imágenes de contenedores

5. Quieres insertar una imagen denominada foo:1.0 en tu cuenta personal jdoe


en Docker Hub. ¿Cuál de las siguientes es la solución correcta?

1. $ docker container push foo:1.0


2. $ docker image tag foo:1.0 jdoe/foo:1.0
$ docker image push jdoe/foo:1.0

3. $ docker login -u jdoe -p <your password>


$ docker image tag foo:1.0 jdoe/foo:1.0
$ docker image push jdoe/foo:1.0

4. $ docker login -u jdoe -p <your password>


$ docker container tag foo:1.0 jdoe/foo:1.0
$ docker container push jdoe/foo:1.0

5. $ docker login -u jdoe -p <your password>


$ docker image push foo:1.0 jdoe/foo:1.0

Lectura adicional
La siguiente lista de referencias te ofrece un material que analiza con mayor detalle el
tema de la creación y construcción de imágenes de contenedor (pueden estar en inglés):

• Prácticas recomendadas para escribir Dockerfiles en http://dockr.


ly/22WiJiO
• Uso de compilaciones multietapa en http://dockr.ly/2ewcUY3
• Acerca de los controladores de almacenamiento en http://dockr.ly/1TuWndC
• Complementos para controladores gráficos en http://dockr.ly/2eIVCab
• Almacenamiento en caché guiado por el usuario en Docker para MAC en
http://dockr.ly/2xKafPf

[ 84 ]
Administración de volúmenes
y sistemas de datos
En el último capítulo, aprendimos a crear y compartir nuestras propias imágenes de
contenedor. Se puso especial énfasis en cómo crear imágenes que fueran lo más pequeñas
posibles incluyendo únicamente los artefactos que de verdad necesita la aplicación
en contenedor.

En este capítulo, vamos a aprender a trabajar con contenedores con estado, es decir,
contenedores que consumen y producen datos. También aprenderemos a mantener
nuestro entorno de Docker limpio y libre de recursos no utilizados. Por último, pero no
menos importante, vamos a examinar la secuencia de eventos que produce un motor
de Docker.

Esta es una lista de los temas que vamos a tratar:

• Creación y montaje de volúmenes de datos


• Compartir datos entre contenedores
• Uso de volúmenes de host
• Definición de volúmenes en imágenes
• Obtención de información exhaustiva sobre el sistema Docker
• Listado del consumo de recursos
• Eliminación de los recursos no utilizados
• Consumo de eventos del sistema Docker

[ 85 ]
Arquitectura de aplicaciones distribuidas

Una vez que leas este capítulo, podrás:

• Crear, eliminar y mostrar volúmenes de datos


• Montar un volumen de datos existente en un contenedor
• Crear datos duraderos desde dentro de un contenedor mediante un volumen
de datos
• Compartir datos entre varios contenedores mediante volúmenes de datos
• Montar cualquier carpeta de host en un contenedor utilizando volúmenes de datos
• Definir el modo de acceso (lectura/escritura o solo lectura) de un contenedor
cuando se accede a los datos de un volumen de datos
• Mostrar la cantidad de espacio consumido por los recursos de Docker en un host
determinado, como imágenes, contenedores y volúmenes
• Liberar el sistema de recursos de Docker no utilizados, como contenedores,
imágenes y volúmenes
• Mostrar los eventos del sistema Docker en una consola en tiempo real

Requisitos técnicos
Para este capítulo, es necesario tener Docker Toolbox instalado en el equipo o acceder
a una máquina virtual Linux que ejecute Docker en un portátil o en el cloud. En este
capítulo no se incluye ningún código.

Creación y montaje de volúmenes de datos


Todas las aplicaciones que tienen alguna utilidad consumen o producen datos. Sin
embargo, los contenedores se suelen diseñar de forma que no tienen estado. ¿Cómo
vamos a lidiar con esto? Una forma es utilizar volúmenes de Docker. Los volúmenes
permiten que los contenedores consuman, produzcan y modifiquen el estado. Los
volúmenes tienen un ciclo de vida que se extiende más allá del ciclo de vida de los
contenedores. Cuando un contenedor que utiliza un volumen deja de existir, el volumen
sigue existiendo. Esto resulta muy útil para la durabilidad del estado.

Modificación de la capa de contenedor


Antes de adentrarnos en los volúmenes, expliquemos primero qué ocurre si una
aplicación incluida en un contenedor cambia algo del sistema de archivos del contenedor.
En este caso, todos los cambios se producen en la capa de contenedor que permite
operaciones de escritura. Enseguida demostraremos esto ejecutando un contenedor y
ejecutando un script que crea un nuevo archivo:
$ docker container run --name demo \
alpine /bin/sh -c 'echo "This is a test" > sample.txt'

[ 86 ]
Capítulo 5

El comando anterior crea un contenedor denominado demo y, dentro de este contenedor, crea
un archivo llamado sample.text con el contenido This is a test. El contenedor termina
una vez que finaliza la operación, pero permanece en la memoria para que podamos hacer
nuestras investigaciones. Vamos a usar el comando diff para averiguar qué ha cambiado
en el sistema de archivos del contenedor con respecto al sistema de archivos de la imagen:
$ docker container diff demo

El resultado debería ser similar al siguiente:


A /sample.txt

Es evidente que se ha añadido un nuevo archivo, A, al sistema de archivos del


contenedor, tal como se esperaba. Como todas las capas que se derivan de la imagen
subyacente (alpine en este caso) son inmutables, el cambio solo podría producirse
en la capa de contenedor que permite operaciones de escritura.

Si ahora eliminamos el contenedor de la memoria, su capa de contenedor también se


eliminará y con ella se borrarán todos los cambios de manera irreversible. Si necesitamos
que nuestros cambios se conserven más allá de la vida útil del contenedor, esta no es
una solución. Afortunadamente, tenemos mejores opciones en forma de volúmenes
de Docker. Vamos a ver qué son estos volúmenes.

Creación de volúmenes
Dado que, a día de hoy, cuando se utiliza Docker para Mac o Windows, los contenedores
no se ejecutan de forma nativa en OS X o Windows, sino en una máquina virtual (oculta)
creada por Docker para Mac y Windows, es mejor utilizar docker-machine para crear
y utilizar una máquina virtual explícita que ejecute Docker. En este punto, vamos
a presuponer que tienes Docker Toolbox instalado en tu sistema. Si no es así, vuelve
al Capítulo 2, Configuración de un entorno de trabajo, en el que encontrarás instrucciones
detalladas sobre cómo instalar Toolbox.

Utiliza docker-machine para mostrar todas las máquinas virtuales que se ejecutan
actualmente en VirtualBox:
$ docker-machine ls

Si no aparece una máquina virtual denominada node-1, crea una:


$ docker-machine create --driver virtualbox node-1

Si tienes una máquina virtual llamada node-1 pero no se está ejecutando, iníciala:
$ docker-machine start node-1

Ahora que todo está listo, accede mediante SSH a esta máquina virtual llamada node-1:
$ docker-machine ssh node-1

Debería aparecer un mensaje de bienvenida de boot2docker.

[ 87 ]
Arquitectura de aplicaciones distribuidas

Para crear un nuevo volumen de datos, podemos utilizar el comando docker volume
create. Este comando creará un volumen con nombre que se puede montar en un
contenedor y utilizarse para el acceso o el almacenamiento de datos persistentes. El siguiente
comando crea un volumen, my-data, con el controlador de volumen predeterminado:
$ docker volume create my-data

El controlador de volumen predeterminado es el controlador local, que almacena los


datos localmente en el sistema de archivos del host. La forma más sencilla de saber dónde
se almacenan los datos en el host es ejecutar el comando inspect en el volumen que
acabamos de crear. La ubicación real puede diferir en función del sistema y, por tanto,
esta es la forma más segura de encontrar la carpeta de destino:
$ docker volume inspect my-data,
[
{
"CreatedAt": "2018-01-28T21:55:41Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/mnt/sda1/var/lib/docker/volumes/my-data/_
data",
"Name": "my-data",
"Options": {},
"Scope": "local"
}
]

La carpeta del host se encuentra en el resultado del comando bajo Mountpoint.


En nuestro caso, al utilizar docker-machine con una máquina virtual basada en
LinuxKit que se ejecuta en VirtualBox, la carpeta es /mnt/sda1/var/lib/Docker/
Volumes/My-Data/_data.

La carpeta de destino a menudo es una carpeta protegida y, por lo tanto, es posible que
tengamos que usar sudo para acceder a esta carpeta y ejecutar cualquier operación en
ella. En nuestro caso, no necesitamos usar sudo:
$ cd /mnt/sda1/var/lib/docker/volumes/my-data/_data

Si se utiliza Docker para Mac para crear un volumen en el portátil


y después ejecutar un comando docker volume inspect en el volumen
recién creado, el Mountpoint se muestra como /var/lib/Docker/
Volumes/My-Data/_data. Pero verás que no hay tal carpeta en el
Mac. La razón es que la ruta es relativa a la máquina virtual oculta que
Docker para Mac utiliza para ejecutar contenedores. En este momento, los
contenedores no pueden ejecutarse de forma nativa en OS X. Lo mismo
ocurre con los volúmenes creados con Docker para Windows.

[ 88 ]
Capítulo 5

Hay otros controladores de volumen disponibles de otros proveedores en forma


de complementos. Podemos utilizar el parámetro --driver en el comando create
para seleccionar un controlador de volumen diferente. Otros controladores de volumen
utilizan diferentes tipos de sistemas de almacenamiento para los volúmenes, como
almacenamiento en el cloud, unidades NFS, almacenamiento definido por software, etc.

Montaje de un volumen
Una vez que hemos creado un volumen con nombre, podemos montarlo
en un contenedor. Para ello, podemos usar el parámetro -v en el comando
docker container run:

$ docker container run --name test -it \


-v my-data:/data alpine /bin/sh

El comando anterior monta el volumen my-data en la carpeta /data dentro del


contenedor. Dentro del contenedor, ahora podemos crear archivos en la carpeta /data
y luego salir:
# / cd /data
# / echo "Some data" > data.txt
# / echo "Some more data" > data2.txt
# / exit

Si accedemos a la carpeta del host que contiene los datos de volumen y mostramos
su contenido, deberíamos ver los dos archivos que acabamos de crear dentro del
contenedor:
$ cd /mnt/sda1/var/lib/docker/volumes/my-data/_data
$ ls -l
total 8
-rw-r--r-- 1 root root 10 Jan 28 22:23 data.txt
-rw-r--r-- 1 root root 15 Jan 28 22:23 data2.txt

Podemos incluso intentar mostrar el contenido de, por ejemplo, el segundo archivo:
$ cat data2.txt

Vamos a intentar crear un archivo en esta carpeta desde el host y luego utilizar
el volumen con otro contenedor:
$ echo "This file we create on the host" > host-data.txt

[ 89 ]
Arquitectura de aplicaciones distribuidas

Ahora, vamos a eliminar el contenedor test y ejecutar otro basado en CentOS. Esta vez
vamos a montar nuestro volumen en una carpeta de contenedor diferente, /app/data:
$ docker container rm test
$ docker container run --name test2 -it \
-v my-data:/app/data \
Centos:7 /bin/bash

Una vez que estemos dentro del contenedor de CentOS, podemos acceder a la carpeta /
app/data en la que hemos montado el volumen y mostrar su contenido:

# / cd /app/data
# / ls -l

Como era de esperar, deberíamos ver estos tres archivos:


-rw-r--r-- 1 root root 10 Jan 28 22:23 data.txt
-rw-r--r-- 1 root root 15 Jan 28 22:23 data2.txt
-rw-r--r-- 1 root root 32 Jan 28 22:31 host-data.txt

Esta es la prueba definitiva de que los datos de un volumen de Docker persisten más allá
de la vida útil de un contenedor, y también de que los volúmenes pueden ser reutilizados
por otros contenedores, incluso contenedores diferentes del primero que usamos.

Es importante tener en cuenta que la carpeta incluida en el contenedor en el que


montamos un volumen de Docker se excluye del sistema de archivos de unión. Es decir,
cada cambio que se produzca dentro de esta carpeta y cualquiera de sus subcarpetas
no formará parte de la capa de contenedor, pero persistirá en el almacenamiento de
respaldo proporcionado por el controlador de volumen. Este hecho es muy importante,
ya que la capa de contenedor se elimina cuando el contenedor correspondiente se detiene
y se retira del sistema.

Eliminación de volúmenes
Los volúmenes se puede eliminar mediante el comando docker volume rm.
Es importante recordar que la eliminación de un volumen destruye los datos que este
contiene de forma irreversible y, por lo tanto, se considera un comando peligroso. Docker
nos ayuda un poco en este sentido, ya que no nos permite eliminar un volumen que
todavía lo está usando un contenedor. Asegúrate siempre, antes de quitar o eliminar
un volumen, de que dispones de una copia de seguridad de tus datos o de que de verdad
ya no necesitas estos datos.

El siguiente comando elimina el volumen my-data que creamos anteriormente:


$ docker volume rm my-data

[ 90 ]
Capítulo 5

Después de ejecutar el comando anterior, comprueba que la carpeta del host


se ha eliminado.

Para eliminar todos los contenedores en ejecución con el fin de limpiar el sistema,
ejecuta el siguiente comando:
$ docker container rm -f $(docker container ls -aq)

Compartir datos entre contenedores


Los contenedores son como entornos sandbox para las aplicaciones que se ejecutan
dentro de ellos. Esto sirve sobre todo para proteger las aplicaciones que se ejecutan
en diferentes contenedores. Esto también significa que todo el sistema de archivos
visible para una aplicación que se ejecuta dentro de un contenedor es privado para esa
aplicación, y ninguna otra aplicación que se ejecute en un contenedor diferente puede
interferir con él.
Sin embargo, algunas veces querremos compartir datos entre contenedores. Supongamos
que una aplicación que se ejecuta en el contenedor A produce algunos datos que los
consumirá otra aplicación que se ejecuta en el contenedor B. ¿Cómo podemos conseguir
esto? Bien, seguro que ya lo has adivinado: podemos usar los volúmenes de Docker
para este propósito. Podemos crear un volumen y montarlo en el contenedor A y en el
contenedor B. De esta forma, ambas aplicaciones, A y B, tienen acceso a los mismos datos.
Ahora, como ocurre siempre que varias aplicaciones o procesos acceden
simultáneamente a los datos, tenemos que ser muy cuidadosos para evitar incoherencias.
Para evitar problemas de simultaneidad, como condiciones de carrera, lo ideal sería tener
una sola aplicación o proceso que cree o modifique los datos y que los demás procesos
que acceden simultáneamente a estos datos solo los lean. Podemos hacer que un proceso
que se ejecuta en un contenedor solo pueda leer los datos de un volumen montando ese
volumen como de solo lectura. Veamos el siguiente comando:
$ docker container run -it --name writer \
-v shared-data:/data \
alpine /bin/sh

Aquí, creamos un contenedor llamado writer que tiene un volumen, shared-data,


montado en modo de lectura/escritura predeterminado. Vamos a intentar crear un
archivo dentro de este contenedor:
# / echo "I can create a file" > /data/sample.txt

Debería funcionar. Salimos de este contenedor y ejecutamos el siguiente comando:


$ docker container run -it --name reader \
-v shared-data:/app/data:ro \
ubuntu:17.04 /bin/bash

[ 91 ]
Arquitectura de aplicaciones distribuidas

Y tenemos un contenedor llamado reader que tiene el mismo volumen montado como
de solo lectura (sl). En primer lugar, debemos asegurarnos de que podamos ver el archivo
creado en el primer contenedor:
$ ls -l /app/data
total 4
-rw-r--r-- 1 root root 20 Jan 28 22:55 sample.txt

Y, a continuación, intentamos crear un archivo:


# / echo "Try to break read/only" > /app/data/data.txt

Esto producirá un error con el siguiente mensaje:


bash: /app/data/data.txt: Read-only file system

Salimos del contenedor escribiendo exit en el símbolo del sistema. De nuevo en el host,
vamos a limpiar todos los contenedores y volúmenes:
$ docker container rm -f $(docker container ls -aq)
$ docker volume rm $(docker volume ls -q)

Una vez hecho esto, salimos de la máquina virtual docker-machine escribiendo también
exit en el símbolo del sistema. Deberíamos volver a Docker para Mac o Windows.
Usamos docker-machine para detener la máquina virtual:
$ docker-machine stop node-1

Uso de volúmenes de host


En algunos escenarios, como cuando se desarrollan nuevas aplicaciones en contenedor
o cuando una aplicación incluida en contenedores necesita consumir datos de una
determinada carpeta producida, por ejemplo, por una aplicación heredada, es muy
útil utilizar volúmenes que monten una carpeta específica de host. Echemos un vistazo
al siguiente ejemplo:
$ docker container run --rm -it \
-v $(pwd)/src:/app/src \
alpine:latest /bin/sh

La expresión anterior inicia interactivamente un contenedor alpine con un shell y monta


la subcarpeta src del directorio actual en el contenedor en /app/src. Necesitamos usar
$(pwd) (o 'pwd'), que es el directorio actual, ya que cuando trabajamos con volúmenes
siempre tenemos que usar rutas absolutas.

[ 92 ]
Capítulo 5

Los desarrolladores utilizan estas técnicas siempre que trabajan en una aplicación que
se ejecuta en un contenedor y quieren asegurarse de que el contenedor siempre contenga
los últimos cambios que hacen en el código, sin tener que volver a compilar la imagen
y volver a ejecutar el contenedor después de cada cambio.

Veamos un ejemplo para demostrar cómo funciona. Supongamos que queremos crear
un sitio web estático simple usando Nginx como nuestro servidor web. En primer lugar,
creamos una nueva carpeta en el host donde pondremos nuestros activos web, como los
archivos HTML, CSS y JavaScript, y accedemos a ella:
$ mkdir ~/my-web
$ cd ~/my-web

A continuación, creamos una página web sencilla como esta:


$ echo "<h1>Personal Website</h1>" > index.html

Ahora, añadimos un Dockerfile que contendrá las instrucciones sobre cómo compilar
la imagen que contiene nuestro sitio web de ejemplo. Añadimos un archivo llamado
Dockerfile a la carpeta con este contenido:

FROM nginx:alpine
COPY . /usr/share/nginx/html

El Dockerfile comienza con la última versión de Alpine de Nginx y, a continuación, copia


todos los archivos del directorio del host actual en la carpeta de contenedores, /usr/
share/nginx/HTML. Aquí es donde Nginx espera que se encuentren los activos web.
Ahora vamos a compilar la imagen con el siguiente comando:
$ docker image build -t my-website:1.0 .

Y, por último, ejecutamos un contenedor desde esta imagen. Vamos a ejecutar


el contenedor en modo desconectado:
$ docker container run -d \
-p 8080:80 --name my-site\
my-website:1.0

Observa el parámetro -p 8080:80. Aún no hemos hablado de él, pero lo explicaremos


detalladamente en el Capítulo 7, Conexión en red con un solo host. De momento, sabemos
que se corresponde con el puerto de contenedor 80 en el que Nginx atiende las solitudes
que entran en el puerto 8080 del portátil con el que puedes acceder a la aplicación.
Ahora, abrimos una pestaña del navegador y vamos a http://localhost:8080/
index.html; deberíamos ver el sitio web, que actualmente contiene únicamente
un título, Personal Website.

[ 93 ]
Arquitectura de aplicaciones distribuidas

A continuación, puedes editar el archivo index.html en el editor que prefieras para que
tenga un aspecto similar al siguiente:

<h1>Personal Website</h1>
<p>This is some text</p>

Y lo guardamos. Luego actualizamos el navegador. Vale, eso no ha funcionado. El


navegador sigue mostrando la versión anterior de index.html que contiene únicamente
el título. Así que vamos a detener y eliminar el contenedor actual y, después, vamos a
compilar de nuevo la imagen y a ejecutar de nuevo el contenedor:
$ docker container rm -f my-site
$ docker image build -t my-website:1.0 .
$ docker container run -d \
-p 8080:80 --name my-site\
my-website:1.0

Esta vez, cuando actualicemos el navegador, deberíamos ver el nuevo contenido. Bien, ha
funcionado, pero este método es demasiado complicado. Imagina que tienes que hacer
esto cada vez que realices un pequeño cambio en tu sitio web. Eso no se sostiene.

Ahora es el momento de utilizar volúmenes montados en el host. Una vez más,


eliminamos el contenedor actual y lo volvemos a ejecutar con el montaje de volumen:
$ docker container rm -f my-site
$ docker container run -d \
-v $(pwd):/usr/share/nginx/html \
-p 8080:80 --name my-site\
my-website:1.0

Ahora, añadimos un poco más de contenido a index.html y lo guardamos. Luego


actualizamos el navegador. Deberíamos ver los cambios. Y esto es exactamente lo que
queríamos conseguir; es lo que solemos llamar una "experiencia de editar y continuar".
Puedes hacer tantos cambios en tus archivos web como quieras, y siempre verás
inmediatamente el resultado en el navegador sin tener que volver a compilar la imagen
ni reiniciar el contenedor que contiene tu sitio web.

Es importante tener en cuenta que las actualizaciones se propagan ahora de forma


bidireccional. Si realizas cambios en el host, estos cambios se propagarán al contenedor, y
viceversa. También es importante el hecho de que cuando se monta la carpeta actual en la
carpeta de destino del contenedor, /usr/share/nginx/html, el contenido que ya existe
se reemplaza por el contenido de la carpeta del host.

[ 94 ]
Capítulo 5

Definición de volúmenes en imágenes


Recordemos lo que aprendimos sobre los contenedores en el Capítulo 3, Trabajar con
contenedores: el sistema de archivos de cada contenedor, cuando se inicia, se compone de
capas inmutables de la imagen subyacente, además de una capa de contenedor de escritura
permitida específica del propio contenedor. Todos los cambios que los procesos que se
ejecutan dentro del contenedor realicen en el sistema de archivos se conservarán en esta
capa de contenedor. Una vez que el contenedor se detiene y se elimina del sistema, la capa
de contenedor correspondiente se elimina del sistema y se pierde irreversiblemente.

Algunas aplicaciones, como las bases de datos que se ejecutan en contenedores, deben
conservar su datos más allá de la vida útil del contenedor. En este caso, pueden utilizar
volúmenes. Para explicarlo con más detalle, veamos un ejemplo concreto. MongoDB es una
conocida base de datos de documentos de código abierto. Muchos desarrolladores utilizan
MongoDB como un servicio de almacenamiento para sus aplicaciones. Los mantenedores de
MongoDB han creado una imagen y la han publicado en Docker Hub, que puede utilizarse
para ejecutar una instancia de la base de datos en un contenedor. Esta base de datos
producirá los datos que deben conservarse durante mucho tiempo. Pero los mantenedores
de MongoDB no saben quién usa esta imagen y cómo se usa. Por lo tanto, no tienen ninguna
influencia sobre el comando docker container run con el que los usuarios de la base
de datos iniciarán este contenedor. ¿Cómo pueden definir los volúmenes?

Afortunadamente, hay una manera de definir volúmenes en el Dockerfile. La palabra


clave para hacer esto es VOLUME, y podemos añadir la ruta absoluta a una sola carpeta
o a una lista de rutas separadas por comas. Estas rutas representan carpetas del sistema
de archivos del contenedor. Veamos algunos ejemplos de estas definiciones de volumen:

VOLUME /app/data
VOLUME /app/data, /app/profiles, /app/config
VOLUME ["/app/data", "/app/profiles", "/app/config"]

La primera línea define un solo volumen que se va a montar en /app/data. La segunda


línea define tres volúmenes como una lista separada por comas, y la última define lo
mismo que la segunda línea, pero esta vez el valor está formateado como una matriz JSON.

Cuando se inicia un contenedor, Docker crea automáticamente un volumen y lo monta


en la carpeta de destino correspondiente del contenedor para cada ruta definida en el
Dockerfile. Puesto que Docker crea automáticamente cada volumen, tendrá un SHA-256
como identificador.

Cuando se ejecute el contenedor, las carpetas definidas como volúmenes en el


Dockerfile se excluirán del sistema de archivos de unión y, por tanto, todos los cambios
que se realicen en esas carpetas no cambiarán la capa del contenedor, sino que se
conservarán en el volumen correspondiente. Ahora, es responsabilidad de los ingenieros
de operaciones asegurarse de que se realiza correctamente una copia de seguridad del
almacenamiento de respaldo.

[ 95 ]
Arquitectura de aplicaciones distribuidas

Podemos usar el comando docker image inspect para obtener información sobre
los volúmenes definidos en el Dockerfile. Veamos lo que nos ofrece MongoDB. Primero,
obtenemos la imagen con el siguiente comando:
$ docker image pull mongo:3.7

A continuación, inspeccionamos esta imagen y usamos el parámetro --format para


extraer únicamente la parte esencial del gran volumen de datos:
$ docker image inspect \
--format='{{json .ContainerConfig.Volumes}}' \
mongo:3.7 | jq

Lo que devolverá el siguiente resultado:

{
"/data/configdb": {},
"/data/db": {}
}

Como se puede ver, el Dockerfile para MongoDB define dos volúmenes en /data/
configdb y /data/db.

Ahora, vamos a ejecutar una instancia de MongoDB de la siguiente manera:


$ docker run --name my-mongo -d mongo:3.7

Ahora podemos utilizar el comando docker container inspect para obtener


información sobre los volúmenes que hemos creado, entre otras cosas. Usamos este
comando solo para obtener la información del volumen:
$ docker inspect --format '{{json .Mounts}}' my-mongo | jq

La expresión debe mostrar algo similar a lo siguiente:

[
{
"Type": "volume",
"Name": "b9ea0158b5...",
"Source": "/var/lib/docker/volumes/b9ea0158b.../_data",
"Destination": "/data/configdb",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""

[ 96 ]
Capítulo 5

},
{
"Type": "volume",
"Name": "5becf84b1e...",
"Source": "/var/lib/docker/volumes/5becf84b1.../_data",
"Destination": "/data/db",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
]

Obsérvese que los valores de los campos Name y Source se han abreviado para facilitar
su lectura. El campo Source nos ofrece la ruta al directorio host donde se almacenarán
los datos producidos por MongoDB dentro del contenedor.

Obtención de información sobre


el sistema Docker
Siempre que necesitemos solucionar los problemas de nuestro sistema, es fundamental
usar los comandos que se presentan en esta sección. Nos proporcionan mucha
información sobre el motor del Docker instalado en el host y sobre el sistema operativo
del host. Primero vamos a explicar el comando docker version. Este comando
proporciona información abundante sobre el cliente y el servidor de Docker que usa la
configuración actual. Si introduces el comando en la CLI, deberías ver algo similar a esto:

Información de versión de Docker

[ 97 ]
Arquitectura de aplicaciones distribuidas

En mi caso, puedo ver que tanto en el cliente como en el servidor estoy utilizando la
versión 18.04.0-CE-RC2 del motor de Docker. También puedo ver que mi orquestador
es Swarm, entre otras cosas.

Ahora que sabemos cuál es el cliente y cuál es el servidor, observemos el siguiente diagrama:

CLI accediendo a diferentes hosts de Docker

Puedes ver que el cliente es la pequeña CLI a través de la cual enviamos comandos
de Docker a la API remota del host de Docker. El host de Docker es el runtime
de contenedor que aloja los contenedores, y puede ejecutarse en el mismo equipo que
la CLI o puede ejecutarse en un servidor remoto, on-premises o en el cloud. Podemos
usar la CLI para administrar diferentes servidores. Para ello, configuramos un montón
de variables de entorno como DOCKER_HOST, DOCKER_TLS_VERIFY y DOCKER_CERT_PATH.
Si estas variables de entorno no están establecidas en tu máquina de trabajo y utilizas
Docker para Mac o Windows, eso significa que estás utilizando el motor de Docker que
se ejecuta en tu máquina.

El siguiente comando importante es el comando docker system info. Este comando


proporciona información sobre el modo en que funciona el motor de Docker (en modo
Swarm o no), el controlador de almacenamiento que se utiliza para el sistema de archivos
de unión, la versión del kernel de Linux que tenemos en nuestro host y otra información.
Analicemos con detalle el resultado generado por el sistema cuando se ejecuta
el comando. Veamos qué tipo de información se muestra:

[ 98 ]
Capítulo 5

Resultado del comando docker system info

[ 99 ]
Arquitectura de aplicaciones distribuidas

Listado del consumo de recursos


Con el tiempo, un host de Docker puede acumular bastantes recursos como imágenes,
contenedores y volúmenes en memoria y en disco. Como en todo buen hogar que se
precie, debemos mantener nuestro entorno limpio y libre de recursos no utilizados para
recuperar espacio. De lo contrario, llegará un momento en que Docker no nos permita
añadir más recursos nuevos, lo que significa que las acciones como obtener una imagen
pueden fallar por falta de espacio disponible en disco o en memoria.

La CLI de Docker proporciona un práctico comando system que muestra cuántos


recursos estamos utilizando en nuestro sistema y qué cantidad de este espacio se puede
recuperar. El comando es:
$ docker system df

Si ejecutas este comando en tu sistema, deberías ver un resultado similar al siguiente:


TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 21 9 1.103GB 845.3MB (76%)
Containers 14 11 9.144kB 4.4kB (48%)
Local Volumes 14 14 340.3MB 0B (0%)
Build Cache 0B 0B

La última línea del resultado, Build Cache, solo se muestra en las versiones más
recientes de Docker. Esta información se ha añadido recientemente. El resultado se
explica de la siguiente manera:

• En mi caso, el resultado me dice que en mi sistema tengo actualmente 21


imágenes almacenadas localmente en caché, de las cuales 9 se utilizan
activamente. Se considera que una imagen se usa activamente si al menos un
contenedor en ejecución o detenido se basa en ella. Estas imágenes ocupan 1,1
GB de espacio en disco. Se pueden recuperar técnicamente alrededor de 845 MB,
ya que las imágenes correspondientes no se están utilizando en este momento.
• Además, tengo 11 contenedores en ejecución en mi sistema y tres detenidos,
lo que hace un total de 14 contenedores. Puedo reclamar el espacio ocupado
por los contenedores detenidos, que es 4,4 kB en mi caso.
• También tengo 14 volúmenes activos en mi host que juntos consumen alrededor
de 340 MB de espacio en disco. Como todos los volúmenes están en uso,
no puedo reclamar ningún espacio en este caso.
• Por último, mi caché de compilación está vacía actualmente y,
evidentemente, tampoco puedo recuperar nada de espacio aquí.

[ 100 ]
Capítulo 5

Si quisiera obtener información aún más detallada sobre el consumo de recursos en mi


sistema, podría ejecutar el mismo comando en modo detallado con la marca -v:
$ docker system df -v

Esto me dará una lista detallada de todas las imágenes, contenedores y volúmenes con
su respectivo tamaño. Un posible resultado podría ser el siguiente:

Resultado detallado de los recursos del sistema consumidos por Docker

Este resultado detallado nos debería ofrecer información detallada suficiente para tomar
una decisión fundamentada sobre si necesitamos o no comenzar a limpiar nuestro
sistema y qué partes podríamos necesitar limpiar.

Eliminación de los recursos no utilizados


Una vez que hayamos decidido que debemos realizar un poco de limpieza, Docker nos
proporciona lo que llamamos comandos "prune". Para cada recurso, como imágenes,
contenedores, volúmenes y redes, existe un comando prune.

Limpieza de contenedores
En esta sección, queremos recuperar los recursos del sistema no utilizados mediante
la limpieza de contenedores. Comencemos con este comando:
$ docker container prune

[ 101 ]
Arquitectura de aplicaciones distribuidas

El comando anterior eliminará todos los contenedores del sistema que no tengan el
estado running (en ejecución). Docker nos pedirá confirmación antes de eliminar los
contenedores que tienen actualmente el estado exited (terminado) o created (creado).
Si deseas omitir este paso de confirmación, puedes utilizar la marca -f (o --force):
$ docker container prune -f

En algunas circunstancias, tal vez queramos eliminar todos los contenedores de nuestro
sistema, incluso los que se están ejecutando. No podemos usar el comando prune para
esto. En su lugar, debemos usar un comando, como la siguiente expresión combinada:
$ docker container rm -f $(docker container ls -aq)

Ten cuidado con el comando anterior. Elimina todos los contenedores sin avisarnos,
incluso los que están en ejecución. Antes de explicar detalladamente el comando anterior,
vamos a explicar qué ocurre exactamente y por qué.

Limpieza de imágenes
Lo siguiente que hay en la línea son imágenes. Si queremos liberar todo el espacio
ocupado por las capas de imagen sin utilizar, podemos usar el siguiente comando:
$ docker image prune

Después de volver a confirmar a Docker que queremos liberar el espacio ocupado por
las capas de imagen no utilizadas, estas se eliminan. Ahora tengo que explicar a qué nos
referimos con "capas de imagen sin utilizar". Como recordarás del capítulo anterior, una
imagen se compone de una pila de capas inmutables. Cuando compilamos una imagen
personalizada varias veces, cada vez realizando algunos cambios, por ejemplo, en el
código fuente de la aplicación para la que vamos a compilar la imagen, volvemos a crear
las capas y las versiones anteriores de la misma capa se quedan huérfanas. ¿Por qué
es así? La razón es que las capas son inmutables, como se explicó detalladamente en el
capítulo anterior. Por tanto, cuando se produce algún cambio en el código fuente usado
para compilar una capa, la propia capa debe volver a compilarse y la versión anterior
se abandona.

En un sistema donde compilamos a menudo imágenes, el número de capas de imagen


huérfanas puede aumentar considerablemente con el tiempo. Todas estas capas
huérfanas se eliminan con el comando prune anterior.

Al igual que con el comando prune para los contenedores, podemos evitar que Docker
nos pida confirmación usando la marca force:
$ docker image prune -f

[ 102 ]
Capítulo 5

Hay una versión aún más radical del comando prune de la imagen. A veces
no solo queremos eliminar las capas de imagen huérfanas, sino todas las imágenes
que no se están usando actualmente en nuestro sistema. Para ello, podemos usar
la marca -a (o --all):
$ docker image prune --force --all

Tras la ejecución del comando anterior, solo quedarán en nuestra caché de imágenes
locales las imágenes utilizadas actualmente por uno o varios contenedores.

Limpieza de volúmenes
Los volúmenes de Docker se utilizan para permitir a los contenedores el acceso
persistente de los datos. Estos datos pueden ser importantes y, por lo tanto, los comandos
que se explican en esta sección deben aplicarse con sumo cuidado.

Si sabes que deseas recuperar el espacio ocupado por los volúmenes y con él destruir
irreversiblemente los datos subyacentes, puedes utilizar el siguiente comando:
$ docker volume prune

Este comando quitará todos los volúmenes que no estén siendo usados actualmente por
al menos un contenedor.

Este es un comando destructivo y es irreversible. Siempre debes


crear una copia de seguridad de los datos asociados con los
volúmenes antes de eliminarlos, excepto cuando estés seguro de
que los datos no tienen ningún valor adicional.

Para evitar que el sistema se corrompa o que las aplicaciones funcionen incorrectamente,
Docker no permite quitar los volúmenes que están siendo usados actualmente por al
menos un contenedor. Esto es así incluso cuando el contenedor que usa el volumen está
detenido. Siempre debes eliminar primero los contenedores que utilizan un volumen.

Una marca útil para la limpieza de volúmenes es la marca -f o --filter, que nos
permite especificar el conjunto de volúmenes que se van a limpiar. Veamos el siguiente
comando:
$ docker volume prune --filter 'label=demo'

Esto solo aplicará el comando a los volúmenes que tengan una etiqueta con el valor
demo. El formato de la marca de filtro es clave=valor. Si necesitamos más de un filtro,
podemos usar varias marcas:
$ docker volume prune --filter 'label=demo' --filter 'label=test'

[ 103 ]
Arquitectura de aplicaciones distribuidas

La marca de filtro también se puede utilizar al limpiar otros recursos como contenedores
e imágenes.

Limpieza de redes
El último recurso que se puede limpiar son las redes. Hablaremos de las redes
detalladamente en el Capítulo 7, Conexión en red con un solo host. Para eliminar todas las
redes no utilizadas, usamos el siguiente comando:
$ docker network prune

Este comando eliminará las redes que no tienen actualmente ningún contenedor o servicio
conectado. No prestes demasiada atención a las redes en este momento. Nos ocuparemos
de ellas más adelante y entenderás claramente lo que acabamos de explicar.

Limpieza de todos los recursos


Si queremos limpiar todos los recursos a la vez sin tener que introducir varios comandos,
podemos usar el siguiente comando:
$ docker system prune

La CLI de Docker nos pedirá confirmación y luego eliminará todos los contenedores,
imágenes, volúmenes y redes no utilizados de una sola vez y en el orden correcto.

Una vez más, para evitar que Docker nos pida confirmación, podemos usar la marca
force con el comando.

Consumo de eventos del sistema Docker


El motor de Docker, cuando se crean, ejecutan, detienen y eliminan contenedores y otros
recursos, como volúmenes o redes, produce un registro de eventos. Estos eventos los
pueden consumir sistemas externos, como algunos servicios de infraestructura que
los utilizan para tomar decisiones fundamentadas. Un ejemplo de este tipo de servicio
podría ser una herramienta que creara un inventario de todos los contenedores que se
están ejecutando actualmente en el sistema.

Podemos interactuar con esta secuencia de eventos del sistema y mostrarlos, por ejemplo,
en un terminal con el siguiente comando:
$ docker system events

Este comando es un comando de bloqueo. Por lo tanto, cuando se ejecuta en la sesión del
terminal, la sesión correspondiente se bloquea. Por consiguiente, te recomendamos que
abras siempre una ventana adicional cuando desees utilizar este comando.

[ 104 ]
Capítulo 5

Supongamos que hemos ejecutado el comando anterior en una ventana del terminal
adicional. Ahora podemos probarlo y ejecutar un contenedor como este:
$ docker container run --rm alpine echo "Hello World"

El resultado generado sería similar al siguiente:


2018-01-28T15:08:57.318341118-06:00 container create
8e074342ef3b20cfa73d17e4ef7796d424aa8801661765ab5024acf166c6ecf3
(image=alpine,
name=confident_hopper)

2018-01-28T15:08:57.320934314-06:00 container attach


8e074342ef3b20cfa73d17e4ef7796d424aa8801661765ab5024acf166c6ecf3
(image=alpine, name=confident_hopper)

2018-01-28T15:08:57.354869473-06:00 network connect


c8fd270e1a776c5851c9fa1e79927141a1e1be228880c0aace4d0daebccd190f
(container=8e074342ef3b20cfa73d17e4ef7796d424aa8801661765ab5024acf
166c6ecf3, name=bridge, type=bridge)

2018-01-28T15:08:57.818494970-06:00 container start


8e074342ef3b20cfa73d17e4ef7796d424aa8801661765ab5024acf166c6ecf3
(image=alpine, name=confident_hopper)

2018-01-28T15:08:57.998941548-06:00 container die


8e074342ef3b20cfa73d17e4ef7796d424aa8801661765ab5024acf166c6ecf3
(exitCode=0, image=alpine, name=confident_hopper)
2018-01-28T15:08:58.304784993-06:00 network disconnect
c8fd270e1a776c5851c9fa1e79927141a1e1be228880c0aace4d0daebccd190f
(container=8e074342ef3b20cfa73d17e4ef7796d424aa8801661765ab5024acf
166c6ecf3, name=bridge, type=bridge)

2018-01-28T15:08:58.412513530-06:00 container destroy


8e074342ef3b20cfa73d17e4ef7796d424aa8801661765ab5024acf166c6ecf3
(image=alpine, name=confident_hopper)

En este resultado, podemos seguir el ciclo de vida exacto del contenedor. El contenedor
se crea, se inicia y luego se destruye. Si el resultado generado por este comando no es
de tu agrado, siempre puedes cambiarlo usando el parámetro --format. El valor del
formato debe escribirse utilizando la sintaxis de plantillas Go. En el ejemplo siguiente
se muestra el tipo, la imagen y la acción del evento:
$ docker system events --format 'Type={{.Type}} Image={{.Actor.
Attributes.image}} Action={{.Action}}'

[ 105 ]
Arquitectura de aplicaciones distribuidas

Si ejecutamos exactamente el mismo comando run que antes, el resultado generado


ahora sería el siguiente:
Type=container Image=alpine Action=create
Type=container Image=alpine Action=attach
Type=network Image=<no value> Action=connect
Type=container Image=alpine Action=start
Type=container Image=alpine Action=die
Type=network Image=<no value> Action=disconnect
Type=container Image=alpine Action=destroy

Resumen
En este capítulo, hemos explicado los volúmenes de Docker, que se pueden utilizar para
mantener los estados producidos por los contenedores y hacer que sean duraderos.
También podemos utilizar los volúmenes para proporcionar a los contenedores datos
procedentes de varios orígenes. Hemos aprendido a crear, montar y utilizar volúmenes.
Hemos aprendido varias técnicas de definición de volúmenes, por ejemplo, por nombre,
montando un directorio de host o definiendo volúmenes en una imagen de contenedor.

En este capítulo, también hemos explicado varios comandos de nivel de sistema que nos
proporcionan información abundante para solucionar los problemas de un sistema o para
administrar y limpiar los recursos utilizados por Docker. Por último, hemos aprendido
a visualizar y potencialmente consumir la secuencia de eventos generada por el runtime
de contenedor.

En el capítulo siguiente, veremos una introducción a los fundamentos de la orquestación


de contenedores. Allí explicaremos lo que necesitaremos cuando tengamos que
administrar y ejecutar no solo uno o unos pocos contenedores, sino cientos de ellos
en muchos nodos de un clúster. Veremos que hay muchos problemas que resolver.
Aquí es donde los motores de orquestación entran en juego.

Preguntas
Intenta responder a las siguientes preguntas para evaluar el progreso de tu aprendizaje:

1. ¿Cómo crearías un volumen de datos con nombre, por ejemplo mis-productos,


utilizando el controlador predeterminado?
2. ¿Cómo ejecutarías un contenedor utilizando la imagen alpine y montarías el
volumen mis-productos en modo de solo lectura en la carpeta del contenedor
/data?

[ 106 ]
Capítulo 5

3. ¿Cómo encontrarías la carpeta que está asociada con el volumen mis-productos


y accederías a ella? Además, ¿cómo crearías un archivo sample.txt con un
poco de contenido?
4. ¿Cómo ejecutarías otro contenedor alpine para montar en él el volumen mis-
productos en la carpeta /app-data, en modo de lectura/escritura? Dentro de
este contenedor, accede a la carpeta /app-data y crea un archivo hello.txt
con algún contenido.
5. ¿Cómo montarías un volumen de host, por ejemplo ~/mi-proyecto, en un
contenedor?
6. ¿Cómo eliminarías todos los volúmenes no utilizados de tu sistema?
7. ¿Cómo determinarías la versión exacta del kernel de Linux y de Docker que
se ejecuta en tu sistema?

Lectura adicional
Los artículos siguientes proporcionan información más detallada (pueden estar en inglés):

• Volúmenes en http://dockr.ly/2EUjTml
• Administrar datos en Docker en http://dockr.ly/2EhBpzD
• Volúmenes de Docker en PWD en http://bit.ly/2sjIfDj
• Contenedores: limpia tu casa en http://bit.ly/2bVrCBn
• Eventos del sistema Docker en http://dockr.ly/2BlZmXY

[ 107 ]
Arquitectura de aplicaciones
distribuidas
En el capítulo anterior, aprendimos a utilizar los volúmenes de Docker para mantener
un estado creado o modificado, así como compartir datos entre las aplicaciones que
se ejecutan en contenedores. También aprendimos a trabajar con eventos generados
por el daemon de Docker y limpiar los recursos no utilizados.

En este capítulo, introducimos el concepto de "arquitectura de aplicaciones distribuidas"


y explicamos los diferentes patrones y las prácticas recomendadas necesarias para
ejecutar una aplicación distribuida con éxito. Por último, explicaremos los requisitos
adicionales que deben cumplirse para ejecutar dicha aplicación en producción.

En este capítulo, abordaremos los siguientes temas:

• ¿Qué es una arquitectura de aplicaciones distribuidas?


• Patrones y prácticas recomendadas
• Ejecución en producción

Cuando termines de leer este capítulo, serás capaz de hacer lo siguiente:

• Nombrar al menos cuatro características de una arquitectura de aplicaciones


distribuidas
• Nombrar al menos cuatro patrones que deben implementarse para una
aplicación distribuida lista para producción

[ 109 ]
Arquitectura de aplicaciones distribuidas

¿Qué es una arquitectura de aplicaciones


distribuidas?
En esta sección, vamos a explicar en detalle lo que queremos decir cuando hablamos
de una arquitectura de aplicaciones distribuidas. En primer lugar, debemos asegurarnos
de que todas las palabras o acrónimos que usamos tengan un significado y que hablamos
el mismo idioma.

Definición de la terminología
En este y en los capítulos siguientes, hablaremos sobre conceptos que podrían no ser
conocidos por todos. Para asegurarnos de que todos hablemos el mismo idioma, vamos
a presentar brevemente y describir los más importantes de estos conceptos o palabras:

MV Acrónimo de máquina virtual. Es un ordenador virtual.


Servidor individual utilizado para ejecutar aplicaciones. Puede ser
un servidor físico, a menudo llamado "bare metal", o una máquina virtual.
Un nodo puede ser un sistema central, un superordenador, un servidor
Nodo
empresarial estándar o incluso una Raspberry Pi. Los nodos pueden ser
equipos del propio centro de datos de una empresa o que estén en el cloud.
Normalmente, un nodo forma parte de un clúster.
Grupo de nodos conectados por una red utilizada para ejecutar aplicaciones
Clúster
distribuidas.
Rutas de comunicación físicas y definidas por software entre los nodos
Red
individuales de un clúster y los programas que se ejecutan en esos nodos.
Canal del que una aplicación como un servidor web atiende las solicitudes
Puerto
entrantes.
Lamentablemente, es un término muy genérico y su significado real depende
del contexto en el que se utilice. Si usamos el término "servicio" en el contexto
de una aplicación como un servicio de aplicación, suele significar que se
Servicio trata de una parte de software que implementa un conjunto limitado de
una funcionalidad que luego es utilizado por otras partes de la aplicación.
Conforme avancemos por este libro, se explicarán otros tipos de servicios
que tienen una definición ligeramente diferente.

[ 110 ]
Capítulo 6

De manera inocente, podemos decir que una arquitectura de aplicaciones distribuidas


es lo contrario de una arquitectura de aplicaciones monolíticas, pero no es descabellado
centrarnos primero en esta arquitectura monolítica. Tradicionalmente, la mayoría de las
aplicaciones empresariales se han escrito de tal manera que el resultado se puede ver como
un programa único y estrechamente acoplado que se ejecuta en un servidor con nombre en
algún lugar de un centro de datos. Todo su código se compila en un solo archivo binario
o unos pocos archivos binarios estrechamente acoplados que deben estar en la misma
ubicación cuando se ejecuta la aplicación. El hecho de que el servidor o el host más general
en el que se ejecuta la aplicación tenga un nombre bien definido o una dirección IP estática
también es importante en este contexto. Echemos un vistazo al siguiente diagrama para
ilustrar de forma más clara este tipo de arquitectura de aplicaciones:

Arquitectura de aplicaciones monolíticas

En el diagrama anterior, vemos un servidor llamado Blue-Box-12A con una dirección


IP 172.52.13.44 que ejecuta una aplicación llamada Pet-Shop, que es un monolito
formado por un módulo principal y algunas bibliotecas estrechamente acopladas.

[ 111 ]
Arquitectura de aplicaciones distribuidas

Echemos un vistazo al siguiente diagrama:

Arquitectura de aplicaciones distribuidas

Aquí, de repente, ya no tenemos un solo servidor con nombre, sino que tenemos muchos
servidores que no tienen nombres muy afortunados, sino algunos ID únicos que pueden
ser algo así como un identificador único universal (UUID). La aplicación pet-shop,
de repente, tampoco consiste en un solo bloque monolítico, sino más bien en una plétora
de servicios que interactúan sin que estén estrechamente acoplados como pet-api,
pet-web y pet-inventory. Además, cada servicio se ejecuta en varias instancias en este
clúster de servidores o hosts.

Puede que te preguntes por qué estamos explicando esto en un libro sobre contenedores
Docker y haces bien en preguntártelo. Aunque todos los temas que vamos a investigar
se aplican igualmente a un mundo donde los contenedores no existían (aún),
es importante entender que los contenedores y los motores de orquestación
de contenedores ayudan a solucionar todos los problemas de una manera mucho
más eficiente y directa. La mayoría de los problemas que solían ser muy difíciles
de resolver en una arquitectura de aplicaciones distribuidas se vuelven bastante
simples en un mundo con contenedores.

[ 112 ]
Capítulo 6

Patrones y prácticas recomendadas


Una arquitectura de aplicaciones distribuidas tiene muchos beneficios convincentes,
pero también tiene un inconveniente muy importante en comparación con una
arquitectura de aplicaciones monolíticas: la primera es mucho más compleja. Para
gestionar esta complejidad, la industria ha aportado algunas prácticas recomendadas
y pautas importantes. En las secciones siguientes, vamos a analizar algunas de las más
importantes con más detalle.

Componentes ligeramente acoplados


La mejor manera de abordar un tema complejo siempre ha sido dividirlo en problemas
más pequeños que sean más manejables. Por ejemplo, sería increíblemente complejo
construir una casa en un solo paso. Es mucho más fácil construir la casa a partir de piezas
simples que luego se combinan en el resultado final.

Lo mismo se aplica al desarrollo de software. Es mucho más fácil desarrollar una


aplicación muy compleja si dividimos esta aplicación en componentes más pequeños
que interoperan y juntos componen la aplicación global. Ahora, es mucho más
fácil desarrollar estos componentes individualmente si solo se acoplan libremente
entre sí. Lo que esto significa es que el componente A no hace suposiciones sobre
el funcionamiento interno de, por ejemplo, los componentes B y C, sino que solo está
interesado en cómo puede comunicarse con esos dos componentes a través de una
interfaz bien definida. Si cada componente tiene una interfaz pública bien definida
y simple a través de la cual es posible la comunicación con los otros componentes
del sistema y del mundo exterior, esto nos permite desarrollar cada componente
individualmente, sin dependencias implícitas de otros componentes. Durante
el proceso de desarrollo, otros componentes del sistema pueden ser sustituidos
por "stubs" o "mocks" que nos permitan probar nuestro componente.

Sin estado o con estado


Todas las aplicaciones empresariales importantes crean, modifican o utilizan datos.
Los datos también se denominan "estado". Un servicio de aplicación que crea o modifica
datos persistentes se denomina "componente con estado". Los componentes de estado
típicos son los servicios de base de datos o los servicios que crean archivos. Por otra
parte, los componentes de aplicación que no crean ni modifican datos persistentes
se denominan "componentes sin estado".

En una arquitectura de aplicaciones distribuidas, los componentes sin estado son mucho
más simples de manejar que los componentes con estado. Los componentes sin estado
pueden ampliarse y reducirse fácilmente. También pueden ser destruidos rápidamente
y sin complicaciones, y reiniciarse en un nodo completamente diferente del clúster,
dado que no tienen datos persistentes asociados.

[ 113 ]
Arquitectura de aplicaciones distribuidas

Por este motivo, es útil diseñar un sistema de manera que la mayoría de los servicios
de aplicación sean sin estado. Es mejor mover todos los componentes con estado al límite
de la aplicación y limitar su número. Administrar componentes con estado es difícil.

Detección de servicios
Cuando creamos aplicaciones que consisten en muchos componentes individuales
o servicios que se comunican entre sí, necesitamos un mecanismo que permita que los
componentes individuales se encuentren entre sí en el clúster. El hecho de encontrarse
significa, normalmente, que es necesario saber en qué nodo se está ejecutando el
componente de destino y de qué puerto está obteniendo la comunicación. Con frecuencia,
los nodos se identifican mediante una dirección IP y un puerto, que es un número
de un intervalo bien definido.

Técnicamente, podríamos decirle al servicio A, que quiere comunicarse con un destino,


el servicio B, cuál es la dirección IP y el puerto del destino. Esto podría suceder,
por ejemplo, a través de una entrada de un archivo de configuración:

Los componentes están cableados

[ 114 ]
Capítulo 6

Aunque esto podría funcionar muy bien en el contexto de una aplicación monolítica
que se ejecute en uno o solo unos pocos servidores bien conocidos y seleccionados,
es totalmente inviable en una arquitectura de aplicaciones distribuidas. En primer
lugar, en este escenario, tenemos muchos componentes, y hacer un seguimiento manual
se convierte en una pesadilla. Definitivamente no es escalable. Además, el servicio
A normalmente no debería saber o nunca sabrá en qué nodo del clúster se ejecutan
los demás componentes. Es posible que su ubicación ni siquiera sea estable, ya que
el componente B se puede mover desde el nodo X a otro nodo Y, por varios motivos
externos a la aplicación. Por lo tanto, necesitamos otra manera de que el servicio A pueda
localizar el servicio B, o cualquier otro servicio para que esto ocurra. Lo más utilizado
es una autoridad externa que conozca la topología del sistema en un momento dado.
Esta autoridad o servicio externo conoce todos los nodos y sus direcciones IP que
pertenecen actualmente al clúster; conoce todos los servicios que se están ejecutando
y dónde se están ejecutando. A menudo, este tipo de servicio se denomina servicio DNS,
donde DNS significa Domain Name System o Sistema de nombres de dominio. Como
veremos, Docker tiene un servicio DNS implementado como parte del motor subyacente.
Kubernetes también utiliza un servicio DNS para facilitar la comunicación entre los
componentes que se ejecutan en el clúster:

Los componentes consultan un servicio de localización externa

En el diagrama anterior, vemos cómo el servicio A quiere comunicarse con el servicio B.


Pero no puede hacerlo directamente; tiene que consultar primero a la autoridad externa,
un servicio de registro, aquí llamado servicio DNS, la ubicación del servicio B. El servicio
de registro responderá con la información solicitada, y enviará la dirección IP y el
número de puerto con el que el servicio A puede contactar con el servicio B. El servicio
A utiliza esta información y establece comunicación con el servicio B. Por supuesto, esta
es una imagen simplificada de lo que realmente está sucediendo a bajo nivel, pero es una
buena imagen para entender el patrón arquitectónico de entrega de servicios.

[ 115 ]
Arquitectura de aplicaciones distribuidas

Enrutamiento
El enrutamiento es el mecanismo de envío de paquetes de datos desde un componente
de origen a un componente de destino. El enrutamiento se clasifica en diferentes tipos.
Uno utiliza el llamado modelo OSI (véase la referencia en la sección Lectura adicional
de este capítulo) para distinguir entre diferentes tipos de enrutamiento. En el contexto
de los contenedores y la orquestación de contenedores, el enrutamiento en las capas
2, 3, 4 y 7 es relevante. Explicaremos con más detalle el enrutamiento en los siguientes
capítulos. Aquí, digamos que el enrutamiento de capa 2 es el tipo de enrutamiento
de menor nivel, que conecta una dirección MAC a una dirección MAC, mientras que
el enrutamiento de capa 7, que también se llama enrutamiento de nivel de aplicación,
es el nivel más alto. Este último se utiliza, por ejemplo, para enrutar las solicitudes que
tienen un identificador de destino que es una dirección URL como example.com/pets
al componente de destino adecuado en nuestro sistema.

Equilibrio de carga
El equilibrio de carga se utiliza siempre que el servicio A solicita un servicio del
servicio B, pero este último se ejecuta en más de una instancia, como se muestra en
el siguiente diagrama:

Solicitud del servicio A con equilibrio de carga al servicio B

Si tenemos varias instancias de un servicio como el servicio B que se ejecuta en nuestro


sistema, queremos asegurarnos de que cada una de esas instancias obtenga una cantidad
igual de carga de trabajo. Esta tarea es genérica, lo que significa que no queremos que el autor
de la llamada tenga que hacer el equilibrio de carga, sino más bien un servicio externo que
intercepte la llamada y se encargue de decidir a cuál de las instancias del servicio de destino
desea reenviar la llamada. Este servicio externo se llama equilibrador de carga. Los
equilibradores de carga pueden utilizar diferentes algoritmos para decidir cómo distribuir
las llamadas entrantes a las instancias del servicio de destino. El algoritmo más usado se
llama "Round Robin". Este algoritmo solo asigna solicitudes de forma repetitiva, comenzando
con la instancia 1 y 2 hasta la instancia n. Después de que se haya servido la última instancia,
el equilibrador de carga comenzará con el número de instancia 1.

[ 116 ]
Capítulo 6

Programación defensiva
Al desarrollar un servicio para una aplicación distribuida, es importante recordar que
este servicio no va a ser independiente, sino que depende de otros servicios de aplicación
o incluso de servicios externos proporcionados por terceros, como servicios de validación
de tarjetas de crédito o servicios de información bursátil, por nombrar solo dos. Todos
estos otros servicios son externos al servicio que estamos desarrollando. No tenemos
control sobre si son correctos o están disponibles en un momento dado. Por lo tanto,
cuando programamos, siempre tenemos que suponer lo peor y esperar lo mejor. Suponer
lo peor significa que tenemos que lidiar con errores potenciales de manera explícita.

Reintentos
Cuando existe la posibilidad de que un servicio externo pueda no estar disponible
temporalmente o no sea lo suficientemente receptivo, se puede utilizar el siguiente
procedimiento. Cuando la llamada al otro servicio falla o se agota el tiempo de espera,
el código de llamada debe estar estructurado de forma que la misma llamada se repita
después de un breve tiempo de espera. Si la llamada falla de nuevo, la espera debe ser un
poco más larga antes del próximo intento. Las llamadas deben repetirse hasta un número
máximo de veces, aumentando cada vez el tiempo de espera. Después, el servicio debe
renunciar y proporcionar un servicio degradado, lo que podría significar devolver
algunos datos almacenados en caché obsoletos o ningún dato, en función de la situación.

Registro
Las operaciones importantes en un servicio siempre deben registrarse. La información
de registro necesita clasificarse para que tenga algún valor. Una lista común de
categorías es depuración, información, aviso, error y grave. La información de registro
debe recopilarse mediante un servicio central de agregación de registros y no debe
almacenarse en un nodo individual del clúster. Los registros agregados son fáciles
de analizar y filtrar para obtener información relevante.

Gestión de errores
Tal y como hemos mencionado, cada servicio de aplicación en una aplicación distribuida
depende de otros servicios. Como desarrolladores, siempre debemos esperar lo peor
y contar con una gestión adecuada de los errores en su lugar. Una de las prácticas
más importantes es equivocarse con rapidez. Programa el servicio de forma que los
errores irrecuperables se descubran lo antes posible y, si se detecta un error, el servicio
falle inmediatamente. Pero no olvides registrar la información más significativa en
STDERR o STDOUT, que puede ser útil más tarde para los desarrolladores u operadores
de sistema para realizar un seguimiento de los errores de funcionamiento del sistema.
Asimismo, envía los errores más útiles al autor de la llamada, indicando con la mayor
precisión posible por qué falló la llamada.

[ 117 ]
Arquitectura de aplicaciones distribuidas

Un ejemplo de un error rápido es comprobar siempre los valores de entrada


proporcionados por el autor de la llamada. ¿Los valores de los intervalos son los esperados
y están completos? De no ser así, no intentes continuar con el procesamiento; cancela
inmediatamente la operación.

Redundancia
Un sistema crítico debe estar disponible todo el tiempo, a cualquier hora, 365 días al año.
No es aceptable encontrar tiempo de inactividad, ya que podría resultar en una enorme
pérdida de oportunidades o de reputación para la empresa. En una aplicación altamente
distribuida, no se puede obviar la probabilidad de que se produzca un fallo en al menos
uno de los muchos componentes. Podemos decir que la pregunta no es si un componente
fallará, sino más bien cuándo fallará.

Para evitar el tiempo de inactividad cuando uno de los muchos componentes del
sistema falla, cada parte individual del sistema necesita ser redundante. Esto incluye
los componentes de la aplicación, así como todas las partes de la infraestructura. Eso
significa que si, por ejemplo, tenemos un servicio de pago como parte de nuestra
aplicación, necesitamos ejecutar este servicio de forma redundante. La forma más fácil
de hacerlo es ejecutar varias instancias de este mismo servicio en diferentes nodos de
nuestro clúster. Lo mismo se aplica, por ejemplo, a un router perimetral o un equilibrador
de carga. No podemos permitirnos que dejen de funcionar. Por eso, el enrutador o el
equilibrador de carga deben ser redundantes.

Comprobaciones de estado
Hemos mencionado varias veces que en una arquitectura de aplicaciones distribuidas,
con todas sus partes, el fallo de un componente individual es muy probable y es solo una
cuestión de tiempo que suceda. Por este motivo, ejecutamos todos los componentes del
sistema de forma redundante. Los servicios proxy equilibran el tráfico entre las instancias
individuales de un servicio.

Pero ahora hay otro problema. ¿Cómo sabe el proxy o el router si una determinada instancia
de servicio está disponible o no? Podría haberse caído o no responder. Para resolver este
problema, utilizamos las llamadas "comprobaciones de estado". El proxy, o algún otro
servicio del sistema en nombre del proxy, sondea periódicamente todas las instancias de
servicio y comprueba su estado. La pregunta es, básicamente, ¿sigues ahí? ¿Estás bien? La
respuesta de cada servicio es sí o no, o se agota el tiempo de espera de la comprobación
de estado si la instancia ya no responde.

Si el componente responde con no o se agota el tiempo de espera, el sistema elimina


la instancia correspondiente e inicia una nueva instancia en su lugar. Si todo esto ocurre
de forma totalmente automatizada, decimos que tenemos un sistema de "reparación
automática".

[ 118 ]
Capítulo 6

Patrón de cortacircuitos
Un cortacircuitos es un mecanismo que se utiliza para evitar que una aplicación
distribuida deje de funcionar por un error en cascada de muchos componentes esenciales.
Los cortacircuitos ayudan a evitar que un componente averiado destruya otros servicios
dependientes en un efecto dominó. Como los cortacircuitos de un sistema eléctrico, que
impiden que una casa arda por una avería de un aparato mal enchufado interrumpiendo
la línea eléctrica, los cortacircuitos de una aplicación distribuida interrumpen la conexión
del servicio A al servicio B si este último no responde o está funcionando mal.

Esto es posible si incluimos una llamada de servicio protegida en un objeto de


cortacircuitos. Este objeto supervisa los fallos. Una vez que el número de errores
alcanza un umbral concreto, el cortacircuitos se activa. Todas las llamadas siguientes
al cortacircuitos devolverán un error, sin realizar la llamada protegida:

Patrón de cortacircuitos

[ 119 ]
Arquitectura de aplicaciones distribuidas

Ejecución en producción
Para ejecutar con éxito una aplicación distribuida en producción, debemos considerar
algunos aspectos además de las prácticas recomendadas y las pautas presentadas
en las secciones anteriores. Un área específica que se nos ocurre es la introspección
y la supervisión. Vamos a explicar en detalle los aspectos más importantes.

Registro
Una vez que una aplicación distribuida está en producción, no es posible depurarla.
Pero ¿cómo podemos entonces averiguar cuál es exactamente la causa de un error
de funcionamiento de la aplicación reportado por un usuario? La solución a este problema
es producir información de registro abundante y significativa. Los desarrolladores
necesitan instrumentar sus servicios de aplicación de forma que puedan emitir
información útil, como cuando se produce un error o se encuentra una situación
potencialmente inesperada o no deseada. A menudo, esta información se transmite
a STDOUT y STDERR, desde donde es recopilada por los daemons del sistema
que escriben la información en archivos locales o la reenvían a un servicio central
de agregación de registros.

Si hay suficiente información en los registros, los desarrolladores pueden utilizar esos
registros para rastrear la causa de los errores en el sistema en que se han notificado.

En una arquitectura de aplicaciones distribuidas con todos sus componentes, el registro


es aún más importante que en una aplicación monolítica. Las rutas de ejecución de una
sola solicitud a través de todos los componentes de la aplicación pueden ser muy
complejas. Además, recuerda que los componentes se distribuyen a través de un clúster
de nodos. Por lo tanto, tiene sentido registrar todo lo importante y en cada entrada
de registro añadir la hora exacta del suceso, el componente en el que ha sucedido y el
nodo en el que el componente se estaba ejecutando, entre otras muchas cosas. Además,
la información de registro debe agregarse en una ubicación central de modo que esté
disponible para que los desarrolladores y los operadores del sistema la analicen.

Seguimiento
El seguimiento se utiliza para averiguar cómo se canaliza una solicitud individual
a través de una aplicación distribuida y cuánto tiempo se emplea en general en la
solicitud y en cada componente individual. Esta información, si se recopila, puede
utilizarse como una de las fuentes de los paneles que muestran el comportamiento
y el estado del sistema.

[ 120 ]
Capítulo 6

Supervisión
A los operadores les gusta tener paneles que muestren las métricas clave en directo del
sistema para conocer el estado general de la aplicación de un solo vistazo. Estas métricas
pueden ser métricas no funcionales como la memoria y el uso de la CPU, el número
de bloqueos de un sistema o un componente de aplicación, el estado de un nodo, etc.,
así como métricas funcionales y, por lo tanto, específicas de la aplicación, como el número
de pagos en un sistema de pedidos o el número de artículos fuera de stock en un servicio
de inventario.

La mayoría de las veces, los datos básicos utilizados para agregar los números que
se utilizan en un panel se extraen de la información de registro. Puede ser un registro
del sistema, que se utilizará principalmente para métricas no funcionales, y registros
de nivel de aplicación para métricas funcionales.

Actualizaciones de la aplicación
Una de las ventajas competitivas para una empresa es poder reaccionar de manera
oportuna a las cambiantes situaciones del mercado. Parte de esta misión consiste
en poder adaptar rápidamente una aplicación para satisfacer las necesidades creadas
y modificadas o agregar nuevas funcionalidades. Cuanto más rápido podamos actualizar
nuestras aplicaciones, mejor. Actualmente, muchas empresas añaden características
nuevas o modificadas varias veces al día.

Dado que las actualizaciones de las aplicaciones son tan frecuentes, estas actualizaciones
deben evitar ser disruptivas. No podemos permitir que el sistema deje de funcionar para
ejecutar tareas de mantenimiento cuando se actualiza. Todo debe suceder de manera
fluida y transparente.

Actualizaciones graduales
Una forma de actualizar una aplicación o un servicio de aplicación es utilizar
actualizaciones progresivas. Suponiendo que la parte concreta de software que debe
actualizarse funciona en varias instancias, solo entonces podemos utilizar este tipo
de actualización.

Lo que ocurre es que el sistema detiene una instancia del servicio actual y la sustituye
por una instancia del nuevo servicio. En cuanto la nueva instancia esté lista, recibirá
tráfico. Por lo general, la nueva instancia se supervisa durante algún tiempo para ver
si funciona o no como se esperaba y, si lo hace, la siguiente instancia del servicio actual
se quita y se sustituye por una nueva instancia. Este patrón se repite hasta que todas las
instancias del servicio se han sustituido.

Dado que siempre hay algunas instancias que se ejecutan en un momento dado, actuales
o nuevas, la aplicación está funcionando todo el tiempo. No es necesario recurrir
al tiempo de inactividad.
[ 121 ]
Arquitectura de aplicaciones distribuidas

Implementaciones blue-green
En las implementaciones blue-green, la versión actual del servicio de aplicación,
denominada Blue (azul), controla todo el tráfico de la aplicación. A continuación, instalamos
la nueva versión del servicio de la aplicación, denominada Green (Verde), en el sistema
de producción. El nuevo servicio aún no está conectado con el resto de la aplicación.

Una vez que se instala Green, se pueden ejecutar pruebas con este nuevo servicio y,
si tienen éxito, el router puede configurarse para canalizar todo el tráfico que antes iba
a Blue al nuevo servicio, Green. Entonces, se monitoriza el comportamiento de Green y,
si se cumplen todos los criterios de éxito, Blue puede desconectarse. Pero si, por alguna
razón, Green muestra algún comportamiento inesperado o no deseado, el router puede
reconfigurarse para devolver todo el tráfico a Blue. Green se puede eliminar y reparar,
y se puede ejecutar una nueva implementación blue-green con la versión corregida:

Implementación blue-green

Versiones Canary
Las versiones Canary son versiones en las que tenemos la versión actual del servicio de
aplicación y la nueva versión instalada en el sistema en paralelo. Por tanto, se parecen a
las implementaciones blue-green. Al principio, todo el tráfico se sigue enrutando a través
de la versión actual. A continuación, configuramos un router de modo que canalice
un pequeño porcentaje, digamos el 1 %, del tráfico global a la nueva versión del servicio
de aplicación. El comportamiento del nuevo servicio se vigila de cerca para averiguar
si funciona o no como se esperaba. Si todos los criterios de éxito se cumplen, entonces
el router está configurado para canalizar más tráfico, digamos un 5 % esta vez, a través
del nuevo servicio. De nuevo, el comportamiento del nuevo servicio se vigila de cerca
y, si tiene éxito, se enruta más y más tráfico hasta que lleguemos al 100 %. Una vez que
todo el tráfico se enruta al nuevo servicio y ha permanecido estable durante un tiempo,
la versión antigua del servicio puede desconectarse.

[ 122 ]
Capítulo 6

¿Por qué llamamos a esto "versión Canary" (canario)? El nombre procede de los mineros
de carbón que usaban canarios como sistema de alerta temprana en las minas.
Los canarios son especialmente sensibles al gas tóxico y si morían, los mineros sabían
que debían abandonar la mina inmediatamente.

Cambios irreversibles de datos


Si parte de nuestro proceso de actualización es ejecutar un cambio irreversible en nuestro
estado, como un cambio de esquema irreversible en una base de datos relacional de
respaldo, necesitamos abordarlo con especial cuidado. Es posible ejecutar estos cambios
sin tiempo de inactividad si se utiliza el enfoque correcto. Es importante reconocer que,
en esta situación, no se pueden implementar los cambios de código que requieren la
nueva estructura de datos en el almacén de datos al mismo tiempo que los cambios en los
datos. Más bien, es necesario dividir toda la actualización en tres pasos distintos. En el
primer paso, se despliega un esquema compatible con versiones anteriores y un cambio
de datos. Si funciona, se despliega el código nuevo en el segundo paso. Una vez más,
si funciona, se limpia el esquema en el tercer paso y se elimina la compatibilidad inversa:

Implementación de datos irreversibles o cambios de esquema

Reversión
Si tenemos actualizaciones frecuentes de nuestros servicios de aplicación que se ejecutan
en producción, tarde o temprano habrá un problema con una de esas actualizaciones.
Tal vez un desarrollador, al arreglar un error, ha introducido otro nuevo, que no fue
capturado por todas las pruebas automatizadas o tal vez manuales, por lo que la
aplicación funciona mal y es obligatorio revertir a la última versión correcta del servicio.
En este sentido, una reversión es una forma de recuperar el sistema ante un problema
muy grave.

[ 123 ]
Arquitectura de aplicaciones distribuidas

De nuevo, en una arquitectura de aplicaciones distribuidas, no se trata de si será


necesario una reversión alguna vez, sino de cuándo deberemos realizar una reversión.
Por lo tanto, debemos estar seguros de que siempre podamos volver a una versión
anterior de cualquier servicio que contenga nuestra aplicación. Las reversiones
no pueden ser una idea de última hora, sino que tienen que ser una parte probada
y comprobada de nuestro proceso de implementación.

Si estamos utilizando implementaciones blue-green para actualizar nuestros servicios,


las reversiones deben ser bastante simples. Todo lo que tenemos que hacer es cambiar
el router de la nueva versión green del servicio de vuelta a la versión blue anterior.

Resumen
En este capítulo, hemos aprendido qué es una arquitectura de aplicaciones distribuidas
y qué pautas y prácticas recomendadas son útiles o necesarias para ejecutar con éxito una
aplicación distribuida. Por último, hemos explicado lo que se necesita además de ejecutar
una aplicación de esta índole en producción.

En el próximo capítulo, explicaremos con detalle la conexión en red limitada a un solo


host. Explicaremos detalladamente cómo los contenedores ubicados en el mismo host
pueden comunicarse entre sí y cómo los clientes externos pueden acceder a aplicaciones
en contenedor si es necesario.

Preguntas
Responde a las siguientes preguntas para evaluar tu comprensión sobre el contenido
de este capítulo.

1. ¿Cuándo y por qué cada parte de una arquitectura de aplicaciones distribuidas


debe ser redundante? Explícalo en unas pocas frases cortas.
2. ¿Por qué necesitamos servicios DNS? Explícalo en tres o cinco frases.
3. ¿Qué es un cortacircuitos y por qué se necesita?
4. ¿Cuáles son las diferencias más importantes entre una aplicación monolítica
y una aplicación distribuida o multiservicio?
5. ¿Qué es una implementación blue-green?

[ 124 ]
Capítulo 6

Lectura adicional
Los siguientes artículos proporcionan información más detallada
(pueden estar en inglés):

• CircuitBreaker en http://bit.ly/1NU1sgW
• El modelo OSI se explica en http://bit.ly/1UCcvMt
• Implementación blue-green en http://bit.ly/2r2IxNJ

[ 125 ]
Conexión en red con
un solo host
En el último capítulo explicamos los patrones de arquitectura y las prácticas
recomendadas más importantes que se utilizan para gestionar una arquitectura
de aplicaciones distribuidas.

En este capítulo, explicaremos qué es el modelo de red de contenedores Docker


y su implementación de host único en forma de red de puente. En este capítulo también
hablaremos del concepto de redes definidas por software y cómo se utilizan para
proteger las aplicaciones en contenedores. Por último, también explicaremos cómo
podemos abrir los puertos del contenedor y hacer que los componentes en contenedores
sean accesibles para el mundo exterior.

En este capítulo trataremos los siguientes temas:


• Modelo de red de contenedores
• Protección mediante cortafuegos de la red
• Red de puente
• Red de host
• Red nula
• Ejecución en un espacio de nombres de red existente
• Gestión de puertos

Después de completar este módulo, serás capaz de hacer lo siguiente:


• Elaborar el modelo de red de contenedores, junto con todos los componentes
esenciales en una pizarra
• Crear y eliminar una red de puente personalizada
• Ejecutar un contenedor conectado a una red de puente personalizada
• Inspeccionar una red de puente
[ 127 ]
Conexión en red con un solo host

• Aislar los contenedores entre sí ejecutándolos en distintas redes de puente


• Publicar un puente de contenedor en un puerto host de tu elección

Requisitos técnicos
Para este capítulo, lo único que necesitarás es un host de Docker que sea capaz
de ejecutar contenedores Linux. También puedes usar tu ordenador portátil con
Docker para Mac o Windows o con Docker Toolbox instalado.

Modelo de red de contenedores


Hasta ahora, hemos trabajado con contenedores únicos. Pero, en realidad, una aplicación
de negocio en contenedores está formada por diversos contenedores que tienen que
colaborar para conseguir un objetivo. Por lo tanto, necesitamos crear un método para
que los contenedores individuales se comuniquen entre sí. Esto se consigue definiendo
rutas que podemos usar para enviar paquetes de datos entre los contenedores. Estas
rutas reciben el nombre de redes. Docker ha definido un modelo de red muy simple
denominado modelo de red de contenedor (CNM), para especificar los requisitos
que tiene que cumplir cualquier software que implemente una red de contenedor.
A continuación, se muestra una representación gráfica del CNM:

Modelo de red del contenedor

El CNM tiene tres elementos: sandbox, punto de conexión y red:

• Sandbox: el sandbox aísla perfectamente un contenedor del mundo exterior.


No se permite ninguna conexión de red entrante en el contenedor aislado.
Naturalmente, es poco probable que un contenedor sea de utilidad en un
sistema si no se pueden establecer comunicaciones. Para solucionarlo, tenemos
el elemento número dos, que es el punto de conexión.

[ 128 ]
Capítulo 7

• Punto de conexión: un punto de conexión es una pasarela controlada desde


el exterior en el sandbox de la red que protege al contenedor. El punto de
conexión conecta el sandbox de la red (pero no el contenedor) al tercer elemento
del modelo, que es la red.
• Red: la red es la ruta que transporta los paquetes de datos de una instancia
de comunicación desde un punto de conexión a otro, o en última instancia
de un contenedor a otro.

Cabe destacar que un sandbox de red puede tener cero o muchos puntos de conexión,
o, dicho de otra forma, cada contenedor que reside en un sandbox de red puede no
conectarse a ninguna red o puede conectarse a varias redes diferentes al mismo tiempo.
En el esquema anterior, la mitad de los tres sandbox de red están conectados a las dos
redes, 1 y 2, a través de un punto de conexión respectivo.

Este modelo de red es muy genérico y no especifica dónde se ejecutan los contenedores
individuales que se comunican entre sí en la red. Por ejemplo, todos los contenedores
deben ejecutarse en un host (local) que debe ser el mismo o bien podrían distribuirse en
un clúster de hosts (global).

Naturalmente, el CNM es tan solo un modelo que describe cómo conectar en red los
trabajos entre contenedores. Para poder usar las redes con nuestros contenedores,
necesitamos implementaciones reales del CNM. Para enfoques locales y globales, tenemos
varias implementaciones del CNM. En la tabla siguiente, se resumen las implementaciones
existentes y sus principales características. La lista no tiene un orden específico:

Red Empresa Alcance Descripción


Puente Docker Local Red simple basada en puentes Linux que
permiten conectar redes en un único host
Macvlan Docker Local Configura las direcciones de la capa
múltiple 2 (es decir, MAC) en una interfaz
de host física única
Overlay Docker Global Red de contenedor con función multinodo
basada en una LAN extensible virtual
(VXLan)
Weave Net Weaveworks Global Red Docker simple, flexible y de varios
hosts
Contiv Network Cisco Global Red de contenedor de código abierto
Plugin

Cualquier tipo de red no proporcionada directamente por Docker puede añadirse


a un host de Docker a modo de complemento.

[ 129 ]
Conexión en red con un solo host

Protección mediante cortafuegos de la red


Para Docker la seguridad ha sido siempre absolutamente prioritaria. Esta filosofía ha
tenido una influencia directa en el diseño y la implementación de las redes en un entorno
Docker único y de varios hosts. Las redes definidas por software son fáciles y baratas de
crear, y protegen con cortafuegos a los contenedores que se han conectado a esta red del
resto de contenedores no conectados, y del mundo exterior. Todos los contenedores que
pertenecen a la misma red pueden comunicarse libremente entre sí, mientras que otros
no tienen medios para hacerlo:

Redes Docker

En el diagrama anterior, tenemos dos redes llamadas delantera y trasera. Conectados a


la red delantera, tenemos los contenedores c1 y c2, y conectados a la red trasera tenemos
a los contenedores c3 y c4. c1 y c2 pueden comunicarse libremente entre sí, al igual que
c3 y c4. Pero c1 y c2 no tienen forma de comunicarse con c3 o c4, y viceversa.

¿Y qué ocurre si ahora tenemos una aplicación formada por tres servicios, webAPI,
productCatalog y base de datos? Queremos que webAPI se pueda comunicar con
productCatalog, pero no con la base de datos, y queremos que productCatalog se pueda
comunicar con el servicio de base de datos. Podemos resolver esta situación colocando
webAPI y la base de datos en redes diferentes y conectando productCatalog a estas dos
redes, como se muestra en el esquema siguiente:

Contenedor conectado a varias redes

[ 130 ]
Capítulo 7

Dado que la creación de SDN no es costosa y cada red proporciona mayor seguridad
aislando recursos del acceso no autorizado, se recomienda diseñar y ejecutar las
aplicaciones de forma que utilicen varias redes y que solo ejecuten servicios en la misma
red que tengan que comunicarse entre sí forzosamente. En el ejemplo anterior, no era
necesario que el componente API web se comunicase directamente con el servicio de
la base de datos, por lo que los hemos colocado en redes diferentes. Si se produce el peor
de los escenarios y un hacker ataca el componente API web, no podrá acceder a la base
de datos desde esa ubicación sin hackear antes el servicio de catálogo de productos.

Red de puente
La red de puente de Docker es la primera implementación del modelo de red del
contenedor que vamos a analizar en detalle. Esta implementación de red se basa en el
puente Linux. Cuando el daemon del docker se ejecuta por primera vez, crea un puente
Linux y lo denomina docker0. Este es el comportamiento predeterminado y puede
cambiarse modificando la configuración. A continuación, Docker crea una red con este
puente Linux y llama al puente de la red. Todos los contenedores que creamos en un host
de Docker y que no vinculamos explícitamente a otra red permiten que Docker se conecte
automáticamente a esta red de puente.
Para verificar que tenemos una red denominada bridge del tipo bridge (puente) definida
en nuestro host, podemos enumerar todas las redes en el host con el siguiente comando:
$ docker network ls

Esto debería proporcionar un resultado similar al siguiente:

Enumeración de todas las redes Docker predeterminadas

En tu caso, los ID serán diferentes pero el resto del resultado debería tener el mismo
aspecto. Tenemos una primera red denominada bridge que utiliza el controlador
bridge. El alcance es local y esto significa que este tipo de red está limitada a un único
host y no puede abarcar varios hosts. En un capítulo posterior, también trataremos los
distintos tipos de redes que tienen un alcance global, lo que quiere decir que pueden
abarcar todo un clúster de hosts.

[ 131 ]
Conexión en red con un solo host

Ahora, vamos a analizar qué es una red bridge con mayor detalle. Para ello,
vamos a usar el comando inspect de Docker:
$ docker network inspect bridge

Cuando se ejecuta, este resultado genera una gran cantidad de información detallada
sobre la red en cuestión. Esta información debería tener el aspecto siguiente:

Resultado generado al inspeccionar la red de puente Docker

[ 132 ]
Capítulo 7

Ya vimos los valores ID, Name, Driver y Scope cuando enumeramos todas las redes;
no es nada nuevo. Ahora vamos a explicar qué es el bloque de gestión de direcciones
IP (IPAM). IPAM es un software utilizado para rastrear las direcciones IP que se
utilizan en un ordenador. Lo más importante del bloque IPAM es el nodo Config con
sus valores para Subnet y Gateway. La subred de la red de puente se define de forma
predeterminada en 172.17.0.0/16.

Esto significa que todos los contenedores conectados a esta red tendrán una dirección IP
asignada por Docker que se obtiene del intervalo específico, que abarca de 172.17.0.2 a
172.17.255.255. La dirección 172.17.0.1 está reservada para el router de esta red cuyo
rol en este tipo de red lo asume el puente Linux. Es de esperar que el primer contenedor
que Docker conecte a esta red obtendrá la dirección 172.17.0.2. Todos los contenedores
posteriores obtendrán un número más alto; el siguiente esquema ilustra este hecho:

Red de puente

En el esquema anterior, podemos ver un espacio de nombres de red del host, que incluye
el punto de conexión eth0 del host, que suele ser un NIC si el host de Docker se ejecuta
en un dispositivo "bare metal" o un NIC virtual si el host de Docker es una máquina
virtual. Todo el tráfico hacia el host procede de eth0. El puente Linux es responsable
de enrutar todo el tráfico de red entre la red del host y la subred de la red de puente.

[ 133 ]
Conexión en red con un solo host

De forma predeterminada, solo se permite el tráfico de salida y se bloquea el de entrada.


Esto significa que las aplicaciones en contenedores pueden acceder a Internet pero no están
accesibles para el tráfico exterior. Cada contenedor conectado a la red obtiene su propia
conexión de ethernet virtual (veth) con el puente. Esto se ilustra en el siguiente diagrama:

Detalles de la red de puente

El diagrama anterior muestra el mundo desde el punto de vista del host. Más adelante,
en esta sección analizaremos la situación desde un contenedor.

No solo nos limitamos a la red bridge, ya que Docker nos permite definir nuestras
propias redes de puente personalizadas. Esta función es muy útil y también es una
práctica recomendada para ejecutar todos los contenedores en la misma red y para
usar redes de puente adicionales para aislar contenedores que no tienen que comunicarse
entre sí. Para crear una red de puente personalizada denominada sample-net, utiliza
el siguiente comando:
$ docker network create --driver bridge sample-net

[ 134 ]
Capítulo 7

Si lo hacemos, podemos inspeccionar qué subred ha creado Docker para esta nueva red
personalizada de la siguiente forma:
$ docker network inspect sample-net | grep Subnet

Esto devuelve el siguiente valor:


"Subnet": "172.18.0.0/16",

Evidentemente, Docker ha asignado el siguiente bloque libre de direcciones IP a nuestra


nueva red de puente personalizada. Si, por algún motivo, queremos especificar nuestro
propio intervalo de subredes al crear una red, podemos hacerlo usando el parámetro
--subnet:

$ docker network create --driver bridge --subnet "10.1.0.0/16"


test-net

Para evitar conflictos provocados por direcciones IP duplicadas, asegúrate de no crear


redes con subredes solapadas.

Ahora que ya hemos explicado qué es una red de puente y cómo podemos crear una
red de puente personalizada, queremos saber cómo podemos conectar contenedores a
estas redes. Primero, vamos a ejecutar de forma interactiva un contenedor Alpine sin
especificar la red a la que se conectará:
$ docker container run --name c1 -it --rm alpine:latest /bin/sh

En otra ventana del Terminal, vamos a inspeccionar el contenedor c1:


$ docker container inspect c1

[ 135 ]
Conexión en red con un solo host

En el resultado, vamos a centrarnos por un momento en la sección que contiene


la información relacionada con la red. Podemos encontrarla debajo del nodo
NetworkSettings. Se muestra en el siguiente resultado:

Sección de configuración de red de metadatos del contenedor

En el resultado anterior, podemos ver que el contenedor se ha conectado en realidad a la red


bridge, ya que el NetworkID es igual a 026e65...; lo que podemos ver en el código
anterior es el ID de la red bridge. También podemos ver que el contenedor ha obtenido
la dirección IP 172.17.0.4 asignada como se esperaba y que la gateway es 172.17.0.1.
Cabe destacar que el contenedor también tenía una MacAddress asociada. Esto es
importante porque el puente Linux utiliza la dirección MAC para el enrutamiento.

[ 136 ]
Capítulo 7

Hasta ahora, hemos tratado este aspecto desde fuera del espacio de nombres de la red
del contenedor. Ahora vamos a ver cómo sería la situación cuando no estamos solamente
dentro del contenedor, sino también dentro del espacio de nombres de la red del
contenedor. Dentro del contenedor c1, vamos a usar la herramienta ip para saber qué está
ocurriendo. Ejecuta el comando ip addr y observa el siguiente resultado que se genera:

Espacio de nombres del contenedor visto por la herramienta IP

La parte interesante del resultado anterior es el número 19, el punto de conexión eth0.
El punto de conexión veth0 que el puente Linux ha creado fuera del espacio de nombres
del contenedor se asigna a eth0 dentro del contenedor. Docker siempre asigna el primer
punto de conexión de un espacio de nombres de la red del contenedor a eth0, tal y como
podemos ver dentro del espacio de nombres. Si el espacio de nombres de la red se conecta
a una red adicional, ese punto de conexión se asignará a eth1, y así sucesivamente.

Dado que ahora no estamos realmente interesados en ningún punto de conexión aparte
de eth0, podríamos haber usado una variante más específica del comando, que habría
generado el siguiente resultado:
/ # ip addr show eth0
195: eth0@if196: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500
qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd
ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope
global eth0
valid_lft forever preferred_lft forever.

En el resultado, también podemos ver la dirección MAC (02:42:ac:11:00:02) y la IP


(172.17.0.2) que Docker ha asociado a este espacio de nombres de red del contenedor.

[ 137 ]
Conexión en red con un solo host

También podemos obtener información sobre cómo se enrutan las solicitudes usando
el comando ip route:
/ # ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.2

Este resultado nos dice que todo el tráfico dirigido a la gateway en 172.17.0.1 se enruta
a través del dispositivo eth0.

Ahora, vamos a ejecutar otro contenedor denominado c2 en la misma red:


$ docker container run --name c2 -d alpine:latest ping 127.0.0.1

El contenedor c2 también se conectará a la red bridge, ya que no hemos especificado


ninguna otra red. Su dirección IP será la siguiente dirección libre de la subred, que
es 172.17.0.3, como podemos comprobar:
$ docker container inspect --format "{{.NetworkSettings.
IPAddress}}" c2
172.17.0.3

Ahora, tenemos dos contenedores conectados a la red bridge. Podemos intentar


inspeccionar esta red una vez más para encontrar una lista de todos los contenedores
conectados en el resultado:
$ docker network inspect bridge

La información la encontramos debajo del nodo Containers:

Sección Containers del resultado de docker network inspect bridge

[ 138 ]
Capítulo 7

Una vez más, hemos acortado el resultado para facilitar su lectura.

Ahora vamos a crear dos contenedores adicionales, c3 y c4, y los vamos a conectar a
test-net. Para ello, utilizaremos el parámetro --network:

$ docker container run --name c3 -d --network test-net \


alpine:latest ping 127.0.0.1
$ docker container run --name c4 -d --network test-net \
alpine:latest ping 127.0.0.1

Vamos a inspeccionar network test-net y vamos a confirmar que los contenedores c3


y c4 están realmente conectados:
$ docker network inspect test-net

Esto producirá el siguiente resultado para la sección Containers:

Sección Containers del comando docker network inspect test-net

La siguiente pregunta que nos haremos es si los dos contenedores c3 y c4 pueden


comunicarse libremente entre sí. Para demostrar que sí pueden podemos utilizar exec
en el contenedor c3:
$ docker container exec -it c3 /bin/sh

Una vez dentro del contenedor, podemos intentar hacer ping en el contenedor c4
por nombre y dirección IP:
/ # ping c4
PING c4 (10.1.0.3): 56 data bytes
64 bytes from 10.1.0.3: seq=0 ttl=64 time=0.192 ms
64 bytes from 10.1.0.3: seq=1 ttl=64 time=0.148 ms
...

[ 139 ]
Conexión en red con un solo host

A continuación se muestra el resultado del comando ping usando la dirección IP del


contenedor c4:
/ # ping 10.1.0.3
PING 10.1.0.3 (10.1.0.3): 56 data bytes
64 bytes from 10.1.0.3: seq=0 ttl=64 time=0.200 ms
64 bytes from 10.1.0.3: seq=1 ttl=64 time=0.172 ms
...

La respuesta en ambos casos es que la comunicación entre los contenedores conectados


a la misma red está funcionando de la forma esperada. El hecho de que podamos usar el
nombre del contenedor al que queremos conectarnos nos muestra que la resolución de
nombres proporcionada por el servicio DNS de Docker funciona dentro de esta red.

Ahora queremos asegurarnos de que las redes bridge y test-net tienen una protección
de cortafuegos entre ambas. Para demostrar este hecho, podemos intentar hacer ping en el
contenedor c2 desde el contenedor c3, por su nombre o por su dirección IP:
/ # ping c2
ping: bad address 'c2'

A continuación se muestra el resultado del comando ping usando la dirección IP del


contenedor de destino c2:
/ # ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
^C
--- 172.17.0.3 ping statistics ---
43 packets transmitted, 0 packets received, 100% packet loss

El comando anterior se quedó bloqueado y tuve que terminarlo con Ctrl+C. De la


respuesta al ping de c2, también podemos ver que la resolución de nombres no funciona
entre redes. Este es el comportamiento esperado. Las redes proporcionan una capa
adicional de aislamiento y, por ende, de seguridad a los contenedores.

Anteriormente hemos comentado que un contenedor puede conectarse a varias redes. Ahora
vamos a conectar un contenedor c5 a las redes sample-net y test-net al mismo tiempo:
$ docker container run --name c5 -d \
--network sample-net \
--network test-net \
alpine:latest ping 127.0.0.1

[ 140 ]
Capítulo 7

Después podemos probar que c5 está accesible desde el contenedor c2 de forma similar
a cuando hemos probado lo mismo para los contenedores c4 y c2. El resultado indicará
que la conexión funciona correctamente.

Si queremos eliminar una red existente, podemos usar el comando docker network
rm, pero debemos tener en cuenta que no es posible eliminar una red que contenga
contenedores accidentalmente:
$ docker network rm test-net
Error response from daemon: network test-net id 863192... has
active endpoints

Antes de continuar, vamos a limpiar y eliminar todos los contenedores:


$ docker container rm -f $(docker container ls -aq)

A continuación, eliminamos las dos redes personalizadas que hemos creado:


$ docker network rm sample-net
$ docker network rm test-net

Red de host
Hay ocasiones en que queremos ejecutar un contenedor en el espacio de nombres de la
red del host. Esto puede ser necesario cuando tenemos que ejecutar el software en un
contenedor que se usa para analizar y depurar el tráfico de la red del host. No obstante,
debemos tener en cuenta que son escenarios muy específicos. Cuando ejecutamos software
empresarial en contenedores, no hay ningún motivo para ejecutar los contenedores
respectivos conectados a la red del host. Por motivos de seguridad, es absolutamente
recomendable que no ejecutes ningún contenedor conectado a la red del host en un
entorno de producción o similar al de producción.

¿Y cómo podemos ejecutar un contenedor dentro del espacio de nombres de la red del host? Solo
tenemos que conectar el contenedor a la red del host:
$ docker container run --rm -it --network host alpine:latest /bin/
sh

Si ahora utilizamos la herramienta ip para analizar el espacio de nombres de la red


desde dentro del contenedor, veremos que obtenemos exactamente la misma imagen que
si estuviésemos ejecutando la herramienta ip directamente en el host. Por ejemplo, si
inspecciono el dispositivo eth0 en mi host, obtendré esto:
/ # ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_
fast state UP qlen 1000
link/ether 02:50:00:00:00:01 brd

[ 141 ]
Conexión en red con un solo host

ff:ff:ff:ff:ff:ff
inet 192.168.65.3/24 brd 192.168.65.255 scope
global eth0
valid_lft forever preferred_lft forever
inet6 fe80::c90b:4219:ddbd:92bf/64 scope link
valid_lft forever preferred_lft forever

Aquí, puedo ver que 192.168.65.3 es la dirección IP asignada al host y que la dirección
MAC mostrada también se corresponde con la dirección del host.

También podemos inspeccionar las rutas para obtener lo siguiente (acortado):


/ # ip route
default via 192.168.65.1 dev eth0 src 192.168.65.3 metric 202
10.1.0.0/16 dev cni0 scope link src 10.1.0.1
127.0.0.0/8 dev lo scope host
172.17.0.0/16 dev docker0 scope link src 172.17.0.1
...
192.168.65.0/24 dev eth0 scope link src 192.168.65.3 metric 202

Antes de pasar a la siguiente sección de este capítulo, me gustaría destacar que el uso
de la red del host es peligrosa y tiene que evitarse en la medida de lo posible.

Red nula
Algunas veces tenemos que ejecutar unos cuantos servicios o trabajos de la aplicación
que no necesitan una conexión de red para llevar a cabo la tarea. Es absolutamente
recomendable que esas aplicaciones se ejecuten en un contenedor que esté conectado a
la red none. Este contenedor estará completamente aislado y estará protegido del acceso
desde el exterior. Vamos a ejecutar este contenedor:
$ docker container run --rm -it --network none alpine:latest /bin/
sh

Una vez dentro del contenedor, podemos comprobar que no hay ningún punto de
conexión de red eth0 disponible:
/ # ip addr show eth0
ip: can't find device 'eth0'

[ 142 ]
Capítulo 7

Tampoco hay información sobre enrutamiento disponible, lo que podemos comprobar


con el siguiente comando:
/ # ip route

Esta acción no devuelve ningún resultado.

Ejecución en un espacio de nombres


de red existente
Normalmente, Docker crea un nuevo espacio de nombres de red para cada contenedor
que ejecutamos. El espacio de nombres de la red del contenedor se corresponde con el
sandbox del modelo de red de contenedor que hemos explicado anteriormente. Cuando
conectamos el contenedor a una red, definimos un punto de conexión que conecta el
espacio de nombres de red del contenedor a la red real. De esta forma, tenemos un
contenedor para cada espacio de nombres de red.

Docker ofrece otra forma de definir el espacio de nombres de red donde se ejecuta un
contenedor. Al crear un nuevo contenedor, podemos especificar que debería conectarse a
la red o que está incluido en el espacio de nombres de red de un contenedor existente. Con
esta técnica podemos ejecutar varios contenedores en un único espacio de nombres de red:

Varios contenedores ejecutándose en un único espacio de nombres de red

En el diagrama anterior, podemos ver que en el espacio de nombres de red situado más
a la izquierda tenemos dos contenedores. Los dos contenedores, dado que comparten
el mismo espacio de nombres, pueden comunicarse entre sí en localhost. El espacio
de nombres de red (no los contenedores individuales) se conecta a la Red 1.

[ 143 ]
Conexión en red con un solo host

Esto resulta útil si queremos depurar la red de un contenedor existente sin ejecutar
procesos adicionales dentro de ese contenedor. Podemos conectar un contenedor especial
al espacio de nombres de red del contenedor para inspeccionarlo. Esta función también
la puede usar Kubernetes al crear un pod. En capítulos posteriores de este documento
obtendremos más información sobre Kubernetes y los pods.

Ahora vamos a demostrar cómo funciona.

1. Primero, vamos a crear una nueva red de puente:


$ docker network create --driver bridge test-net

2. Después, ejecutaremos un contenedor conectado a esta red:


$ docker container run --name web -d --network test-net
nginx:alpine

3. Por último, ejecutamos otro contenedor y lo conectamos a la red de nuestro


contenedor web:
$ docker container run -it --rm --network container:web
alpine:latest /bin/sh

Observa con detenimiento cómo definimos la red: --network container:web. Esto


indica a Docker que nuestro nuevo contenedor debe usar el mismo espacio de nombres
de red que el contenedor llamado web.

Dado que el nuevo contenedor está en el mismo espacio de nombres de red que el
contenedor web que ejecuta Nginx, podremos acceder a Nginx en localhost. Para
ello, podemos usar la herramienta wget que forma parte del contenedor Alpine para
conectarnos a Nginx. Debemos ver lo siguiente:
/ # wget -qO - localhost

<html>
<head>
<title>Welcome to nginx!</title>
...
</html>

Hemos acortado el resultado para facilitar su lectura. Ten en cuenta que existe una
diferencia importante entre ejecutar dos contenedores conectados a la misma red y dos
contenedores que se ejecutan en el mismo espacio de nombres de la red. En ambos casos,
los contenedores pueden comunicarse libremente entre sí, pero en el último caso, la
comunicación se realiza sobre localhost.

[ 144 ]
Capítulo 7

Para limpiar el contenedor y la red podemos usar el siguiente comando:


$ docker container rm --force web
$ docker network rm test-net

Gestión de puertos
Ahora que ya sabemos cómo podemos aislar entre sí los contenedores con cortafuegos
colocándolos en redes diferentes, y que podemos tener un contenedor conectado a más de
una red, tenemos un problema sin resolver. ¿Cómo podemos exponer un servicio de aplicación
al mundo exterior? Imagina un contenedor ejecutándose en un servidor web que aloja
nuestra webAPI anterior. Queremos que los clientes de Internet puedan acceder a esta API.
Hemos diseñado la API para que sea públicamente accesible. Para lograrlo, tenemos que
abrir una compuerta de nuestro cortafuegos para poder canalizar todo el tráfico externo
a nuestra API. Por motivos de seguridad, no queremos abrir las puertas del todo, solo
queremos tener una única compuerta controlada para dejar fluir el tráfico a través de ella.

Podemos crear esta compuerta asignando un puerto del contenedor a un puerto


disponible del host. También vamos a llamar a este puerto del contenedor para publicar
un puerto. Recuerda que el contenedor tiene su propia pila de red virtual, al igual que
el host. Por lo tanto, los puertos del contenedor y los puertos del host existen de forma
independiente y no tienen nada en común de forma predeterminada. Ahora podemos
comunicar un puerto del contenedor con un puerto libre del host para canalizar el tráfico
externo a través de este enlace, como se muestra en la captura de pantalla siguiente:

Asignación de puertos del contenedor a puertos del host

[ 145 ]
Conexión en red con un solo host

Ahora ha llegado el momento de demostrar cómo podemos asignar un puerto del


contenedor a un puerto del host. Este proceso se realiza al crear un contenedor. Tenemos
varias formas de hacerlo:

• En primer lugar, podemos dejar que Docker decida a qué puerto del host debe
asignarse nuestro puerto del contenedor. A continuación, Docker escogerá unos
de los puertos de host libres en el intervalo de 32xxx. Esta asignación automática
se realiza usando el parámetro -P:
$ docker container run --name web -P -d nginx:alpine

El comando anterior ejecuta un servidor Nginx en un contenedor. Nginx está a


la escucha en el puerto 80 dentro del contenedor. Con el parámetro -P estamos
diciendo a Docker que asigne todos los puertos del contenedor expuestos a un
puerto libre en el intervalo 32xxx. Podemos averiguar qué puerto de host está
usando Docker con el comando docker container port:
$ docker container port web
80/tcp -> 0.0.0.0:32768

El contenedor Nginx solo expone el puerto 80, y podemos ver que se ha asignado
al puerto del host 32768. Si abrimos una ventana del navegador y vamos a
localhost:32768, deberíamos ver la siguiente captura de pantalla:

Página de bienvenida de Nginx

• Otra forma de averiguar qué puerto del host está usando Docker para
nuestro contenedor es inspeccionarlo. El puerto del host forma parte del nodo
NetworkSettings:
$ docker container inspect web | grep HostPort
"HostPort": "32768"

[ 146 ]
Capítulo 7

• Por último, la tercera forma de obtener esta información es enumerar el contenedor:


$ docker container ls
CONTAINER ID IMAGE ...
PORTS NAMES
56e46a14b6f7 nginx:alpine ... 0.0.0.0:32768->80/tcp web

Ten en cuenta que, en el resultado anterior, la parte /tcp nos dice que el puerto se ha
abierto para la comunicación con el protocolo TCP, pero no para el protocolo UDP. TCP
es la opción predeterminada, y si queremos especificar que queremos abrir el puerto para
UDP, tenemos que especificarlo de forma explícita. La parte 0.0.0.0 de la asignación
nos dice que el tráfico de cualquier dirección IP del host puede llegar al puerto del
contenedor 80 del contenedor web.

Algunas veces, queremos asignar un puerto del contenedor a un puerto del host muy
específico. Podemos hacerlo usando el parámetro-p (o --publish). Ahora veamos cómo
se hace con el siguiente comando:
$ docker container run --name web2 -p 8080:80 -d nginx:alpine

El valor del parámetro -p tiene la siguiente forma <puerto del host>:<puerto del
contenedor>. Por lo tanto, en el caso anterior, asignamos el puerto del contenedor 80
al puerto del host 8080. Una vez que se ejecute el contenedor web2, podemos probarlo
en el navegador dirigiéndonos a localhost:8080. Deberíamos poder ver la página
de bienvenida de Nginx que vimos en el ejemplo anterior al realizar la asignación
automática de puertos.

Si se utiliza el protocolo UDP para las comunicaciones en un puerto específico, el


parámetro publish tendrá el siguiente aspecto -p 3000:4321/udp. Ten en cuenta que,
si queremos permitir las comunicaciones con los protocolos TCP y UDP en el mismo
puerto, tenemos que asignar cada protocolo por separado.

Resumen
En este capítulo hemos aprendido cómo pueden comunicarse entre sí los contenedores
que se ejecutan en un host único. Primero, hemos analizado el CNM que define
los requisitos de una red del contenedor y después hemos visto las distintas
implementaciones del CNM, como la red de puente. Después, analizamos las funciones
de la red de puente detalladamente y el tipo de información que nos proporciona Docker
sobre las redes y los contenedores conectados a esas redes. También explicamos cómo
adoptar dos perspectivas diferentes: desde el exterior y desde el interior del contenedor.

En el siguiente capítulo vamos a conocer Docker Compose. Explicaremos cómo crear


una aplicación formada por varios servicios, cada uno ejecutándose en un contenedor,
y de qué forma Docker Compose nos permite crear, ejecutar y escalar una aplicación
usando un enfoque declarativo.

[ 147 ]
Conexión en red con un solo host

Preguntas
Para evaluar tus conocimientos, responde a las siguientes preguntas:

1. Nombra los tres principales elementos del modelo de red de contenedor (CNM).
2. ¿Cómo crearías una red de puente personalizada llamada, por ejemplo, frontend?
3. ¿Cómo ejecutarías dos contenedores nginx:alpine conectados a la red frontend?
4. Para la red frontend, tendrás que obtener lo siguiente:
1. Las direcciones IP de todos los contenedores conectados
2. La subred asociada a la red

5. ¿Cuál es el objetivo de la red del host?


6. Nombra uno o dos escenarios donde el uso de la red del host sea adecuado.
7. ¿Cuál es el objetivo de la red none?
8. ¿En qué escenarios debe usarse la red none?

Lectura adicional
A continuación, puedes encontrar algunos artículos que describen los temas que hemos
tratado en este capítulo de forma más detallada (pueden estar en inglés):

• Información general sobre las redes de Docker en http://dockr.ly/2sXGzQn


• Redes de contenedores en http://dockr.ly/2HJfQKn
• Qué es un puente en https://bit.ly/2HyC3Od
• Uso de redes de puente en http://dockr.ly/2BNxjRr
• Uso de redes Macvlan en http://dockr.ly/2ETjy2x
• Conexión en red mediante la red del host en http://dockr.ly/2F4aI59

[ 148 ]
Docker Compose
En el capítulo anterior, aprendimos muchas cosas acerca de cómo funciona la red
de contenedores en un único host de Docker. Explicamos el modelo de red del
contenedor (CNM), que es la base de todas las redes formadas entre los contenedores
Docker, y luego nos sumergimos en diferentes implementaciones de CNM,
específicamente la red de puente.

En este capítulo, explicaremos el concepto de aplicación formada por varios servicios,


cada uno ejecutándose en un contenedor, y de qué forma Docker Compose nos permite
crear, ejecutar y escalar una aplicación usando un enfoque declarativo.

En el capítulo, abordaremos los siguientes temas:

• Desmitificación del enfoque declarativo frente al imperativo


• Ejecución de una aplicación multiservicio
• Escalado de un servicio
• Creación y envío de una aplicación

Después de terminar este capítulo, el lector será capaz de hacer lo siguiente:

• Explicar en unas pocas frases cortas las principales diferencias entre un enfoque
imperativo y declarativo para definir y ejecutar una aplicación
• Describir con sus propias palabras la diferencia entre un contenedor y un servicio
de Docker Compose
• Crear un archivo YAML de Docker Compose para una sencilla aplicación
multiservicio
• Crear, enviar, implementar y desmantelar una sencilla aplicación multiservicio
mediante Docker Compose
• Utilizar Docker Compose para aumentar o reducir la capacidad de un servicio
de aplicación

[ 149 ]
Docker Compose

Requisitos técnicos
El código que acompaña a este capítulo se encuentra en https://github.com/
appswithdockerandkubernetes/labs/tree/master/ch08.

Desmitificación del enfoque declarativo


frente al imperativo
Docker Compose es una herramienta que incluye Docker que se utiliza principalmente
cuando necesitamos ejecutar y organizar contenedores que se ejecutan en un único host
de Docker. Esto incluye, entre otras cosas, el desarrollo , la integración continua (CI),
las pruebas automáticas y los procesos de control de calidad manuales.

Docker Compose utiliza archivos formateados en YAML como entrada. De forma


predeterminada, Docker Compose espera que estos archivos se denominen docker-
compose.yml, pero es posible utilizar otros nombres. Se dice que el contenido de un
docker-compose.yml es una forma declarativa de describir y ejecutar una aplicación
en contenedores que potencialmente conste de varios contenedores.

Entonces, ¿qué significa declarativo?

En primer lugar, declarativo es el antónimo de imperativo. Es verdad que eso no ayuda


mucho. Ahora que he introducido otro concepto, tengo que explicar ambos:

• Imperativo: es una forma en la que podemos resolver los problemas


especificando el procedimiento exacto que debe seguir el sistema.

Si le indico a un sistema como, por ejemplo, el daemon de Docker, de forma


imperativa cómo ejecutar una aplicación, eso significa que tengo que describir
paso a paso lo que tiene que hacer el sistema y cómo tiene que reaccionar
si se produce alguna situación inesperada. Tengo que ser muy explícito
y preciso en mis instrucciones. Tengo que cubrir todos los casos extremos
y cómo deben tratarse.

• Declarativo: es una forma en la que podemos resolver problemas sin que sea
necesario que el programador especifique un procedimiento exacto a seguir.

Un enfoque declarativo significa que le indico al motor de Docker cuál


es el estado que deseo que tenga una aplicación y este tiene que averiguar por
sí mismo cómo lograr ese estado y cómo reconciliarlo si el sistema se desvía de él.

Docker recomienda claramente el enfoque declarativo cuando se trata de aplicaciones


en contenedores. En consecuencia, la herramienta Docker Compose utiliza este enfoque.

[ 150 ]
Capítulo 8

Ejecución de una aplicación multiservicio


En la mayoría de los casos, las aplicaciones no constan de un solo bloque monolítico,
sino más bien de varios servicios de aplicaciones que trabajan juntos. Cuando se utilizan
contenedores Docker, cada servicio de aplicación se ejecuta en su propio contenedor.
Cuando queramos ejecutar una aplicación multiservicio, podríamos iniciar todos los
contenedores participantes con el conocido comando de ejecución docker container.
Pero esto es ineficaz en el mejor de los casos. Con la herramienta Docker Compose,
disponemos de una forma de definir la aplicación de forma declarativa en un archivo
que utiliza el formato YAML.

Echemos un vistazo al contenido de un archivo docker-compose.yml sencillo:


version: "3.5"
services:
web:
image: appswithdockerandkubernetes/ch08-
web:1.0
ports:
- 3000:3000
db:
image: appswithdockerandkubernetes/ch08-db:1.0
volumes:
- pets-data:/var/lib/postgresql/data

volumes:
pets-data:

Las líneas del archivo se explican de la siguiente manera:

• version: en esta línea, especificamos la versión del formato de Docker Compose


que queremos utilizar. En el momento de escribir este libro, era la versión 3.5.
• services: en esta sección, especificamos los servicios que componen nuestra
aplicación en el bloque services. En nuestro ejemplo, tenemos dos servicios
de aplicación y los llamamos web y db:
°° web: el servicio web utiliza la imagen appswithdockerandkubernetes/
ch08-web:1.0 de Docker Hub y publica el puerto del contenedor
3000 en el puerto del host,que también es 3000.
°° dB: el servicio db, por otro lado, utiliza la imagen
appswithdockerandkubernetes/ch08-db:1.0, que es una base
de datos PostgreSQL personalizada. Vamos a montar un volumen
llamado pets-data en el contenedor del servicio db .

[ 151 ]
Docker Compose

• volumes: los volúmenes que utilizan cualquiera de los servicios deben


declararse en esta sección. En nuestro ejemplo, esta es la última sección del
archivo. La primera vez que se ejecuta la aplicación, Docker crea un volumen
denominado pets-data y, a continuación, en ejecuciones posteriores, si el
volumen todavía está allí, se reutiliza. Esto podría ser importante cuando
la aplicación, por alguna razón, se bloquea y hay que reiniciarla. En ese caso,
los datos anteriores todavía están presentes y listos para ser utilizados por
el servicio de base de datos reiniciado.

Dirígete a la subcarpeta ch08 de la carpeta labs e inicia la aplicación utilizando


Docker Compose:
$ docker-compose up

Si escribimos el comando anterior, la herramienta supondrá que debe haber un archivo


en el directorio actual denominado docker-compose.yml y lo usará para ejecutarse.
En nuestro caso, efectivamente es así y la aplicación se iniciará. El resultado que
deberíamos ver se parece al siguiente:

Ejecución de la aplicación de ejemplo (primera parte)

[ 152 ]
Capítulo 8

Ejecución de la aplicación de ejemplo (segunda parte)

[ 153 ]
Docker Compose

El resultado se explica de la siguiente manera:

• En la primera parte del resultado, podemos ver como Docker Compose obtiene
las dos imágenes de las que consta nuestra aplicación. A esto le sigue la creación
de una red ch08_default y un volumen ch08_pets-data, seguido de los
dos contenedores, ch08_web_1 y ch08_db_1, uno para cada servicio, web y db.
A todos los nombres, Docker Compose les añade automáticamente el prefijo del
nombre del directorio principal que, en este caso, se denomina ch08.
• Después de eso, vemos los registros que han producido los dos contenedores.
Cada línea del resultado lleva el prefijo del nombre del servicio y cada resultado
del servicio es de un color diferente. Aquí, la parte esencial la produce la base
de datos y solo hay una línea que es del servicio web.

Ahora podemos abrir una pestaña del navegador y acceder a localhost:3000/pet.


Deberíamos ser recibidos con una bonita imagen de un gato y alguna información
adicional sobre el contenedor del que proviene, como se muestra en la siguiente captura
de pantalla:

La aplicación de ejemplo en el navegador

[ 154 ]
Capítulo 8

Actualiza el navegador varias veces para ver otras imágenes de gatos. La aplicación
selecciona la imagen actual de forma aleatoria a partir de un conjunto de 12 imágenes
cuyas URL están almacenadas en la base de datos.

Dado que la aplicación se ejecuta en el modo interactivo y, por lo tanto, el Terminal


donde ejecutamos Docker Componer está bloqueado, podemos cancelar la aplicación
pulsando Ctrl+C. Si lo hacemos, veremos lo siguiente:
^CGracefully stopping... (press Ctrl+C again to force)
Stopping ch08_web_1 ... done
Stopping ch08_db_1 ... done

Veremos que el servicio de base de datos se detiene inmediatamente mientras que


el servicio web tarda unos 10 segundos en hacerlo. El motivo es que el servicio de base
de datos recibe la señal SIGTERM que envía Docker y reacciona a ella, mientras que
el servicio web no y, por lo tanto, Docker lo interrumpe pasados 10 segundos.

Si volvemos a ejecutar la aplicación, el resultado será mucho más corto:

Resultado de docker-compose up

Esta vez, no hemos tenido que descargar las imágenes y la base de datos no ha tenido
que inicializarse a partir de cero, sino que se han reutilizado los datos que ya estaban
presentes en el volumen pets-data de la ejecución anterior.

También podemos ejecutar la aplicación en segundo plano. Todos los contenedores


se ejecutarán como daemons. Para ello, solo tenemos que utilizar el parámetro -d,
como se muestra en el siguiente código:
$ docker-compose up -d

[ 155 ]
Docker Compose

Docker Compose nos ofrece muchos más comandos aparte de up. Podemos utilizarlo
para obtener una lista de todos los servicios que forman parte de la aplicación:

Resultado de docker-compose ps

Este comando es similar a docker container ls, con la única diferencia de que solo
muestra los contenedores que forman parte de la aplicación.

Para detener y limpiar la aplicación, usamos el comando docker-compose down


$ docker-compose down
Stopping ch08_web_1 ... done
Stopping ch08_db_1 ... done
Removing ch08_web_1 ... done
Removing ch08_db_1 ... done
Removing network ch08_default

Si también queremos eliminar el volumen de la base de datos, podemos utilizar


el siguiente comando:
$ docker volume rm ch08_pets-data

¿Por qué aparece el prefijo ch08 en el nombre del volumen? En el archivo docker-compose.
yml, hemos pedido al volumen que utilice pets-data. Pero, como ya hemos
mencionado, Docker Compose incluye un prefijo en todos los nombres con el nombre
de la carpeta principal del archivo docker-compose.yml, más un signo de subrayado.
En este caso, la carpeta principal se llama ch08.

Escalado de un servicio
Ahora, vamos a suponer, por un momento, que nuestra aplicación de ejemplo ha estado
en funcionamiento en el sitio web y ha sido todo un éxito. Un montón de gente quiere
ver nuestras bonitas imágenes de animales. Pero ahora tenemos un problema, porque
nuestra aplicación ha empezado a ralentizarse. Para contrarrestarlo, queremos ejecutar
varias instancias del servicio web. Con Docker Compose, esto es muy fácil.

[ 156 ]
Capítulo 8

La ejecución de más instancias también se denomina escalado vertical. Podemos utilizar


esta herramienta para escalar verticalmente nuestro servicio web hasta, por ejemplo,
tres instancias:
$ docker-compose up --scale web=3

Si hacemos esto, nos espera una sorpresa. El resultado será similar a la siguiente captura
de pantalla:

Resultado de docker-compose --scale

La segunda y la tercera instancia del servicio web no se inician. El mensaje de error nos
dice por qué: no podemos usar el mismo puerto de host más de una vez. Cuando las
instancias 2 y 3 intentan iniciarse, Docker se da cuenta de que el puerto 3000 ya está
ocupado por la primera instancia. ¿Qué podemos hacer? Pues bien, simplemente podemos
dejar que Docker decida qué puerto de host usar para cada instancia.
Si, en la sección ports del archivo compose, solo especificamos el puerto del contenedor
y dejamos fuera el puerto del host, Docker selecciona automáticamente un puerto
efímero. Vamos a hacer exactamente eso:
1. Primero, vamos a desmantelar la aplicación:
$ docker-compose down

2. A continuación, modificamos el archivo docker-compose.yml para que quede


de la siguiente manera:
version: "3.5"
services:
web:
image: appswithdockerandkubernetes/ch08-web:1.0
ports:
- 3000
db:

[ 157 ]
Docker Compose

image: appswithdockerandkubernetes/ch08-db:1.0
volumes:
- pets-data:/var/lib/postgresql/data

volumes:
pets-data:

3. Ahora, podemos volver a iniciar la aplicación y escalarla verticalmente


justo después:
$ docker-compose up -d
$ docker-compose scale web=3
Starting ch08_web_1 ... done
Creating ch08_web_2 ... done
Creating ch08_web_3 ... done

4. Si ahora utilizamos docker-compose ps, deberíamos ver la siguiente captura


de pantalla:

Resultado de docker-compose ps

5. Como podemos ver, cada servicio se ha asociado a un puerto de host diferente.


Podemos intentar ver si funcionan, por ejemplo, usando curl. Vamos a probar
la tercera instancia, ch08_web_3:
$ curl -4 localhost:32770
Pets Demo Application

La respuesta, Pets Demo Application, nos indica que, de hecho, nuestra aplicación
sigue funcionando como se esperaba. Pruébalo para las otras dos instancias para
asegurarte.

[ 158 ]
Capítulo 8

Creación y envío de una aplicación


También podemos usar el comando docker-compose build para crear las imágenes de
una aplicación definida en el archivo compose subyacente. Sin embargo, para que esto
funcione, tendremos que agregar la información de compilación al archivo de docker-
compose. En la carpeta, tenemos un archivo, docker-compose.dev.yml, que contiene
las instrucciones ya agregadas:
version: "3.5"
services:
web:
build: web
image: appswithdockerandkubernetes/ch08-
web:1.0
ports:
- 3000:3000
db:
build: database
image: appswithdockerandkubernetes/ch08-db:1.0
volumes:
- pets-data:/var/lib/postgresql/data

volumes:
pets-data:

Observa la clave build de cada servicio. El valor de esa clave indica el contexto o
la carpeta en la que Docker espera encontrar el Dockerfile para compilar la imagen
correspondiente.

Vamos a usar ese archivo:


$ docker-compose -f docker-compose.dev.yml build

El parámetro -f le indica a la aplicación Docker Compose qué archivo se va a utilizar.

Para enviar todas las imágenes a Docker Hub, podemos usar docker-compose push.
Para que esto se realice correctamente, tenemos que haber iniciado sesión en Docker
Hub; de lo contrario aparece un error de autenticación al enviarlas. Por lo tanto, en mi
caso, hago lo siguiente:
$ docker login -u appswithdockerandkubernetes -p <password>

Suponiendo que hayamos iniciado sesión correctamente, puedo enviar el siguiente


código:
$ docker-compose -f docker-compose.dev.yml push

[ 159 ]
Docker Compose

El comando anterior envía las dos imágenes a la cuenta


appswithdockerandkubernetes en Docker Hub. Puedes encontrar estas dos imágenes
en la URL https://hub.docker.com/u/appswithdockerandkubernetes/:.

Resumen
En este capítulo, hemos presentado la herramienta docker-compose. Esta herramienta
se utiliza principalmente para ejecutar y escalar aplicaciones multiservicio en un solo
host de Docker. Normalmente, los desarrolladores y los servidores CI trabajan con hosts
individuales y esos dos son los principales usuarios de Docker Compose. La herramienta
utiliza archivos YAML como entrada que contienen la descripción de la aplicación
de forma declarativa.

La herramienta también se puede utilizar para crear y enviar imágenes, entre otras
muchas útiles tareas. El código que acompaña a este capítulo se encuentra en labs/ch08.

En el siguiente capítulo, explicaremos los orquestadores. Un orquestador es un software


de infraestructura que se utiliza para ejecutar y administrar aplicaciones en contenedores
en un clúster y se asegura de que estas aplicaciones estén siempre en el estado deseado.

Preguntas
Para evaluar el progreso de tu aprendizaje, responde a las siguientes preguntas:

1. ¿Cómo se utiliza docker-compose para ejecutar una aplicación en el modo


de daemon?
2. ¿Cómo se utiliza docker-compose para ver los detalles del servicio en
ejecución?
3. ¿Cómo se escala verticalmente un servicio web en particular a, por ejemplo,
tres instancias?

Lectura adicional
En los siguientes enlaces, se proporciona información adicional sobre los temas que
hemos tratado en este capítulo (el contenido puede estar en inglés):

• El sitio web oficial de YAML en http://yaml.org/


• Documentación de Docker Compose en http://dockr.ly/1FL2VQ6
• Referencia de la versión 3 del archivo de Compose en http://dockr.ly/2iHUpeX

[ 160 ]
Orquestadores
En el capítulo anterior, ofrecimos una introducción de Docker Compose, una herramienta
que nos permite trabajar con aplicaciones multiservicio que se definen de forma
declarativa en un único host de Docker.

En este capítulo vamos a explicar el concepto de "orquestador". Aprenderás por qué


los orquestadores son necesarios y cómo funcionan. En este capítulo también se ofrece
información general sobre los orquestadores más populares, así como algunas de sus
ventajas e inconvenientes.

En este capítulo, abordaremos los siguientes temas:

• ¿Qué son los orquestadores y por qué los necesitamos?


• Las tareas de un orquestador
• Información general de orquestadores populares

Una vez que leas este capítulo, podrás:

• Nombrar tres o cuatro tareas que realiza un orquestador


• Enumerar dos o tres de los orquestadores más populares
• Explicar a un lego en la materia con tus propias palabras y con analogías
apropiadas por qué necesitamos orquestadores de contenedores

[ 161 ]
Orquestadores

¿Qué son los orquestadores y por qué


los necesitamos?
En el Capítulo 6, Arquitectura de aplicaciones distribuidas, explicamos los patrones y las
prácticas recomendadas que se utilizan habitualmente para crear, distribuir y ejecutar
una aplicación altamente distribuida. Ahora bien, si nuestra aplicación altamente
distribuida está incluida en un contenedor, nos enfrentamos a los mismos problemas
o desafíos que con una aplicación distribuida no incluida en un contenedor. Algunos
de estos desafíos que se explican en el Capítulo 6, Arquitectura de aplicaciones distribuidas,
son la detección de servicios, el equilibrio de carga y el escalado, entre otros.

Similar a lo que hizo Docker con los contenedores (estandarizar el empaquetado y la


distribución de software con la introducción de contenedores), nos gustaría tener alguna
herramienta o software de infraestructura que se encargara de todos o de la mayoría
de los desafíos mencionados. Este software resulta ser lo que llamamos "orquestadores"
o "motores de orquestación".

Si lo que acabo de decir no tiene mucho sentido para ti todavía, vamos a explicarlo
desde un ángulo diferente. Imagina un artista que toca un instrumento. Podría dar un
maravilloso concierto a su público, solo el artista con su instrumento. Ahora imagina una
orquesta de músicos. Mételos en una habitación, dales las notas de una sinfonía, pídeles
que toquen y sal de la habitación. Sin un director de orquesta, este grupo de músicos
talentosos no sería capaz de tocar esta pieza en armonía; sonaría como una cacofonía.
Solo si la orquesta tiene un director que dirija al grupo de músicos, la música resultante
de la orquesta será placentera para nuestros oídos:

Un orquestador de contenedores es como el director de una orquesta

[ 162 ]
Capítulo 9

En lugar de músicos, ahora tenemos contenedores, y en lugar de instrumentos diferentes,


tenemos contenedores que tienen diferentes requisitos para los hosts de contenedor
en los que se ejecutan. Y en lugar de música sonando a tempos distintos, tenemos
contenedores que se comunican entre sí de formas particulares y cuya capacidad debe
aumentarse y reducirse. En este sentido, un orquestador de contenedores desempeña
un papel muy similar al de un director de orquesta. Se asegura de que los contenedores
y otros recursos de un cluster funcionen al unísono en armonía.

Espero que ahora tengas más claro lo que es un orquestador de contenedores y por qué
lo necesitamos. Ahora podemos preguntarnos cómo el orquestador consigue el resultado
esperado, o dicho de otra forma, cómo asegurarnos de que todos los contenedores del
clúster funcionen al unísono en armonía. Bueno, la respuesta es que el orquestador tiene
que ejecutar tareas muy específicas, del mismo modo que el director de una orquesta
tiene también un conjunto de tareas que realizar para frenar y avivar al mismo tiempo
la orquesta.

Las tareas de un orquestador


Entonces, ¿cuáles son las tareas que esperamos que realice un orquestador para que valga la pena
el dinero invertido? Veámoslas en detalle. En la siguiente lista se muestran las tareas más
importantes que, en el momento de escribir este documento, los usuarios de empresa
esperan normalmente que realice su orquestador.

Conciliar el estado deseado


Cuando se utiliza un orquestador, se indica de forma declarativa cómo se quiere
que ejecute una aplicación o un servicio de aplicación determinado. Aprendimos las
diferencias entre declarativo e imperativo en el Capítulo 8, Docker Compose. Parte de esta
forma declarativa de describir el servicio de aplicación que queremos ejecutar son los
elementos como la imagen de contenedor que se va a utilizar, la cantidad de instancias
que se van a ejecutar de este servicio, los puertos que se van a abrir, etcétera. Esta
declaración de las propiedades de nuestro servicio de aplicación es lo que llamamos
estado deseado.

Por lo tanto, cuando le decimos por primera vez al orquestador que cree este nuevo
servicio de aplicación basado en la declaración, el orquestador se asegura de programar
tantos contenedores en el clúster como sean necesarios. Si la imagen del contenedor
aún no está disponible en los nodos de destino del clúster donde se supone que deben
ejecutarse los contenedores, el programador se asegura de que se hayan descargado
del registro de imágenes. A continuación, los contenedores se inician con toda la
configuración, como las redes a las que se conectan o los puertos en los que se exponen.
El orquestador hace todo lo posible para que la configuración del clúster coincida
exactamente con lo que se indica en nuestra declaración.

[ 163 ]
Orquestadores

Una vez que nuestro servicio está funcionando tal como se solicitó, es decir, se está
ejecutando en el estado deseado, el orquestador continúa supervisándolo. Cada vez que
el orquestador descubre una discrepancia entre el estado real del servicio y su estado
deseado, vuelve a hacer todo lo posible para llegar al estado deseado.

¿Qué discrepancia podría haber entre los estados reales y deseados de un servicio de aplicación?
Bueno, supongamos que una de las réplicas del servicio, es decir, uno de los
contenedores, se bloquea debido a, por ejemplo, un error; el orquestador descubrirá que
el estado real difiere del estado deseado en el número de réplicas: hay una réplica que
falta. El orquestador programará inmediatamente una nueva instancia en otro nodo
del clúster que sustituya a la instancia bloqueada. Otra discrepancia podría ser que
haya demasiadas instancias del servicio de aplicación ejecutándose, si se ha reducido
la capacidad del servicio. En este caso, el orquestador solo eliminará al azar tantas
instancias como sea necesario para lograr la paridad entre el número de instancias
real y el deseado. Otra discrepancia podría surgir cuando el orquestador descubre
que hay una instancia del servicio de aplicación que ejecuta una versión incorrecta
(tal vez  antigua) de la imagen del contenedor subyacente. Captas la idea, ¿no?

Por tanto, en lugar de supervisar activamente los servicios de nuestra aplicación que se
ejecutan en el clúster y corregir cualquier desviación del estado deseado, delegamos esta
tediosa tarea al orquestador. Esto funciona muy bien si usamos una forma declarativa
y no imperativa de describir el estado deseado de nuestros servicios de aplicación.

Servicios replicados y globales


Hay dos tipos de servicios muy diferentes que podríamos querer ejecutar en un clúster
administrado por un orquestador. Son los servicios replicados y globales. Un servicio
replicado es aquel que debe ejecutarse en un número específico de instancias, por
ejemplo,10. A su vez, un servicio global es un servicio que debe tener una instancia
ejecutándose en cada nodo de trabajo del clúster. He utilizado el término nodo de trabajo
aquí. En un clúster administrado por un orquestador, normalmente tenemos dos
tipos de nodos: administradores y trabajadores. Por lo general, el orquestador utiliza
exclusivamente un nodo de administración para administrar el clúster y no ejecuta
ninguna otra carga de trabajo. Los nodos de trabajo, a su vez, ejecutan aplicaciones reales.

Por lo tanto, el orquestador se asegura de que, para un servicio global, una instancia
del mismo se esté ejecutando en cada nodo de trabajo, independientemente de cuántos
nodos haya. No necesitamos preocuparnos por el número de instancias, solo de que
cada nodo se ejecute en una sola instancia del servicio.

[ 164 ]
Capítulo 9

Una vez más, podemos confiar plenamente en el orquestador para que se encargue
de esta tarea. En un servicio replicado, siempre tendremos la garantía de encontrar
el número exacto de instancias, mientras que para un servicio global, podemos estar
seguros de que en cada nodo de trabajo siempre se ejecutará una y solo una instancia del
servicio. El orquestador siempre hará todo lo posible para garantizar este estado deseado.

En Kubernetes, un servicio global también se denomina


conjunto de daemon.

Detección de servicios
Cuando describimos un servicio de aplicación de forma declarativa, en ningún caso
se supone que tengamos que indicarle al orquestador en qué nodos del clúster deben
ejecutarse las diferentes instancias del servicio. Dejamos que el orquestador decida qué
nodos son los más adecuados para esta tarea.

Por supuesto, es técnicamente posible indicar al orquestador que utilice reglas


de colocación muy deterministas, pero esto sería un antipatrón y no se recomienda
en absoluto.

Por lo tanto, si presuponemos que el motor de orquestación tiene total libertad en cuanto
a dónde colocar las distintas instancias del servicio de aplicación y que el orquestador
puede bloquear y volver a programar las instancias en nodos diferentes, queda claro que
es absurdo que nos ocupemos nosotros de controlar dónde se ejecutan las instancias en
cada momento. Aún mejor, ni siquiera deberíamos intentar conocer esta información,
ya que no es importante.

Vale, pero ¿qué ocurre si tenemos dos servicios, A y B, y el servicio A depende del
servicio B?; ¿no debería una instancia determinada del servicio A saber dónde puede encontrar
una instancia del servicio B?

Esto lo tengo que decir alto y claro: no, no debería. Este tipo de conocimiento no es
deseable en una aplicación altamente distribuida y escalable. Más bien, debemos confiar
en que el orquestador nos proporcione la información que necesitamos para acceder
a otras instancias del servicio de las que dependemos. Es algo similar a los viejos
tiempos de la telefonía, cuando no podíamos llamar directamente a nuestros amigos,
sino que teníamos que llamar a la oficina central de la compañía telefónica, donde algún
operador nos comunicaba con el destino correcto. En nuestro caso, el orquestador hace
el papel de operador, enrutando una solicitud procedente de una instancia del servicio
A a una instancia disponible del servicio B. Este proceso recibe el nombre de detección
de servicios.

[ 165 ]
Orquestadores

Enrutamiento
Hemos aprendido hasta ahora que en una aplicación distribuida tenemos muchos
servicios interactuando. Cuando el servicio A interactúa con el servicio B, esta interacción
se produce a través de intercambio de paquetes de datos. Estos paquetes de datos
necesitan canalizarse de alguna manera del servicio A al servicio B. Este proceso
de canalización de los paquetes de datos desde un origen a un destino también se
denomina enrutamiento. Como autores u operadores de una aplicación, esperamos que
el orquestador se encargue de esta tarea de enrutamiento. Como veremos en capítulos
posteriores, el enrutamiento puede ocurrir en diferentes niveles. Es como en la vida
real. Supón que estás trabajando en una gran empresa en uno de sus edificios. Tienes un
documento que debes reenviar a otro empleado de la compañía. El servicio de correos
interno recogerá el documento de tu buzón y lo llevará a la oficina de correos ubicada
en el mismo edificio. Si el destinatario trabaja en el mismo edificio, el documento puede
reenviarse directamente a esa persona. Si, por el contrario, la persona trabaja en otro
edificio del mismo bloque, el documento se reenviará a la oficina de correos de ese
edificio de destino, desde donde se distribuirá al receptor a través del servicio de correo
interno. En tercer lugar, si el documento se dirige a un empleado que trabaja en otra
sucursal de la empresa ubicada en una ciudad o incluso un país diferente, el documento
se reenvía a un servicio postal externo como UPS, que lo transportará a la ubicación
de destino, desde donde, una vez más, el servicio de correo interno se encargará
de entregarlo al destinatario.

Ocurren cosas similares cuando se enrutan paquetes de datos entre servicios


de aplicaciones que se ejecutan en contenedores. Los contenedores de origen y de
destino pueden estar emplazados en el mismo nodo del clúster, lo que se corresponde
a la situación en la que ambos empleados trabajan en el mismo edificio. El contenedor
de destino se puede ejecutar en un nodo de clúster diferente, lo que se corresponde
a la situación en la que los dos empleados trabajan en diferentes edificios del mismo
bloque. Por último, la tercera situación es cuando un paquete de datos procede de fuera
del clúster y debe enrutarse al contenedor de destino que se ejecuta dentro del clúster.

De todas estas situaciones y de algunas más se encarga el orquestador.

Equilibrio de carga
En una aplicación distribuida altamente disponible, todos los componentes deben
ser redundantes. Esto significa que cada servicio de aplicación debe ejecutarse
en varias instancias, de modo que si una instancia falla, el servicio en su conjunto
siga funcionando.

[ 166 ]
Capítulo 9

Para asegurarnos de que todas las instancias de un servicio están realmente haciendo
su trabajo y no están inactivas, tenemos que comprobar que las solicitudes del servicio
se distribuyen por igual entre todas las instancias. Este proceso de distribución de
la carga de trabajo entre instancias se denomina equilibrio de carga. Existen varios
algoritmos relacionados con cómo se distribuye la carga de trabajo. Normalmente,
un equilibrador de carga funciona con lo que llamamos "algoritmo robin", que se asegura
de que la carga de trabajo se distribuya equitativamente entre las instancias usando
un algoritmo cíclico.

Una vez más, esperamos que el orquestador se encargue de equilibrar la carga de las
solicitudes de un servicio a otro o de orígenes externos a servicios internos.

Escalado
Cuando ejecutamos nuestra aplicación distribuida en contenedores en un clúster
administrado por un orquestador, también queremos disponer de una manera sencilla
de controlar los incrementos esperados o inesperados en la carga de trabajo. Para manejar
una mayor carga de trabajo, normalmente basta con programar instancias adicionales
de un servicio que está experimentando un incremento de la carga. Los equilibradores
de carga se configurarán automáticamente para distribuir la carga de trabajo entre
las instancias de destino que estén más disponibles.

Pero en los escenarios de la vida real, la carga de trabajo varía con el tiempo. Pensemos,
por ejemplo, en un sitio de compras como Amazon. Este sitio podría tener una carga alta
por la tarde durante las horas pico, cuando todo el mundo está en casa y comprando
online; podría tener cargas extremas durante días especiales como el Black Friday;
y podría tener muy poco tráfico a primeras horas de la mañana. Por lo tanto, los
servicios no solo deben poder escalarse en dirección ascendente, sino también en
dirección descendente cuando se reduzca la carga.

También esperamos que los orquestadores distribuyan las instancias de un servicio


de forma inteligente durante el escalado ascendente o descendente. No sería prudente
programar todas las instancias del servicio en el mismo nodo del clúster, ya que
si ese nodo deja de funcionar, todo el servicio se vendrá abajo. El programador del
orquestador, que es responsable de la colocación de los contenedores, necesita considerar
también que no debe colocar todas las instancias en el mismo rack de ordenadores,
ya que si la fuente de alimentación del rack falla, de nuevo todo el servicio se verá
afectado. Asimismo, las instancias de los servicios críticos deberían distribuirse incluso
entre distintos centros de datos para evitar interrupciones. Todas estas decisiones
y muchas más son responsabilidad del orquestador.

[ 167 ]
Orquestadores

Reparación automática
En estos días, los orquestadores son muy sofisticados y pueden hacer mucho por
nosotros para mantener un sistema en buen estado. Los orquestadores supervisan todos
los contenedores que se ejecutan en el clúster y reemplazan automáticamente los que
se bloquean o no responden con nuevas instancias. Los orquestadores supervisan el
estado de los nodos del clúster y los sacan del bucle del programador si dejan de estar
en buen estado o de funcionar. Una carga de trabajo que esté emplazada en esos nodos
se reprograma automáticamente en diferentes nodos disponibles.

Todas estas actividades en las que el orquestador supervisa el estado actual y repara
automáticamente el daño o alcanza el estado deseado conforman lo que se denomina
un sistema de reparación automática. En la mayoría de los casos, nosotros no tenemos
que participar activamente y reparar los daños. El orquestador lo hará por nosotros
automáticamente.

Pero hay algunas situaciones en las que el orquestador no puede encargarse de esto
sin nuestra ayuda. Imagina una situación en la que tengamos una instancia de servicio
ejecutándose en un contenedor. El contenedor está funcionando y, desde fuera, parece
que está en perfecto estado. Pero la aplicación por dentro no tiene un estado correcto.
La aplicación no se ha bloqueado; simplemente no funciona según lo previsto. ¿Cómo
podría el orquestador saber esto sin que nosotros le demos una pista? ¡No puede! Estar en un
estado incorrecto o no válido significa algo completamente diferente para cada servicio
de aplicación. En otras palabras, el estado de salud depende del servicio. Solo los autores
del servicio o sus operadores saben lo que significa "estar en buen estado" en el contexto
de un servicio.

Los orquestadores definen sondas, a través de las cuales un servicio de la aplicación


puede comunicar al orquestador en qué estado se encuentra. Existen dos tipos
fundamentales de sondas:

• El servicio puede indicar al orquestador que está en buen estado o no


• El servicio puede indicar al orquestador que está listo o temporalmente fuera
de servicio

La forma en que el servicio determina cualquiera de las respuestas anteriores depende


totalmente del servicio. El orquestador solo define cómo va a preguntar (por ejemplo,
a través de una solicitud HTTP GET) o qué tipo de respuestas espera (por ejemplo,
OK o NOT OK).

Si nuestros servicios implementan lógica para responder a las preguntas anteriores sobre
el estado o la disponibilidad, entonces tenemos un verdadero sistema de reparación
automática, ya que el orquestador puede eliminar las instancias de servicio que no
están en buen estado y reemplazarlas por otras saludables, y puede sacar las instancias
de servicio que no están disponibles temporalmente fuera del circuito "round robin"
del equilibrador de carga.

[ 168 ]
Capítulo 9

Implementaciones sin tiempo de inactividad


En estos días, cada vez es más difícil justificar un tiempo de inactividad absoluto para
una aplicación crítica que necesite actualizarse. Eso no solo implica oportunidades
perdidas, sino que también puede dañar la reputación de la empresa. Los clientes
que utilizan la aplicación simplemente no aceptarán este inconveniente y dejarán
rápidamente de usar la aplicación. Además, nuestros ciclos de lanzamiento cada vez
son más cortos. Mientras que en el pasado teníamos uno o dos nuevos lanzamientos por
año, hoy en día, muchas empresas actualizan sus aplicaciones varias veces a la semana
o incluso varias veces al día.

La solución a ese problema es adoptar una estrategia de actualización de aplicaciones


con un tiempo de inactividad igual a cero. El orquestador tiene que poder actualizar
los distintos servicios de la aplicación por lotes. Esto también recibe el nombre de
actualizaciones graduales. En un momento dado, solo una o varias del total de instancias
de un determinado servicio se retiran y se reemplazan por la nueva versión del servicio.
Solo si las nuevas instancias están funcionando y no producen errores inesperados ni
muestran un comportamiento erróneo, se actualizará el siguiente lote de instancias. Esto
se repite hasta que todas las instancias se sustituyen por su nueva versión. Si, por alguna
razón, la actualización falla, esperamos que el orquestador revierta automáticamente
las instancias actualizadas a su versión anterior.

Otras posibles implementaciones con cero tiempo de inactividad son las llamadas
versiones "canary" y versiones "blue-green". En ambos casos, la nueva versión de
un servicio se instala en paralelo con la versión activa actual. Pero al principio, la nueva
versión solo es accesible internamente. Las operaciones pueden entonces ejecutar pruebas
de humo contra la nueva versión, y cuando la nueva versión parezca estar funcionando
correctamente, entonces, en el caso de la implementación "blue-green", el router cambia
la versión "blue" actual a la nueva versión "green". Durante algún tiempo, la nueva
versión "green" del servicio se vigila de cerca y, si todo va bien, se da de baja la antigua
versión "blue". Si, por el contrario, la nueva versión "green" no funciona según lo
previsto, solo hay que configurar el router de nuevo a la versión blue para lograr
una reversión completa.

En el caso de una versión "canary", el router se configura de forma que canaliza


un pequeño porcentaje, digamos el 1 %, del tráfico global a través de la nueva versión
del servicio, mientras que el 99 % del tráfico se sigue enrutando a través de la versión
antigua. El comportamiento de la nueva versión se vigila de cerca y se compara con el
comportamiento de la versión antigua. Si todo va bien, entonces el porcentaje del tráfico
canalizado a través del nuevo servicio se aumenta ligeramente. Este proceso se repite
hasta que el 100 % del tráfico se enruta a través del nuevo servicio. Si el nuevo servicio se
ha ejecutado durante un tiempo y todo va bien, entonces el servicio antiguo se da de baja.

La mayoría de los orquestadores admiten de serie al menos el tipo de actualizaciones


graduales de la implementación con cero tiempo de inactividad. Las versiones
"blue-green" y "canary" suelen ser mucho más fáciles de implementar.

[ 169 ]
Orquestadores

Afinidad y reconocimiento de ubicación


Algunas veces, determinados servicios de aplicación requieren la disponibilidad
de hardware dedicado en los nodos en los que se ejecutan. Por ejemplo, los servicios
relacionados con operaciones de E/S requieren nodos del clúster con una unidad de estado
sólido (SSD) de alto rendimiento conectada, o algunos servicios requieren una unidad de
procesamiento acelerado (APU). Los orquestadores nos permiten definir las afinidades
de nodo por servicio de aplicación. El orquestador se asegurará de que su programador
solo programe los contenedores en nodos del clúster que cumplan los criterios exigidos.

Debe evitarse definir una afinidad con un nodo determinado, ya que esto introduciría
un único punto de error y, por tanto, pondría en peligro la alta disponibilidad. Siempre
se debe definir un conjunto de varios nodos del clúster como destino de un servicio
de aplicación.

Algunos motores de orquestación admiten también lo que se denomina reconocimiento


de ubicación o reconocimiento geográfico. Lo que esto significa es que se puede
solicitar al orquestador que distribuya equitativamente las instancias de un servicio
entre un conjunto de ubicaciones diferentes. Podríamos, por ejemplo, definir un centro
de datos con los valores posibles oeste, centro y norte, y aplicar la etiqueta a todos
los nodos del clúster con el valor correspondiente a la región geográfica en la que se
encuentra el nodo respectivo. A continuación, podríamos indicar al orquestador que
utilice esta etiqueta para el reconocimiento geográfico de un determinado servicio
de aplicación. En este caso, si se solicitan nueve réplicas del servicio, el orquestador
se asegurará de que se implementen tres instancias en los nodos en cada uno de los
tres centros de datos: oeste, centro y este.

El reconocimiento geográfico puede definirse incluso jerárquicamente; por ejemplo,


podríamos tener un centro de datos como el discriminador de nivel superior, seguido
de la zona de disponibilidad y seguido del rack de servidores.

El reconocimiento geográfico o reconocimiento de ubicación se utiliza para reducir


la probabilidad de que se produzca una interrupción debido a una avería en la fuente
de alimentación o un apagón del centro de datos. Si las instancias de la aplicación
se distribuyen en racks de servidores, zonas de disponibilidad o incluso centros
de datos, es muy improbable que todo deje de funcionar a la vez. Una región siempre
estará disponible.

Seguridad
En estos días, la seguridad de TI es un tema muy candente. La guerra cibernética
está en pleno apogeo. La mayoría de las empresas conocidas han sido víctimas
de ataques de hackers, con consecuencias muy costosas. Una de las peores pesadillas
de cualquier director de informática (CIO) o director de tecnología (CTO) es levantarse
por la mañana y oír en las noticias que su empresa ha sido víctima de un ataque
informático y que se ha robado o atacado información confidencial.

[ 170 ]
Capítulo 9

Para contrarrestar la mayor parte de estas amenazas de seguridad, necesitamos establecer


una cadena de suministro de software seguro y hacer cumplir la estrategia de defensa
en profundidad. Echemos un vistazo a algunas de las tareas que podemos esperar
de un orquestador de clase empresarial.

Comunicación segura e identidad de nodo


criptográfica
En primer lugar, queremos asegurarnos de que nuestro clúster administrado por el
orquestador sea seguro. Solo los nodos de confianza pueden unirse al clúster. Cada nodo
que se une al clúster obtiene una identidad de nodo criptográfica y se debe cifrar toda la
comunicación entre los nodos. Para ello, los nodos pueden utilizar la seguridad mutua
de la capa de transporte (MTLS). Para autenticar los nodos del clúster entre sí, se utilizan
certificados. Estos certificados se rotan automáticamente de forma periódica o a petición
para proteger el sistema en caso de que se filtre un certificado.

La comunicación que se produce en un clúster se puede dividir en tres tipos. Aquí es


donde entran en juego los planos de comunicación. Existen planos de administración, de
control y de datos:

• El plano de administración lo utilizan los administradores o responsables


del clúster para, por ejemplo, programar instancias del servicio, ejecutar
comprobaciones de estado o crear y modificar cualquier otro recurso del clúster,
como volúmenes de datos, secretos o redes.
• El plano de control se utiliza para intercambiar información de estado importante
entre todos los nodos del clúster. Este tipo de información se utiliza, por ejemplo,
para actualizar las tablas de direcciones IP locales de los clústeres que se emplean
con fines de enrutamiento.
• El plano de datos es donde los servicios de aplicación se comunican entre sí e
intercambian datos.

Normalmente, los orquestadores se preocupan principalmente de proteger el plano de


administración y de control. La protección del plano de datos se deja al usuario, pero el
orquestador puede facilitar esta tarea.

Redes seguras y políticas de red


Cuando se ejecutan servicios de aplicación, no todos los servicios necesitan comunicarse
con todos los demás servicios del clúster. Por lo tanto, queremos poder aislar los servicios
unos de otros y ejecutar solo aquellos que están en la misma zona aislada de la red que
necesitan sí o sí comunicarse entre sí. Todos los demás servicios y todo el tráfico de red
que viene de fuera del clúster no deben tener la posibilidad de acceder a los servicios que
se encuentran aislados.

[ 171 ]
Orquestadores

Hay al menos dos formas en las que puede tener lugar este aislamiento basado
en red. Podemos utilizar una red defina por software (SDN) para agrupar servicios
de aplicación o podemos tener una red plana y usar políticas de red para controlar
quién tiene y no tiene acceso a un determinado servicio o grupo de servicios.

Control de acceso basado en roles (RBAC)


Una de las tareas más importantes, junto con la seguridad, que un orquestador debe
poder realizar para que sea apto para la empresa es proporcionar acceso basado en roles
al clúster y sus recursos. RBAC define cómo los sujetos, usuarios o grupos de usuarios
del sistema, organizados en equipos, etcétera, pueden acceder al sistema y manipularlo.
Se asegura de que el personal no autorizado no pueda hacer ningún daño al sistema ni
ver los recursos disponibles en el sistema que se supone que no deben conocer o ver.

Una empresa típica podría tener grupos de usuarios como Desarrollo,


Control de calidad y Producción, y cada uno de esos grupos podría
tener uno o muchos usuarios asociados. Juan Pérez, el desarrollador,
es miembro del grupo de desarrollo y, como tal, puede acceder a los
recursos dedicados al equipo de desarrollo, pero no puede acceder,
por ejemplo, a los recursos del equipo Producción, del que es miembro
Ana Barroso. Ella, a su vez, no puede interferir con los recursos del
equipo Desarrollo.

Una forma de implementar RBAC es mediante la definición de concesiones. Una


concesión es una asociación entre un sujeto, un rol y una colección de recursos. Aquí,
un rol se compone de un conjunto de permisos de acceso a un recurso. Estos permisos
pueden ser crear, detener, quitar, mostrar o ver contenedores; implementar un nuevo
servicio de aplicación; mostrar los nodos del clúster o ver los detalles de un nodo
de clúster, entre otros muchos.

Una colección de recursos es un grupo de recursos relacionados lógicamente con el


clúster, como servicios de aplicaciones, secretos, volúmenes de datos o contenedores.

Secretos
En nuestra vida diaria, tenemos montones de secretos. Los secretos son información
que no está destinada a que se conozca públicamente, como la combinación
de nombre de usuario y contraseña que utilizamos para acceder a la cuenta bancaria
online, el código del teléfono móvil o la clave de la taquilla de un gimnasio.

[ 172 ]
Capítulo 9

Cuando escribimos software, a menudo también necesitamos usar secretos. Por ejemplo,


necesitamos algún certificado para autenticar nuestro servicio de aplicación con
algún servicio externo al que queramos acceder o necesitamos un token para
autenticar y autorizar nuestro servicio cuando accede a alguna otra API. En el pasado,
los desarrolladores, por comodidad, simplemente incluían estos valores en el código
o los ponían en texto visible en algunos archivos de configuración externos. Allí, esta
información altamente confidencial estaba accesible a un amplio público que en realidad
nunca debería haber tenido la oportunidad de ver esos secretos.

Afortunadamente, en estos días, los orquestadores ofrecen lo que llamamos "secretos"


para tratar con esta información confidencial de una manera muy segura. Los secretos
los crean personal autorizado o de confianza. Los valores de esos secretos se cifran
y almacenan en la base de datos de estado del clúster altamente disponible. Los secretos,
como están cifrados, ahora están protegidos en reposo. Cuando un servicio de aplicación
autorizado solicita un secreto, el secreto solo se reenvía a los nodos del clúster que
realmente ejecutan una instancia de ese servicio en particular, y el valor del secreto no
se almacena nunca en el nodo, sino que se monta en el contenedor en un volumen tmpfs
basado en RAM. Solo dentro del contenedor correspondiente está el valor del secreto
disponible en texto visible.

Ya hemos mencionado que los secretos están protegidos en reposo. Una vez solicitado
por un servicio, el administrador o responsable del clúster descifra el secreto y lo envía
por cable a los nodos de destino. Entonces, ¿están protegidos los secretos cuando están
en tránsito? Bueno, hemos aprendido antes que los nodos del clúster utilizan MTLS para
su comunicación; por lo tanto, el secreto, aunque se transmita en texto visible, sigue
estando protegido, ya que MTLS cifrará los paquetes de datos. Por tanto, los secretos
están protegidos en reposo y en tránsito. Solo los servicios que están autorizados a usar
secretos tendrán acceso a esos valores de secretos.

Confianza en el contenido
Para mayor seguridad, queremos asegurarnos de que solo las imágenes de confianza
se ejecuten en nuestro clúster de producción. Algunos orquestadores nos permiten
configurar un clúster de forma que solo pueda ejecutar imágenes firmadas. Con la
confianza en el contenido y la firma de imágenes se pretende garantizar que los autores
de la imagen sean los que esperamos que sean, es decir, nuestros desarrolladores de
confianza o, aún mejor, nuestro servidor de CI de confianza. Además, con la confianza
en el contenido, queremos garantizar que la imagen que obtenemos sea nueva y no una
imagen antigua y posiblemente vulnerable. Y por último, queremos asegurarnos de que
la imagen no pueda ser atacada por hackers malintencionados mientras está en tránsito.
Esto último recibe el nombre a menudo de ataque man-in-the-middle (MITM).

Al firmar imágenes en el origen y validar la firma en el destino, podemos garantizar


que las imágenes que queremos ejecutar no hayan sido atacadas.

[ 173 ]
Orquestadores

Tiempo de actividad inverso


El último punto que quiero explicar en el contexto de la seguridad es el tiempo de actividad
inverso. ¿Qué quiero decir con esto? Imagina que has configurado y protegido un clúster
de producción. En este clúster, estás ejecutando algunas aplicaciones críticas de tu empresa.
Ahora, un hacker ha logrado encontrar un agujero de seguridad en una de tus pilas de
software y ha obtenido acceso "root" a uno de tus nodos del clúster. Eso, que ya es bastante
malo por sí solo, podría ser peor si este hacker pudiera enmascarar su presencia en este nodo,
que es un nodo raíz de la máquina, y usarlo como base para atacar otros nodos del clúster.

El acceso "root" en Linux o cualquier sistema operativo de tipo


Unix significa que cualquiera puede hacer lo que quiera en este
sistema. Es el nivel más alto de acceso que alguien puede tener.
En Windows, el rol equivalente es el de un administrador.

Pero ¿y si aprovechamos el hecho de que los contenedores son efímeros y los nodos de clúster
se aprovisionan rápidamente, generalmente en cuestión de minutos si están completamente
automatizados? Acabamos de matar todos los nodos del clúster después de un
determinado tiempo de actividad, por ejemplo, 1 día. El orquestador tiene la instrucción
de vaciar el nodo y excluirlo del clúster. Una vez que el nodo está fuera del clúster,
se retira y se reemplaza por un nodo recién aprovisionado.

De esa manera, el hacker ha perdido su base de operaciones y se ha eliminado


el problema. Aunque este concepto aún no está ampliamente disponible, creo
que es un gran paso hacia una mayor seguridad y, por lo que me han comentado
los ingenieros que trabajan en esta área, no es difícil de implementar.

Introspección
Hasta ahora, hemos analizado muchas tareas de las que el orquestador es responsable
y que pueden ejecutarse de forma totalmente autónoma. Pero también existe la necesidad
de que los operadores humanos puedan ver y analizar lo que actualmente se está
ejecutando en el clúster y en qué estado se encuentran las distintas aplicaciones. Para
todo esto, necesitamos la posibilidad de introspección. El orquestador necesita presentar
información crucial de una manera que sea fácilmente consumible y comprensible.
El orquestador debe recopilar métricas del sistema de todos los nodos del clúster
y ponerla a disposición de los operadores. Las métricas incluyen uso de CPU, memoria
y disco, consumo de ancho de banda de red, etc. La información debe estar disponible
fácilmente para cada nodo, además de en forma de resumen.
También queremos que el orquestador nos dé acceso a los registros producidos por las
instancias del servicio o los contenedores. Aún más, el orquestador debe proporcionarnos
acceso de ejecución a todos y cada uno de los contenedores si tenemos la autorización
correcta para hacerlo. Con el acceso de ejecución a los contenedores, podemos depurar
los contenedores que no funcionan correctamente.
[ 174 ]
Capítulo 9

En las aplicaciones altamente distribuidas, donde cada solicitud que se realiza


a la aplicación pasa por numerosos servicios hasta que se tramita completamente,
las solicitudes de rastreo son tareas realmente importantes. Lo ideal es que el orquestador
nos ayude con la implementación de una estrategia de rastreo o nos ofrezca algunas
buenas directrices que podamos seguir.
Por último, los operadores humanos pueden supervisar mejor un sistema cuando
trabajan con una representación gráfica de todas las métricas recopiladas y la
información de registro y rastreo. Aquí, estamos hablando de paneles. Cualquier
orquestador decente debería ofrecer al menos un panel básico con una representación
gráfica de los parámetros del sistema más críticos.
Pero los operadores humanos no están solo preocupados por la introspección. También
necesitamos poder conectar sistemas externos con el orquestador para consumir esta
información. Tiene que haber una API disponible, a partir de la cual los sistemas externos
puedan acceder a datos como el estado del clúster, las métricas y los registros, y utilizar
esta información para tomar decisiones automatizadas, como la creación de alertas
de buscapersonas o de teléfono, el envío de mensajes de correo electrónico o la activación
de una sirena de alarma si el sistema sobrepasa algunos umbrales.

Información general de orquestadores


populares
En el momento de escribir este documento, hay muchos motores de orquestación en
el mercado y en uso. Pero hay algunos claros ganadores. El puesto número uno lo
ocupa indiscutiblemente Kubernetes, que es el rey supremo. Le sigue de lejos el propio
SwarmKit de Docker, seguido de otros como Microsoft Azure Kubernetes Service
(AKS), Apache Mesos o AWS Elastic Container Service (ECS).

Kubernetes
Kubernetes fue diseñado originalmente por Google y posteriormente donado a la
Fundación de Computación Nativa en el Cloud (CNCF, por sus siglas en inglés).
Kubernetes se diseñó como el sistema Borg patentado de Google, que ha estado
ejecutando contenedores a escala supermasiva durante años. Kubernetes fue el intento
de Google de volver a la mesa de dibujo, y empezar totalmente desde el principio
y diseñar un sistema que incorporase todas las lecciones aprendidas con Borg.
A diferencia de Borg, que es una tecnología patentada, Kubernetes fue de código abierto
desde el principio. Esta fue una decisión muy acertada de Google, ya que atrajo a un
gran número de colaboradores de fuera de la empresa y, durante solo un par de años,
se desarrolló un ecosistema aún más masivo en torno a Kubernetes. Se puede decir sin
lugar a dudas que Kubernetes es el espacio de orquestación de contenedores favorito
de la comunidad. Ningún otro orquestador ha sido capaz de producir tanto revuelo ni
de atraer a tantas personas con talento dispuestas a contribuir al éxito del proyecto como
un colaborador o un usuario precoz.
[ 175 ]
Orquestadores

En ese sentido, Kubernetes, en el espacio de orquestación de contenedores es similar,


creo yo, a lo que Linux es en el espacio de sistemas operativos de servidor. Linux
se ha convertido en el estándar de facto de los sistemas operativos de servidor. Todas
las empresas importantes, como Microsoft, IBM, Amazon, RedHat, e incluso Docker,
han adoptado Kubernetes.

Y hay una cosa innegable: Kubernetes se ha diseñado desde el principio para una
escalabilidad masiva. Después de todo, fue diseñado con Google Borg en mente.

Un aspecto negativo que se puede objetar en contra de Kubernetes es que es difícil


de configurar y administrar, al menos en el momento de redactar este artículo. Hay un
obstáculo importante que superar para los recién llegados. El primer paso es empinado.
Pero una vez que trabajas con este orquestador durante un tiempo, todo cobra sentido.
El diseño general se ha pensado cuidadosamente y se ha ejecutado a la perfección.

En la versión más reciente de Kubernetes, 1.10, cuya disponibilidad con carácter general
(GA) tuvo lugar en marzo de 2018, la mayoría de los defectos iniciales, en comparación
con otros orquestadores como Docker Swarm, se han eliminado. Por ejemplo, la seguridad
y la confidencialidad no son solo ideas de última hora, sino una parte integral del sistema.

Las nuevas características se implementan a una velocidad increíble. Los nuevos


lanzamientos se producen cada tres meses más o menos o, para ser más precisos,
cada 100 días. La mayoría de las nuevas funcionalidades se implementan en función
de la demanda, es decir, las empresas que utilizan Kubernetes para orquestar sus
aplicaciones críticas pueden expresar sus necesidades. Esto hace que Kubernetes esté
listo para la empresa. Sería erróneo pensar que este orquestador es solo para "start-ups" y
no para empresas que rehúyen el riesgo. Es todo lo contrario. ¿En qué me baso para afirmar
esto? Bueno, mi afirmación se basa en el hecho de que empresas como Microsoft, Docker
y RedHat, cuyos clientes son en su mayoría grandes empresas, han adoptado en su
totalidad Kubernetes y proporcionan soporte de nivel empresarial para este orquestador
cuando se utiliza y se integra en las soluciones de la empresa.

Kubernetes admite contenedores Linux y Windows.

Docker Swarm
Es bien sabido que Docker ha popularizado y ha masificado el uso de los contenedores
de software. Docker no inventó los contenedores, pero los estandardizó y los puso
a disposición de un público más amplio, ofreciendo el registro de imágenes gratuito
Docker Hub. Inicialmente, Docker se centró principalmente en el desarrollador
y en el ciclo de vida de desarrollo. Pero las empresas que empezaron a utilizar con
gran satisfacción los contenedores muy pronto quisieron usarlos no solo durante
el desarrollo o las pruebas de nuevas aplicaciones, sino también para ejecutar las
aplicaciones en producción.

[ 176 ]
Capítulo 9

Inicialmente, Docker no tenía nada que ofrecer en ese espacio, así que otras compañías
ocuparon ese vacío y ofrecieron ayuda a los usuarios. Pero no pasó mucho tiempo hasta
que Docker reconoció que había una enorme demanda de un orquestador sencillo pero
eficaz. El primer intento de Docker fue un producto llamado Swarm clásico. Se trataba de
un producto independiente que permitía a los usuarios crear un clúster de máquinas host
de Docker que podían utilizarse para ejecutar y escalar sus aplicaciones en contenedor
de una manera altamente disponible y autorrecuperable.

La configuración de un Docker Swarm clásico era, sin embargo, difícil. Se necesitaban


muchos pasos manuales complejos. Los clientes adoraban el producto, pero tenían que
pelear con su complejidad. Así que Docker decidió que podría hacerlo mejor. Volvió
a la mesa de dibujo y salió con SwarmKit. SwarmKit se presentó en DockerCon 2016 en
Seattle como una parte integral de la versión más reciente del motor de Docker. Sí, tienes
razón, SwarmKit era y sigue siendo a día de hoy una parte integral del motor de Docker.
Por lo tanto, si instalas un host de Docker, automáticamente tendrás SwarmKit.

SwarmKit fue diseñado teniendo en cuenta la simplicidad y la seguridad. El mantra era


y sigue siendo que tenía que ser casi trivial configurar una instancia del producto, y esa
instancia tenía que ser muy segura de serie. Docker Swarm funciona con el supuesto
de privilegios mínimos.

La instalación de un Docker Swarm altamente disponible es literalmente tan simple


como ejecutar un comando docker swarm init en el primer nodo del clúster, que
se convertirá en lo que viene llamándose el "líder" y después un docker swarm join
<join-token> en todos los demás nodos. El join-token lo genera el líder durante
la inicialización. Todo el proceso tarda menos de cinco minutos en un Swarm con hasta
10 nodos. Si está automatizado, tarda aún menos tiempo.

Como ya he mencionado, la seguridad ocupaba el primer puesto en la lista de


prioridades cuando se diseñó Docker y se desarrolló SwarmKit. Los contenedores
proporcionan seguridad al depender de los espacios de nombres del kernel de Linux
y cgroups, así como de las listas blancas syscall de Linux y de la compatibilidad con
las funciones de Linux y con el módulo de seguridad de Linux (LSM). Ahora, además
de eso, SwarmKit incorpora MTLS y secretos que están cifrados en reposo y en tránsito.
Además, Swarm define lo que se denomina modelo de red de contenedores (CNM),
que permite a los SDN proporcionar un espacio aislado para los servicios de aplicación
que se ejecutan en la instancia de Swarm.

Docker SwarmKit admite contenedores Linux y Windows.

[ 177 ]
Orquestadores

Microsoft Azure Kubernetes Service (AKS)


AKS es la solución de Microsoft de un clúster Kubernetes totalmente alojado, altamente
disponible, escalable y tolerante a errores. Se ocupa de la tarea de aprovisionar
y administrar un clúster de Kubernetes y te permite centrarte en la implementación
y ejecución de tus aplicaciones en contenedores. Con AKS, un usuario puede,
literalmente, aprovisionar un clúster listo para producción en cuestión de minutos.
Además, las aplicaciones que se ejecutan en un clúster de este tipo pueden entrar
fácilmente en el amplio ecosistema de servicios ofrecidos por Azure, como Log
Analytics o la administración de identidades.

Es un eficaz servicio de orquestación basado en las versiones más recientes de Kubernetes


que tiene sentido si ya has hecho una gran inversión en el ecosistema de Azure.
Cada clúster de AKS se puede administrar a través del portal de Azure, mediante las
plantillas de Azure Resource Manager o mediante la CLI de Azure. Las aplicaciones
se implementan y mantienen con la conocida CLI de Kubernetes, kubectl.

Microsoft, en sus propias palabras, afirma lo siguiente:

AKS permite implementar y administrar más rápido y fácilmente aplicaciones en


contenedor sin tener conocimientos de orquestación de contenedores. También elimina
la carga de las operaciones y el mantenimiento continuo mediante el aprovisionamiento,
la actualización y el escalado de los recursos bajo demanda, sin tener que trabajar con
tus aplicaciones sin conexión.

Apache Mesos y Marathon


Apache Mesos es un proyecto de código abierto y fue diseñado originalmente para
hacer que un clúster de servidores o nodos pareciera un único gran servidor desde fuera.
Mesos es un software que simplifica la gestión de los clústeres informáticos. Los usuarios
de Mesos no deberían tener que preocuparse por los servidores individuales, sino
simplemente trabajar con el supuesto de que tienen un gigantesco grupo de recursos
a su disposición, que se corresponde con la suma de todos los recursos de todos los
nodos del clúster.

Mesos, en términos informáticos, ya es bastante antiguo, al menos en comparación


con los otros orquestadores. Se presentó públicamente en 2009, pero, en ese momento,
no estaba diseñado para ejecutar contenedores, ya que Docker ni siquiera existía
todavía. Similar a lo que hace Docker con los contenedores, Mesos utiliza cgroups
de Linux para aislar recursos como CPU, memoria o E/S de disco para aplicaciones
o servicios individuales.

Mesos es en realidad la infraestructura subyacente de otros servicios interesantes


instalados encima. Desde la perspectiva de los contenedores específicamente,
Marathon es importante. Marathon es un orquestador de contenedores que se ejecuta
encima de Mesos y que se puede escalar a miles de nodos.

[ 178 ]
Capítulo 9

Marathon admite varios runtimes de contenedor, como Docker o sus propios


contenedores Mesos. Admite no solo servicios de aplicación sin estado, sino también
con estado, como las bases de datos PostgreSQL o MongoDB. Al igual que Kubernetes
y Docker SwarmKit, admite muchas de las características descritas anteriormente en este
capítulo, como la alta disponibilidad, comprobaciones de estado, detección de servicios,
equilibrio de carga y reconocimiento de ubicación, por nombrar solo algunas de las
más importantes.

Aunque Mesos y, hasta cierto punto, Marathon son proyectos bastante maduros,
su alcance es relativamente limitado. Parece ser el más popular en el área del big data,
es decir, para ejecutar servicios de procesamiento masivo de datos como Spark o Hadoop.

Amazon ECS
Si buscas un orquestador sencillo y ya estás metido a fondo en el ecosistema de AWS,
entonces Amazon ECS podría ser la opción adecuada para ti. Es importante señalar
una limitación muy importante de ECS: si optas por este orquestador de contenedores,
quedarás atrapado en AWS. No podrás migrar fácilmente una aplicación que se ejecute
en ECS a otra plataforma o cloud.

Amazon promociona su servicio ECS como un servicio de administración de


contenedores rápido y altamente escalable que simplifica la ejecución, detención
y administración de contenedores Docker en un clúster. Además de la ejecución
de contenedores, ECS ofrece acceso directo a muchos otros servicios de AWS desde los
servicios de aplicaciones que se ejecutan dentro de los contenedores. Esta integración
estrecha y sin fisuras con muchos de los servicios populares de AWS es lo que hace que
ECS sea una opción atractiva para los usuarios que buscan una manera fácil de conseguir
que sus aplicaciones en contenedor funcionen en un entorno robusto y altamente
escalable. Amazon también ofrece su propio registro de imágenes privado.

Con AWS ECS, puedes utilizar Fargate para que administre completamente
la infraestructura subyacente, de manera que puedas centrarte exclusivamente en la
implementación de aplicaciones en contenedor y no tengas que preocuparte de cómo
crear y administrar un clúster de nodos. ECS admite contenedores Linux y Windows.

En resumen, ECS es fácil de usar, altamente escalable y está bien integrado con otros
servicios populares de AWS, pero no es tan potente como, por ejemplo, Kubernetes
o Docker SwarmKit, y solo está disponible en Amazon AWS.

[ 179 ]
Orquestadores

Resumen
En este capítulo hemos explicado en primer lugar por qué son necesarios los
orquestadores y cómo funcionan en la teoría. Se ha señalado qué orquestadores son
los más prominentes en el momento de escribir este documento y se han explicado
las principales similitudes y diferencias entre los distintos orquestadores.

En el siguiente capítulo se ofrecerá una introducción del contenedor más popular


en la actualidad, Kubernetes. Hablaremos de todos los conceptos y objetos que
Kubernetes utiliza para implementar y ejecutar una aplicación distribuida, resistente,
robusta y altamente disponible en un clúster on-premises o en el cloud.

Preguntas
Para evaluar el progreso de tu aprendizaje, responde a las siguientes preguntas:

1. ¿Por qué necesitamos un orquestador? Indica dos o tres razones.


2. Menciona tres o cuatro responsabilidades típicas de un orquestador.
3. Nombra al menos dos orquestadores de contenedores, además de la empresa
que los patrocina.

Lectura adicional
Los siguientes enlaces proporcionan información más detallada sobre temas relacionados
con la orquestación (pueden estar en inglés):

• Kubernetes: orquestación lista para producción en https://kubernetes.io/


• Información general sobre Docker Swarm Mode en https://docs.docker.com/
engine/swarm/
• Mesosphere: servicios de orquestación de contenedores en http://bit.ly/2GMpko3
• Explicación de los contenedores y la orquestación en http://bit.ly/2DFoQgx
• Azure Kubernetes Service (AKS) en https://bit.ly/2MECYzY

[ 180 ]
Orquestación de aplicaciones
en contenedores con
Kubernetes
En el último capítulo, explicamos los orquestadores. Al igual que un director de
orquesta, un orquestador se asegura de que todos nuestros servicios de aplicaciones
en contenedor funcionen perfectamente y contribuyan armoniosamente a un objetivo
común. Estos orquestadores tienen una cuantas responsabilidades, que hemos explicado
detalladamente. También proporcionamos un breve resumen de los orquestadores de
contenedores más importantes del mercado.
En este capítulo, vamos a hablar de Kubernetes. Kubernetes es actualmente el líder
indiscutible en el espacio de la orquestación de contenedores. Empezaremos con
una visión general de la arquitectura de un clúster de Kubernetes y, a continuación,
analizaremos los objetos principales utilizados en Kubernetes para definir y ejecutar
aplicaciones en contenedores.
Los temas tratados en este capítulo son los siguientes:

• Arquitectura
• Nodos maestros de Kubernetes
• Nodos del clúster
• Introducción a MiniKube
• Compatibilidad de Kubernetes en Docker para Mac y Docker para Windows
• Pods
• Conjunto de réplicas de Kubernetes
• Implementación de Kubernetes
• Servicio de Kubernetes
• Enrutamiento basado en contexto
[ 181 ]
Orquestación de aplicaciones en contenedores con Kubernetes

Cuando termines de leer este capítulo, serás capaz de hacer lo siguiente:

• Realizar un dibujo de la arquitectura general de un clúster de Kubernetes en una


servilleta
• Explicar tres o cuatro características principales de un pod de Kubernetes
• Describir el papel que desempeñan los conjuntos de réplicas de Kubernetes
en dos o tres frases cortas
• Explicar las dos o tres responsabilidades principales de un servicio Kubernetes
• Crear un pod en Minikube
• Configurar Docker para Mac o Windows para utilizar Kubernetes como
un orquestador
• Crear una implementación en Docker para Mac o Windows
• Crear un servicio Kubernetes para exponer un servicio de aplicación
internamente (o externamente) al clúster

Requisitos técnicos
El enlace a los archivos de código puede encontrarse aquí, en https://github.com/
appswithdockerandkubernetes/labs/tree/master/ch10.

Arquitectura
Un clúster de Kubernetes consta de un conjunto de servidores. Estos servidores pueden
ser máquinas virtuales o servidores físicos. Estos últimos también se llaman bare metal.
Cada miembro del clúster puede tener uno de dos roles. Puede ser un nodo maestro o
un nodo (de trabajo) de Kubernetes. El primero se utiliza para administrar el clúster,
mientras que el segundo ejecutará la carga de trabajo de la aplicación. He incluido el
nodo de trabajo entre paréntesis, ya que en el lenguaje de Kubernetes solo se habla de
un nodo cuando se hace referencia a un servidor que ejecuta la carga de trabajo de una
aplicación. Pero en el lenguaje de Docker y en Swarm, el equivalente es un nodo de
trabajo. Creo que la noción de un nodo de trabajo describe mejor el rol del servidor que
simplemente un nodo.

En un clúster, tenemos un número pequeño e impar de nodos maestros y tantos nodos


de trabajo como sea necesario. Es posible que los clústeres pequeños solo tengan unos
pocos nodos de trabajo, mientras que los clústeres más realistas pueden tener decenas o
incluso cientos de nodos de trabajo. Técnicamente, no hay límite en el número de nodos
de trabajo que un clúster puede tener; la realidad, sin embargo, es que el rendimiento de
algunas operaciones de administración se puede reducir considerablemente si trabajamos
con miles de nodos. Todos los miembros del clúster deben estar conectados mediante una
red física, que recibe el nombre de red subyacente.

[ 182 ]
Capítulo 10

Kubernetes define una red plana para todo el clúster. Kubernetes no proporciona
ninguna implementación de red estándar, sino que utiliza complementos de
terceros. Kubernetes solo define la interfaz de red de contenedores (CNI) y deja la
implementación a otros. La CNI es muy simple. Básicamente, indica que cada pod que
se ejecute en el clúster debe poder conectar con cualquier otro pod que también se ejecute
en el clúster sin que ocurra ninguna conversión de direcciones de red (NAT) entre
medias. Lo mismo ocurre entre los nodos del clúster y los pods; es decir, las aplicaciones
o daemons que se ejecutan en un nodo del clúster deben poder comunicarse con cada
pod del clúster, y viceversa.

En el siguiente diagrama, he intentado ilustrar la arquitectura general de un clúster


de Kubernetes:

Diagrama de arquitectura general de Kubernetes

[ 183 ]
Orquestación de aplicaciones en contenedores con Kubernetes

El diagrama anterior se explica de la siguiente manera:

• Arriba en el centro, tenemos un clúster de nodos etcd. etcd es un almacén


de claves-valores distribuido que, en un clúster de Kubernetes, se utiliza para
almacenar todos los estados del clúster. El número de nodos etcd tiene que ser
impar, como indica el protocolo de consenso Raft, que usan los nodos para
coordinarse unos con otros. Cuando hablamos del estado del clúster, no nos
referimos a los datos producidos o consumidos por las aplicaciones que se
ejecutan en el clúster, sino a toda la información sobre la topología del clúster,
a los servicios que se están ejecutando, a la configuración de red, a los secretos
utilizados, etc. Dicho esto, este clúster etcd es en realidad crítico para el clúster,
y, por lo tanto, nunca debemos ejecutar solo un servidor etcd en un entorno
de producción o en cualquier entorno que necesite estar disponible en todo
momento.
• A continuación, tenemos un clúster de nodos maestros Kubernetes que también
forman un grupo de consenso entre ellos, similar a los nodos etcd. El número
de nodos maestros también tiene que ser un número impar. Podemos ejecutar
el clúster con un solo nodo maestro, pero nunca en un sistema de producción
o crítico. Allí, siempre deberíamos tener al menos tres nodos maestros. Como
los nodos maestros se utilizan para administrar todo el clúster, también estamos
hablando del plano de administración. Los nodos maestros utilizan el clúster
etcd como su almacén de respaldo. Es recomendable instalar un equilibrador
de carga (LB) delante de los nodos maestros con un nombre de dominio
completo (FQDN) conocido, como https://admin.example.com. Todas las
herramientas que se utilizan para administrar el clúster de Kubernetes deben
tener acceso a él a través del equilibrador de carga, en lugar de utilizar la
dirección IP pública de uno de los nodos maestros. Esto se muestra en la parte
superior izquierda del diagrama anterior.
• En la parte inferior del diagrama, tenemos un clúster de nodos de trabajo.
El número de nodos puede ser tan bajo como 1 y tan alto como se quiera. El
nodo maestro y los nodos de trabajo de Kubernetes se comunican entre sí. Es
una forma bidireccional de comunicación, que es diferente de la que se produce
en Docker Swarm. En Docker Swarm, solo los nodos de administración se
comunican con los nodos de trabajo y nunca al revés. Todo el tráfico entrante
que accede a las aplicaciones que se ejecutan en el clúster debe pasar por otro
equilibrador de carga. Este es el equilibrador de carga o el proxy inverso de la
aplicación. En ningún caso queremos que el tráfico externo acceda directamente
a ninguno de los nodos de trabajo.

Ahora que tenemos una idea de la arquitectura general de un clúster de Kubernetes,


vamos a examinar más detenidamente el nodo maestro y los nodos de trabajo
de Kubernetes.

[ 184 ]
Capítulo 10

Nodos maestros de Kubernetes


Los nodos maestros de Kubernetes se utilizan para administrar un clúster de Kubernetes.
A continuación, se muestra un diagrama general de estos nodos maestros:

Nodo maestro de Kubernetes

En la parte inferior del diagrama anterior, tenemos la infraestructura, que puede ser una
MV on-premises, en el cloud o un servidor (que recibe también el nombre de bare metal).
Actualmente, los nodos maestros de Kubernetes solo funcionan en Linux. Se admiten las
distribuciones más populares de Linux, como RHEL, CentOS y Ubuntu. En esta máquina
Linux, tenemos al menos los siguientes cuatro servicios Kubernetes ejecutándose:

• Servidor de API: esta es la gateway que se comunica con Kubernetes. Todas


las solicitudes para enumerar, crear, modificar o eliminar cualquier recurso
del clúster deben pasar por este servicio. Expone una interfaz REST que
las herramientas como kubectl utilizan para administrar el clúster y las
aplicaciones del clúster.
• Controlador: el controlador o más precisamente, el administrador del
controlador, es un bucle de control que observa el estado del clúster a través
del servidor de la API y realiza cambios para intentar pasar del estado actual
o vigente al estado deseado.
• Programador: el programador es un servicio que hace todo lo posible por
programar pods en los nodos de trabajo teniendo en cuenta varias condiciones
de demarcación, como los requisitos de los recursos, las políticas, los requisitos
de calidad de servicio, etc.
• Almacén del clúster: es una instancia de etcd que se utiliza para almacenar toda
la información sobre el estado del clúster.

[ 185 ]
Orquestación de aplicaciones en contenedores con Kubernetes

Para ser más precisos, etcd, que se utiliza como almacén del clúster, no tiene que estar
necesariamente instalado en el mismo nodo que los otros servicios de Kubernetes.
A veces, los clústeres de Kubernetes se configuran para utilizar clústeres independientes
de servidores etcd, como se muestra en el diagrama de arquitectura de la sección
anterior. Pero la variante que se debe utilizar es una decisión avanzada de administración
y queda fuera del alcance de este libro.

Necesitamos al menos un nodo maestro, pero para conseguir una alta disponibilidad,
necesitamos tres o más nodos maestros. Esto es muy similar a lo que hemos aprendido
acerca de los nodos de administración de Docker Swarm. En este sentido, un nodo
maestro de Kubernetes equivale a un nodo de administración de Swarm.

Los nodos maestros de Kubernetes nunca ejecutan la carga de trabajo de una aplicación.
Su única finalidad es administrar el clúster. Los nodos maestros se basan en un grupo
de consenso Raft. El protocolo Raft es un protocolo estándar utilizado en aquellas
situaciones en las que un grupo de miembros necesita tomar decisiones. Se utiliza
en muchos productos de software conocidos tales como MongoDB, Docker SwarmKit
y Kubernetes. Para una explicación más exhaustiva sobre el protocolo Raft, visita
el enlace de la sección Lectura adicional.

Como hemos mencionado en la sección anterior, el estado del clúster de Kubernetes


se almacena en etcd. Si se supone que el clúster Kubernetes debe estar altamente
disponible, entonces etcd también debe configurarse en modo de alta disponibilidad, lo
que normalmente significa que debe haber al menos tres instancias de etcd ejecutándose
en diferentes nodos.

Téngase en cuenta una vez más que todo el estado del clúster se almacena en etcd. Esto
incluye toda la información acerca de todos los nodos del clúster, todos los conjuntos
de réplicas, las implementaciones, los secretos, las políticas de red, la información de
enrutamiento, etc. Por lo tanto, es crucial que dispongamos de una sólida estrategia
de respaldo para este almacén de claves-valores.

Veamos ahora los nodos que ejecutan la carga de trabajo real del clúster.

Nodos del clúster


Los nodos del clúster son los nodos en los que Kubernetes programa la carga de trabajo
de una aplicación. Son las "bestias de carga" del clúster. Un clúster de Kubernetes puede
tener unos pocos, decenas, cientos o incluso miles de nodos. Kubernetes se ha diseñado
para una alta escalabilidad. No olvidemos que Kubernetes se creó después de Google
Borg, que lleva años ejecutando decenas de miles de contenedores:

[ 186 ]
Capítulo 10

Nodo de trabajo de Kubernetes

Un nodo de trabajo puede ejecutarse en una máquina virtual o en un servidor físico,


on-premises o en el cloud. Originalmente, los nodos de trabajo solo podían configurarse
en Linux. Pero desde la versión 1.10 de Kubernetes, los nodos de trabajo también pueden
ejecutarse en Windows Server 2010. Es totalmente aceptable tener un clúster mixto con
nodos de trabajo de Linux y Windows.

En cada nodo, tenemos tres servicios que necesitan ejecutarse, los cuales se describen
de la siguiente manera:

• Kubelet: este es el primer servicio y el más importante. Kubelet es lo que se


llama el agente de nodo principal. El servicio Kubelet utiliza las especificaciones
de los pods para asegurarse de que todos los contenedores de los pods
correspondientes estén funcionando y en buen estado. Las especificaciones
de los pods son archivos escritos en YAML o JSON, y describen un pod de forma
declarativa. Explicaremos los pods en la siguiente sección. Las especificaciones
de los pods (PodSpecs) se proporcionan a Kubelet principalmente a través del
servidor de la API.
• Runtime de contenedor: el segundo servicio que debe estar presente en
cada nodo de trabajo es un runtime de contenedor. Kubernetes, de forma
predeterminada, utiliza containerd desde la versión 1.9 como su runtime
de contenedor. Anteriormente utilizaba el daemon de Docker. Se pueden usar
otros runtimes de contenedor, como RKT o CRI-O. El runtime de contenedor
es responsable de administrar y ejecutar los distintos contenedores de un pod.
• kube-proxy: por último, está kube-proxy. Se ejecuta como un daemon y es un
proxy de red sencillo y un equilibrador de carga para todos los servicios de la
aplicación que se ejecutan en ese nodo en particular.

[ 187 ]
Orquestación de aplicaciones en contenedores con Kubernetes

Ahora que conocemos la arquitectura de Kubernetes y los nodos maestros y de trabajo,


es hora de presentar las herramientas que podemos utilizar para desarrollar aplicaciones
dirigidas a Kubernetes.

Minikube
Minikube es una herramienta que crea un clúster de Kubernetes de un solo nodo
o Hyper-V (se admiten otros hipervisores) para su uso durante el desarrollo de
una aplicación en contenedor. En el Capítulo 2, Configuración de un entorno de trabajo,
mostramos cómo Minikube, y con él, la herramienta kubectl, se puede instalar en un
portátil Mac o Windows. Como ya se ha dicho, Minikube es un clúster de Kubernetes de
un solo nodo y, por lo tanto, el nodo es al mismo tiempo un nodo maestro de Kubernetes
y un nodo de trabajo.

Vamos a asegurarnos de que Minikube se está ejecutando con el siguiente comando:


$ minikube start

Una vez que Minikube esté listo, podremos acceder a su clúster de un solo nodo
mediante kubectl. Y deberíamos ver algo similar a la siguiente captura de pantalla:

Enumeración de todos los nodos de Minikube

Como se mencionó anteriormente, tenemos un clúster de un solo nodo con un nodo


llamado minikube. No te dejes confundir por el valor <none> de la columna ROLES:
el nodo desempeña el rol de un nodo de trabajo y maestro al mismo tiempo.

Ahora, vamos a intentar implementar un pod en este clúster. No te preocupes por lo que
es exactamente un pod en este momento; lo explicaremos en detalle más adelante en este
capítulo. Por el momento, vamos a usarlo sin más.

Podemos utilizar el archivo sample-pod.yaml de la subcarpeta ch10 de nuestra carpeta


labs para crear este pod. Tiene el siguiente contenido:

apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx

[ 188 ]
Capítulo 10

image: nginx:alpine
ports:
- containerPort: 80
- containerPort: 443

Vamos a usar la CLI de Kubernetes llamada kubectl para implementar este pod:
$ kubectl create -f sample-pod.yaml
pod "nginx" created

Si ahora enumeramos todos los pods, deberíamos ver esto:


$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 51s

Para poder acceder a este pod, necesitamos crear un servicio. Vamos a utilizar el archivo
sample-service.yaml, que tiene el siguiente contenido:

apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: LoadBalancer
ports:
- port: 8080
targetPort: 80
protocol: TCP
name: http
- port: 443
protocol: TCP
name: https
selector:
app: nginx

Una vez más, no te preocupes ahora por lo que es exactamente un servicio.


Lo explicaremos en detalle más adelante. Vamos a crear este servicio:
$ kubectl create -f sample-service.yaml

Ahora podemos usar curl para acceder al servicio:


$ curl -4 http://localhost

Deberíamos recibir la página de bienvenida de nginx como respuesta. Antes


de continuar, vamos a eliminar los dos objetos que acabamos de crear:
$ kubectl delete po/nginx
$ kubectl delete svc/nginx-service

[ 189 ]
Orquestación de aplicaciones en contenedores con Kubernetes

Compatibilidad de Kubernetes en Docker


para el escritorio
A partir de la versión 18.01-ce, Docker para Mac y Docker para Windows admiten
Kubernetes "out of the box". Los desarrolladores que deseen implementar sus
aplicaciones en contenedor en Kubernetes pueden utilizar este orquestador en
lugar de SwarmKit. La compatibilidad con Kubernetes está desactivada de forma
predeterminada y tiene que activarse en la configuración. La primera vez que se habilita
Kubernetes, Docker para Mac o Windows necesitará un momento para descargar
todos los componentes necesarios para crear un clúster de Kubernetes de un solo
nodo. A diferencia de Minikube, que también es un clúster de un solo nodo, la versión
proporcionada por las herramientas de Docker utiliza versiones en contenedor de todos
los componentes de Kubernetes:

Compatibilidad de Kubernetes en Docker para Mac y Windows

El diagrama anterior nos ofrece una descripción aproximada de cómo se ha agregado


la compatibilidad con Kubernetes a Docker para Mac y Windows. Docker para Mac
utiliza hyperkit para ejecutar una MV basada en LinuxKit. Docker para Windows utiliza
Hyper-V para el mismo propósito. Dentro de la MV, el motor de Docker está instalado.
Parte del motor es SwarmKit, que permite Swarm-Mode. Docker para Mac o Windows
utiliza la herramienta kubeadm para preparar y configurar Kubernetes en esa máquina
virtual. Cabe destacar los tres hechos siguientes:

• Kubernetes almacena su estado de clúster en etcd; por lo tanto, tenemos etcd


ejecutándose en esta máquina virtual.
• A continuación, tenemos todos los servicios que componen Kubernetes.

[ 190 ]
Capítulo 10

• Por último, tenemos algunos servicios que admiten la implementación de Docker


desde la CLI de Docker en Kubernetes. Este servicio no forma parte de la
distribución oficial de Kubernetes, sino que es específico de Docker.

Todos los componentes de Kubernetes se ejecutan en contenedores en la máquina virtual


basada en LinuxKit. Estos contenedores se pueden ocultar a través de una configuración
en Docker para Mac o Windows. Más abajo, en la sección, puedes ver una lista completa
de los contenedores del sistema de Kubernetes que funcionan en tu portátil, si tienes
la compatibilidad con Kubernetes activada. Para evitar repetirme, a partir de ahora,
solo hablaré de Docker para el escritorio, en lugar de Docker para Mac y Docker para
Windows. Todo lo que diga se aplicará igualmente a ambas ediciones.

Una gran ventaja de Docker para el escritorio con Kubernetes habilitado a través de
Minikube es que permite a los desarrolladores utilizar una única herramienta para
crear, probar y ejecutar una aplicación en contenedor dirigida a Kubernetes. Incluso es
posible implementar una aplicación multiservicio en Kubernetes, utilizando un archivo
de Docker Compose.

Ahora, vamos a ensuciarnos las manos:

1. Primero tenemos que activar Kubernetes.


2. En el Mac, hacemos clic en el icono Docker en la barra de menús y seleccionamos
Preferencias.
3. En el cuadro de diálogo que se abre, seleccionamos la opción Kubernetes ,
como se muestra en la siguiente captura de pantalla:

Habilitar Kubernetes en Docker para Mac

[ 191 ]
Orquestación de aplicaciones en contenedores con Kubernetes

4. A continuación, activamos la casilla de verificación Habilitar Kubernetes.


También marcamos la casilla avanzada Mostrar contenedores del sistema.
5. Hacemos clic en el botón Aplicar. Aparecerá una advertencia que nos dice que
la instalación y configuración de Kubernetes tarda unos minutos:

Advertencia de que la instalación y configuración de Kubernetes tarda un tiempo

6. Hacemos clic en el botón Instalar para iniciar la instalación. Ahora es el momento


de tomarnos un descanso y disfrutar de una buena taza de té.

Una vez finalizada la instalación (Docker nos avisa de esto mostrando un icono de estado
verde en el cuadro de diálogo de configuración), podemos probarla. Puesto que ahora
tenemos dos clústeres de Kubernetes que se ejecutan en nuestro portátil, Minikube y
Docker para Mac, necesitamos configurar kubectl para acceder a este último. En primer
lugar, vamos a enumerar todos los contextos que tenemos:

Lista de contextos de kubectl

Aquí, podemos ver que, en mi portátil, tengo los dos contextos mencionados
anteriormente. En este momento, el contexto de Minikube está activo, visible por el
asterisco de la columna CURRENT. Podemos cambiar al contexto docker-for-desktop
utilizando el siguiente comando:

Cambiar el contexto de la CLI de Kubernetes

[ 192 ]
Capítulo 10

Ahora podemos usar Kubectl para acceder al clúster que Docker para Mac acaba de crear.
Deberíamos ver esto:

El clúster de Kubernetes de un solo nodo creado por Docker para Mac

Esto es muy reconocible. Es prácticamente lo mismo que vimos cuando trabajamos


con Minikube. La versión de Kubernetes que utiliza mi Docker para Mac es la 1.9.2.
También podemos ver que el nodo es un nodo maestro.

Si enumeramos todos los contenedores que se están ejecutando en nuestro Docker para
Mac, obtenemos esta lista (obsérvese que he usado el argumento --format para mostrar
solo el Container ID y los Names de los contenedores), como se muestra en la siguiente
captura de pantalla:

Contenedores del sistema Kubernetes

En la lista, podemos identificar todos los componentes ya conocidos que componen


Kubernetes, como los siguientes:

• Servidor de API
• etcd
• Proxy de Kube
• Servicio DNS
• Controlador de Kube
• Programador de Kube

[ 193 ]
Orquestación de aplicaciones en contenedores con Kubernetes

También hay contenedores con la palabra "compose". Estos son servicios específicos de
Docker y se utilizan para permitirnos implementar aplicaciones de Docker Compose en
Kubernetes. Docker traduce la sintaxis de Docker Compose y crea implícitamente los
objetos de Kubernetes necesarios, como las implementaciones, los pods y los servicios.

Normalmente, no querríamos llenar nuestra lista de contenedores con estos contenedores


del sistema. Por lo tanto, podemos desactivar la casilla Mostrar contenedores del sistema
en la configuración de Kubernetes.

Ahora vamos a intentar implementar una aplicación de Docker Compose en


Kubernetes. Vamos a desplazarnos hasta la subcarpeta ch10 de nuestra carpeta labs.
Implementamos la aplicación como una pila utilizando el archivo docker-compose.
yml:

$ docker stack deploy -c docker-compose.yml app

Esto es lo que vemos:

Implementar la pila en Kubernetes

Podemos probar la aplicación usando, por ejemplo, curl, y veremos que se está
ejecutando según lo previsto:

Aplicación Pets ejecutándose en Kubernetes en Docker para Mac

[ 194 ]
Capítulo 10

Ahora, deberías tener curiosidad y preguntarte qué hizo exactamente Docker


cuando ejecutamos el comando docker stack deploy. Podemos utilizar kubectl
para averiguarlo:

Una lista de todos los objetos de Kubernetes, creados por el comando docker stack deploy

Docker creó una implementación para el servicio web y un conjunto con estado para el
servicio db. También creó automáticamente los servicios de Kubernetes para web y db,
para que se pueda acceder a ellos desde dentro del clúster. Asimismo, creó el servicio de
Kubernetes svc/web-published, que se utiliza para el acceso externo.

Esto es magnífico, ya que disminuye enormemente la fricción en el proceso de desarrollo


para los equipos que utilizan Kubernetes como orquestador.

Antes de continuar, no olvides eliminar la pila del clúster:


$ docker stack rm app

Asegúrate también de restablecer el contexto de kubectl de nuevo a Minikube, ya que


vamos a utilizar Minikube para todos nuestros ejemplos de este capítulo:
$ kubectl config use-context minikube

Ahora que hemos explicado las herramientas que podemos utilizar para desarrollar
aplicaciones que finalmente se ejecutarán en un clúster de Kubernetes, es hora de conocer
todos los objetos de Kubernetes importantes que se utilizan para definir y administrar
dicha aplicación. Vamos a empezar con los pods.

[ 195 ]
Orquestación de aplicaciones en contenedores con Kubernetes

Pods
Contrariamente a lo que ocurre con un Docker Swarm, no se pueden ejecutar
contenedores directamente en un clúster de Kubernetes. En un clúster de Kubernetes,
solo se pueden ejecutar pods. Los pods son unidades atómicas de implementación en
Kubernetes. Un pod es una abstracción de uno o varios contenedores coubicados que
comparten los mismos espacios de nombres del kernel, como el espacio de nombres
de red. No existe ningún equivalente en Docker SwarmKit. El hecho de que más de
un contenedor pueda estar coubicado y compartir el mismo espacio de nombres de red
es un concepto muy útil. En el siguiente diagrama se muestran dos pods:

Pods de Kubernetes

En el diagrama anterior, tenemos dos pods, Pod 1 y Pod 2. El primer pod tiene dos
contenedores, mientras que el segundo solo tiene un contenedor. Cada pod obtiene una
dirección IP asignada por Kubernetes que es única en todo el clúster de Kubernetes. En
nuestro caso, tenemos las direcciones IP 10.0.12.3 y 10.0.12.5. Ambas forman parte
de una subred privada administrada por el controlador de red de Kubernetes.

Un pod puede contener uno o varios contenedores. Todos estos contenedores comparten
los mismos espacios de nombres del kernel y, en concreto, comparten el espacio de
nombres de red. Esto se indica con el rectángulo punteado que rodea los contenedores.
Como todos los contenedores que se ejecutan en el mismo pod comparten el espacio de
nombres de red, cada contenedor necesita asegurarse de utilizar su propio puerto, ya que
no se permiten puertos duplicados en el mismo espacio de nombres de red. En este caso,
en Pod 1, el contenedor principal utiliza el puerto 80, mientras que el contenedor auxiliar
utiliza el puerto 3000.

Las solicitudes de otros pods o nodos pueden utilizar la dirección IP del pod junto con
el número de puerto correspondiente para acceder a los distintos contenedores. Por
ejemplo, podríamos acceder a la aplicación que se ejecuta en el contenedor principal
de Pod 1 a través de 10.0.12.3:80.

[ 196 ]
Capítulo 10

Comparación de la red de un contenedor


Docker con un pod de Kubernetes
Ahora vamos a comparar la red del contenedor Docker con la red de un pod
de Kubernetes. En el diagrama de aquí, tenemos el primero en el lado izquierdo
y el segundo en el lado derecho:

Contenedores en un pod que comparten el espacio de nombres

Cuando se crea un contenedor Docker y no se especifica ninguna red específica, el motor


de Docker crea un punto de conexión de ethernet virtual (veth). El primer contenedor
obtiene veth0, el segundo obtiene veth1, y así sucesivamente. Estos puntos de conexión
de ethernet virtual están conectados al puente de Linux docker0 que Docker crea
automáticamente cuando se instala. El tráfico se enruta desde el docker0 del puente
a cada punto de conexión veth conectado. Cada contenedor tiene su propio espacio de
nombres de red. No hay dos contenedores que utilicen el mismo espacio de nombres.
Esto es deliberado, para aislar las aplicaciones que se ejecutan dentro de los contenedores
unas de otras.

Para un pod de Kubernetes, la situación es diferente. Cuando se crea un nuevo pod,


Kubernetes crea primero lo que se llama un contenedor pause, cuya única finalidad
es crear y administrar los espacios de nombres que el pod compartirá con todos los
contenedores. Aparte de eso, no hace nada útil; solo está a la espera. El contenedor
pause está conectado al docker0 de puente a través de veth0. Cualquier contenedor
posterior que forme parte del pod utiliza una característica especial del motor de Docker
que le permite reutilizar un espacio de nombres de red existente. La sintaxis de esto
es la siguiente:
$ docker container create --net container:pause ...

[ 197 ]
Orquestación de aplicaciones en contenedores con Kubernetes

La parte importante es el argumento --net, que se usa como un valor


container:<nombre de contenedor>. Si creamos un nuevo contenedor de esta
manera, Docker no crea un nuevo punto de conexión veth, sino que el contenedor usa
el mismo que el contenedor pause.

Otra consecuencia importante de que varios contenedores compartan el mismo espacio


de nombres de red es la forma en que se comunican entre sí. Veamos la siguiente
situación en la que un pod tiene dos contenedores, uno que escucha en el puerto
80 y otro en el puerto 3000:

Los contenedores en pods se comunican a través de localhost

Cuando dos contenedores utilizan el mismo espacio de nombres de red del kernel
de Linux, pueden comunicarse entre sí a través de localhost; esto es similar a cuando dos
procesos se ejecutan en el mismo host, ya que pueden comunicarse entre sí también a
través de localhost. Esto se ilustra en el diagrama anterior. Desde el contenedor principal,
la aplicación en contenedor incluida en él puede comunicarse con el servicio que
se ejecuta dentro del contenedor auxiliar a través de http://localhost:3000.

Compartir el espacio de nombres de red


Después de toda esta teoría, tal vez te preguntes cómo Kubernetes crea en realidad un
pod. Kubernetes solo utiliza lo que le proporciona Docker. Entonces, ¿cómo funciona este
espacio de nombres de red? En primer lugar, Kubernetes crea lo que llamamos "contenedor
pause", como se mencionó anteriormente. Este contenedor no tiene otra función que
reservar los espacios de nombres del kernel para ese pod y mantenerlos activos, incluso
si no se está ejecutando ningún otro contenedor dentro del pod. Vamos a simular la
creación de un pod. Empezamos creando el contenedor pause y usamos Nginx para este
propósito:
$ docker container run -d --name pause nginx:alpine

[ 198 ]
Capítulo 10

Y ahora agregamos un segundo contenedor llamado main y lo conectamos al mismo


espacio de nombres de red que el contenedor pause:
$ docker container run --name main -dit \
--net container:pause \
alpine:latest /bin/sh

Como el contenedor pause y el contenedor de ejemplo forman parte del mismo espacio
de nombres de red, pueden comunicarse entre sí a través de localhost. Para mostrar
esto, primero tenemos que ejecutar el comando exec en el contenedor principal:
$ docker exec -it main /bin/sh

Ahora podemos probar la conexión con Nginx ejecutándolo en el contenedor pause


y escuchando en el puerto 80. Esto es lo que obtenemos si usamos la utilidad wget para
tal fin:

Dos contenedores que comparten el mismo espacio de nombres de red

[ 199 ]
Orquestación de aplicaciones en contenedores con Kubernetes

El resultado muestra que efectivamente podemos acceder a Nginx en localhost. Esta


es la prueba de que los dos contenedores comparten el mismo espacio de nombres. Si eso
no es suficiente, podemos usar la herramienta ip para mostrar eth0 dentro de ambos
contenedores, y obtendremos el mismo resultado, en concreto, la misma dirección IP,
que es una de la características de un pod cuando todos sus contenedores comparten la
misma dirección IP:

Visualización de las propiedades de eth0 con la herramienta ip

Si inspeccionamos la red bridge, podemos ver que solo se muestra el contenedor pause.
El otro contenedor no tiene una entrada en la lista Containers, ya que está reutilizando
el punto de conexión del contenedor pause:

Inspección de la red de puente predeterminada de Docker

[ 200 ]
Capítulo 10

Ciclo de vida de los pods


Hemos aprendido anteriormente en este libro que los contenedores tienen un ciclo de
vida. Un contenedor se inicializa, se ejecuta y después se termina. Cuando un contenedor
se termina, lo puede hacer elegantemente con un código de salida cero o con un error,
que equivale a un código de salida distinto de cero.

Del mismo modo, los pods tienen un ciclo de vida. Debido al hecho de que un pod puede
contener más de un contenedor, este ciclo de vida es un poco más complicado que el de
un solo contenedor. El ciclo de vida de un pod se muestra en el siguiente diagrama:

Ciclo de vida de los pods de Kubernetes

Cuando se crea un pod en un nodo del clúster, primero tiene el estado pending
(pendiente). Una vez que todos los contenedores del pod están funcionando, el pod
adopta el estado running (en ejecución). El pod solo pasa a este estado si todos sus
contenedores se ejecutan correctamente. Si se le pide al pod que termine, pedirá a todos
sus contenedores que terminen. Si todos los contenedores terminan con un código
de salida de cero, entonces el pod pasa al estado "succeeded" (conseguido). Esto es así
cuando todo sale bien.

Veamos ahora algunos escenarios que hacen que un pod acabe con el estado failed
(fallido). Hay tres escenarios posibles:

• Si durante el arranque del pod, al menos un contenedor no es capaz de ejecutarse


y falla (es decir, termina con un código de salida distinto de cero), el pod pasa del
estado pending al estado failed.
• Si el pod tiene el estado "running" y uno de los contenedores se bloquea de
repente o termina con un código de salida distinto de cero, el pod pasa del estado
running al estado "failed".
• Si se pide al pod que termine y durante el apagado al menos uno de los
contenedores termina con un código de salida distinto de cero, el pod también
pasa al estado "failed".

[ 201 ]
Orquestación de aplicaciones en contenedores con Kubernetes

Especificación del pod


Cuando se crea un pod en un clúster de Kubernetes, podemos utilizar un enfoque
imperativo o un enfoque declarativo. Hemos explicado la diferencia de los dos enfoques
anteriormente en este libro, pero vamos a repetir lo más importante: usar un enfoque
declarativo significa que escribimos un manifiesto que describe el estado final que
queremos alcanzar. Dejaremos los detalles de cómo hacerlo al orquestador. El estado
final que queremos alcanzar también se llama estado deseado. En general, el enfoque
declarativo es el preferido con diferencia en todos los orquestadores establecidos,
y Kubernetes no es una excepción.

Por tanto, en este capítulo, nos centraremos exclusivamente en el enfoque declarativo.


Los manifiestos o las especificaciones de un pod pueden escribirse utilizando el formato
YAML o JSON. En este capítulo, nos centraremos en YAML, ya que es más fácil de
leer para nosotros, los seres humanos. Veamos una especificación de ejemplo. Este
es el contenido del archivo pod.yaml, que se puede encontrar en la subcarpeta ch10
de nuestra carpeta labs:
apiVersion: v1
kind: Pod
metadata:
name: web-pod
spec:
containers:
- name: web
image: nginx:alpine
ports:
- containerPort: 80

Cada especificación en Kubernetes comienza con la información de versión. Los pods


llevan con nosotros bastante tiempo, así que la versión de la API es v1. La segunda línea
especifica el tipo de objeto o recurso de Kubernetes que queremos definir. Obviamente,
en este caso, queremos especificar un pod. A continuación tenemos un bloque con
metadatos. Como mínimo, necesitamos asignar un nombre al pod. Aquí, lo llamamos
web-pod. El siguiente bloque es el bloque spec, que contiene la especificación del pod.
La parte más importante (y la única en este sencillo ejemplo) es la lista de todos los
contenedores que forman parte de este pod. Aquí solo tenemos un contenedor, pero
podríamos tener unos cuantos. El nombre que elegimos para nuestro contenedor es
web y la imagen es nginx:alpine. Por último, definimos la lista de puertos en los que
se expone el contenedor.

Una vez que hayamos creado esta especificación, podemos aplicarla al clúster utilizando
la CLI de Kubernetes kubectl. En el terminal, nos desplazamos hasta la subcarpeta
ch10 y ejecutamos el siguiente comando:

$ kubectl create -f pod.yaml

[ 202 ]
Capítulo 10

Este comando responderá con el pod "web-pod" creado. Ahora podemos mostrar todos
los pods del clúster con kubectl get pods:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-pod 1/1 Running 0 2m

Como era de esperar, tenemos uno de un solo pod con el estado "running". El pod
se llama web-pod, tal como lo hemos definido. Podemos obtener información más
detallada acerca del pod en ejecución con el comando describe:

Descripción de un pod que se ejecuta en el clúster

Observa la notación pod/web-pod del comando describe anterior. Otras variantes


son posibles, por ejemplo, pods/web-pod o po/web-pod. pod y po son alias de pods.
La herramienta kubectl define muchos alias para hacernos la vida un poco más fácil.

[ 203 ]
Orquestación de aplicaciones en contenedores con Kubernetes

El comando describe nos ofrece multitud de información valiosa sobre el pod, como la
lista de eventos que se produjeron con este pod. La lista se muestra al final del resultado.

La información de la sección Containers es muy similar a lo que encontramos en un


resultado del comando docker container inspect.

También vemos una sección Volumes con alguna entrada del tipo Secret. Hablaremos
de los secretos de Kubernetes en el próximo capítulo. Los volúmenes, por otra parte,
se explican a continuación.

Pods y volúmenes
En el capítulo sobre contenedores, hemos aprendido qué son los volúmenes y su
finalidad: acceder y almacenar datos persistentes. Al igual que los contenedores, los
pods también pueden montar volúmenes. En realidad, son los contenedores incluidos
en el pod los que montan los volúmenes, pero este es solo un detalle semántico. Veamos
primero cómo podemos definir un volumen en Kubernetes. Kubernetes admite muchos
tipos de volúmenes y no vamos a detenernos demasiado en ello. Vamos a crear un
volumen local de manera implícita definiendo un PersistentVolumeClaim llamado
my-data-claim:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-data-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi

Hemos definido una notificación que solicita 2 GB de datos. Vamos a crear esta
notificación:
$ kubectl create -f volume-claim.yaml

Podemos mostrar la notificación mediante kubectl (pvc es el nombre abreviado de


PersistentVolumeClaim):

Lista de objetos PersistentStorageClaim del clúster

[ 204 ]
Capítulo 10

En el resultado, podemos ver que la notificación ha creado implícitamente un volumen


llamado pvc-<ID>. Ahora estamos listos para usar el volumen creado por la notificación
en un pod. Vamos a usar una versión modificada de la especificación del pod que
usamos anteriormente. Podemos encontrar esta especificación actualizada en el archivo
pod-with-vol.yaml en la carpeta ch10. Analicemos esta especificación detalladamente:

apiVersion: v1
kind: Pod
metadata:
name: web-pod
spec:
containers:
- name: web
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: my-data
mountPath: /data
volumes:
- name: my-data
persistentVolumeClaim:
claimName: my-data-claim

En las últimas cuatro líneas, en el bloque volumes, definimos la lista de volúmenes que
queremos utilizar para este pod. Los volúmenes que mostramos aquí los puede utilizar
cualquiera de los contenedores del pod. En nuestro caso particular, solo tenemos un
volumen. Definimos que tenemos un volumen my-data que es una notificación de
volumen persistente cuyo nombre de notificación es el que acabamos de crear. Luego,
en la especificación del contenedor, tenemos el bloque volumeMounts, donde definimos
el volumen que queremos usar y la ruta (absoluta) dentro del contenedor donde se
montará el volumen. En nuestro caso, montamos el volumen en la carpeta /data del
sistema de archivos del contenedor. Vamos a crear este pod:
$ kubectl create -f pod-with-vol.yaml

A continuación, podemos ejecutar el comando exec en el contenedor para comprobar


que el volumen se ha montado; para ello, nos desplazamos hasta la carpeta /data,
creamos un archivo allí y terminamos el contenedor:
$ kubectl exec -it web-pod -- /bin/sh
/ # cd /data
/data # echo "Hello world!" > sample.txt
/data # exit

[ 205 ]
Orquestación de aplicaciones en contenedores con Kubernetes

Si tenemos razón, entonces los datos de este contenedor deben persistir más allá del ciclo
de vida del pod. Por lo tanto, vamos a eliminar el pod, y después vamos a crearlo de
nuevo y ejecutar el comando exec en él para asegurarnos de que los datos están todavía
allí. Este es el resultado:

Los datos almacenados en el volumen sobreviven a la recreación del pod

Conjunto de réplicas de Kubernetes


Un único pod en un entorno con requisitos de alta disponibilidad es insuficiente. ¿Y si el
pod deja de funcionar? ¿Qué sucede si necesitamos actualizar la aplicación que se ejecuta dentro
del pod, pero no podemos permitirnos ninguna interrupción del servicio? Estas preguntas
y otras más solo pueden indicar que los pods por sí solos no son suficientes y que
necesitamos un concepto de nivel superior que pueda gestionar múltiples instancias
del mismo pod. En Kubernetes, ReplicaSet (conjunto de réplicas) se utiliza para definir
y administrar una colección de pods idénticos que se ejecutan en diferentes nodos del
clúster. Entre otras cosas, un ReplicaSet define qué imágenes de contenedor utilizan los
contenedores que se ejecutan dentro de un pod y cuántas instancias del pod se ejecutarán
en el clúster. Estas propiedades y muchas otras reciben el nombre de estado deseado.

El ReplicaSet es responsable de conciliar el estado deseado en todo momento, si el estado


real se desvía en algún momento de él. Este es un ReplicaSet de Kubernetes:

Conjunto de réplicas de Kubernetes

En el diagrama anterior, vemos uno de estos ReplicaSet llamado rs-api, que regula un
número de pods. Los pods se llaman pod-api. El ReplicaSet es responsable de asegurarse
de que en un momento dado siempre haya el número deseado de pods en ejecución.
Si uno de los pods deja de funcionar por cualquier motivo, el ReplicaSet programa
un nuevo pod en un nodo con recursos libres. Si hay más pods que el número deseado,
el ReplicaSet destruye los pods superfluos. Podemos decir, pues, que el ReplicaSet
garantiza un conjunto de pods escalables y autorregenerables. No hay límite en el
número de pods que puede contener un ReplicaSet.

[ 206 ]
Capítulo 10

Especificación de ReplicaSet
De manera similar a lo que hemos aprendido acerca de los pods, Kubernetes también
nos permite definir y crear de forma imperativa o declarativa un ReplicaSet. Como
el enfoque declarativo es, de lejos, el recomendado en la mayoría de los casos, vamos
a centrarnos en este enfoque. Esta es una especificación de ejemplo de un ReplicaSet
de Kubernetes:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: rs-web
spec:
selector:
matchLabels:
app: web
replicas: 3
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80

Se parece mucho a la especificación del pod que explicamos antes. Vamos a centrarnos
en las diferencias entonces. Primero, en la línea 2, tenemos el tipo, que era Pod y ahora es
ReplicaSet. Luego, en las líneas 6-8, tenemos un selector que determina los pods que
formarán parte del ReplicaSet. En este caso, son todos los pods que tienen una etiqueta
app con el valor web. Después, en la línea 9, definimos cuántas réplicas del pod queremos
ejecutar: tres, en este caso. Por último, tenemos la sección template, que primero define
los metadatos y después la sección spec, que define los contenedores que se ejecutan
dentro del pod. En nuestro caso, tenemos un solo contenedor que utiliza la imagen
nginx:alpine y el puerto 80.

Los elementos realmente importantes son el número de réplicas y el selector que


especifica el conjunto de pods regulados por el ReplicaSet.

En nuestra carpeta ch10, tenemos un archivo llamado replicaset.yaml que


contiene exactamente la especificación anterior. Vamos a usar este archivo para crear
el ReplicaSet:
$ kubectl create -f replicaset.yaml
replicaset "rs-web" created

[ 207 ]
Orquestación de aplicaciones en contenedores con Kubernetes

Si mostramos todos los ReplicaSets del cluster, obtenemos esto (rs es el nombre
abreviado de replicaset):
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
rs-web 3 3 3 51s

En el resultado anterior, podemos ver que tenemos un solo ReplicaSet llamado


rs-web cuyo estado deseado es tres (pods). El estado actual también muestra tres
pods y todos ellos están listos. También podemos mostrar todos los pods del sistema
y obtenemos esto:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
rs-web-6qzld 1/1 Running 0 4m
rs-web-frj2m 1/1 Running 0 4m
rs-web-zd2kt 1/1 Running 0 4m

Aquí, vemos nuestros tres pods esperados. Para los nombres de los pods se utiliza el
nombre del ReplicaSet con un identificador único anexado a cada pod. En la columna
READY, vemos cuántos contenedores se han definido en el pod y cuántos de ellos están
listos. En nuestro caso, tenemos un solo contenedor por pod y todos ellos están listos. Por
tanto, el estado general del pod es Running (en ejecución). También vemos cuántas veces
se tuvo que reiniciar cada pod. En nuestro caso, aún no tenemos ningún reinicio.

Reparación automática
Ahora vamos a probar los poderes mágicos de la reparación automática del ReplicaSet,
que permiten destruir uno de los pods automáticamente, y vamos a observar lo que
ocurre. Vamos a eliminar el primer pod de la lista anterior:
$ kubectl delete po/rs-web-6qzld
pod "rs-web-6qzld" deleted

A continuación, mostramos de nuevo todos los pods. Esperamos ver solo dos pods,
¿verdad? Respuesta incorrecta:

Lista de pods después de haber destruido un pod del ReplicaSet

[ 208 ]
Capítulo 10

Evidentemente, el segundo pod de la lista se ha vuelto crear, como podemos ver en la


columna AGE. Estamos viendo la reparación automáticamente en acción. Veamos lo que
descubrimos si describimos el ReplicaSet:

Descripción del ReplicaSet

Y, efectivamente, encontramos una entrada bajo Events que nos dice que el ReplicaSet
ha creado el nuevo pod rs-web-q6cr7.

Implementación de Kubernetes
Kubernetes se toma muy en serio el principio de responsabilidad única. Todos los objetos
de Kubernetes se han diseñado para que hagan una y solo una cosa. Y se han diseñado
para hacer esta única cosa muy bien. En este sentido, tenemos que entender los conceptos
de ReplicaSet e implementación de Kubernetes. El ReplicaSet, como hemos aprendido,
es responsable de conseguir y conciliar el estado deseado del servicio de una aplicación.
Esto significa que el ReplicaSet administra un conjunto de pods.

[ 209 ]
Orquestación de aplicaciones en contenedores con Kubernetes

La implementación aumenta la eficacia del ReplicaSet proporcionando las funciones de


actualización gradual y reversión. En Docker Swarm, el servicio swarm incorporaría la
funcionalidad del ReplicaSet y la implementación. En este sentido, SwarmKit es mucho
más monolítico que Kubernetes. En el diagrama siguiente se muestra la relación de una
implementación con un ReplicaSet:

Implementación de Kubernetes

En el diagrama anterior, el ReplicaSet define y regula un conjunto de pods idénticos.


Las características principales del ReplicaSet son que es autorregenerable, escalable
y que siempre hace lo posible para alcanzar el estado deseado. La implementación de
Kubernetes, a su vez, añade la funcionalidad de actualización gradual y reversión. En
este sentido, una implementación es realmente un objeto contenedor de un ReplicaSet.

Aprenderemos más acerca de las actualizaciones graduales y las reversiones en el


siguiente capítulo de este libro.

Servicio de Kubernetes
En el momento en que empezamos a trabajar con aplicaciones que constan de más de
un servicio, necesitamos una función de detección de servicios. En el siguiente diagrama
se ilustra este problema:

Detección de servicios

[ 210 ]
Capítulo 10

En este diagrama, tenemos un servicio API web que necesita acceder a otros tres
servicios: pagos, envíos y pedidos. La API web no debería en ningún momento tener
que preocuparse acerca de cómo y dónde encontrar esos tres servicios. En el código de la
API, solo queremos usar el nombre del servicio con el que queremos comunicarnos y su
número de puerto. Un ejemplo sería la URL http://payments:3000 que se utiliza para
tener acceso a una instancia del servicio de pagos.

En Kubernetes, el servicio de aplicación de pagos está representado por un ReplicaSet de


pods. Debido a la naturaleza de los sistemas altamente distribuidos, no podemos suponer
que los pods tienen puntos de conexión estables. Un pod puede existir y dejar de existir
en un abrir y cerrar de ojos. Pero eso es un problema si necesitamos tener acceso al
servicio de aplicación correspondiente de un cliente interno o externo. Si no podemos
confiar en que los puntos de conexión de los pods sean estables, ¿qué más podemos hacer?

Ahí es donde los servicios de Kubernetes entran en juego. Su finalidad es proporcionar


puntos de conexión estables a los ReplicaSets o implementaciones, como se muestra
a continuación:

Servicio de Kubernetes que proporciona puntos de conexión estables a los clientes

En el diagrama anterior, en el centro, vemos un servicio de Kubernetes. Proporciona


una dirección IP fiable para todo el clúster denominada también IP virtual (VIP), así
como un puerto fiable que es único en todo el clúster. Los pods que utiliza el servicio
de Kubernetes se determinan por el selector definido en la especificación del servicio.
Los selectores siempre se basan en etiquetas. Cada objeto de Kubernetes puede tener
cero o muchas etiquetas asignadas. En nuestro caso, el selector es app=web; es decir,
se utilizan todos los pods que tienen una etiqueta app con un valor de web.
[ 211 ]
Orquestación de aplicaciones en contenedores con Kubernetes

Enrutamiento basado en contexto


A menudo, queremos configurar el enrutamiento basado en contexto para nuestro
clúster de Kubernetes. Kubernetes nos ofrece varias formas de hacerlo. La manera
preferida y más escalable en este momento es utilizar un IngressController (controlador
de entrada) para esta tarea. El siguiente diagrama intenta ilustrar cómo funciona este
controlador de entrada:

Enrutamiento basado en contexto mediante un controlador de entrada de Kubernetes

En este diagrama, podemos ver cómo funciona el enrutamiento basado en contexto


(o capa 7) cuando se utiliza un controlador de entrada, como Nginx. Aquí, tenemos
una implementación de un servicio de aplicación llamado web. Todos los pods de este
servicio de aplicación tienen una etiqueta app=web. Luego tenemos un servicio de
Kubernetes llamado web que proporciona un punto de conexión estable a esos pods.
El servicio tiene una IP (virtual) 52.14.0.13 y expone el puerto 30044. Es decir, si una
solicitud llega a cualquier nodo del clúster de Kubernetes para el nombre web y el puerto
30044, se reenvía a este servicio. El servicio entonces envía la solicitud equilibrando
la carga a uno de los pods.

[ 212 ]
Capítulo 10

Todo esto está muy bien, pero ¿cómo se enruta una solicitud de entrada desde un
cliente a la URL http[s]://example.com/web a nuestro servicio web? En primer lugar,
tenemos que definir el enrutamiento desde una solicitud basada en contexto a una
solicitud <nombre de servicio>/<puerto> correspondiente. Esto se hace mediante
un objeto Ingress:

1. En el objeto Ingress, definimos el host y la ruta como el origen y el nombre


(servicio) y el puerto como destino. Cuando el servidor de la API de Kubernetes
crea este objeto Ingress, un proceso que se ejecuta como sidecar en el
IngressController detecta este cambio.
2. Modifica el archivo de configuración del proxy inverso de Nginx.
3. Al añadir la nueva ruta, se le pide a Nginx que vuelva a cargar su configuración
y, de ese modo, podrá enrutar correctamente cualquier solicitud entrante
a http[s]://example.com/web.

Resumen
En este capítulo, hemos aprendido los fundamentos de Kubernetes. Hemos visto una
descripción de su arquitectura y una introducción a los recursos principales utilizados
para definir y ejecutar aplicaciones en un clúster de Kubernetes. También hemos
explicado la compatibilidad con Minikube y Kubernetes de Docker para Mac y Windows.

En el siguiente capítulo, vamos a implementar una aplicación en un clúster de


Kubernetes. A continuación, actualizaremos uno de los servicios de esta aplicación
utilizando una estrategia de "tiempo de inactividad cero". Por último, instrumentaremos
los servicios de aplicación que se ejecutan en Kubernetes con datos confidenciales,
mediante secretos. ¡Sigue leyendo!

Preguntas
Responde las siguientes preguntas para evaluar lo que has aprendido en este capítulo:

1. Explica con algunas frases cortas qué papel desempeña un nodo maestro
de Kubernetes.
2. Enumera los elementos que deben estar presentes en cada nodo (de trabajo)
de Kubernetes.
3. Verdadero o falso: No podemos ejecutar contenedores individuales en un clúster
de Kubernetes.
4. Explica la razón por la que los contenedores de un pod pueden usar localhost
para comunicarse entre sí.
5. ¿Cuál es la finalidad del contenedor llamado pause en un pod?

[ 213 ]
Orquestación de aplicaciones en contenedores con Kubernetes

6. Roberto te dice: nuestra aplicación está formada por tres imágenes de Docker:
web, inventory y db. Puesto que podemos ejecutar varios contenedores en
un pod de Kubernetes, vamos a implementar todos los servicios de nuestra
aplicación en un único pod. Indica tres o cuatro razones que expliquen por qué
esta es una mala idea.
7. Explica con tus propias palabras por qué necesitamos ReplicaSets de Kubernetes.
8. ¿En qué circunstancias necesitamos implementaciones de Kubernetes?
9. Indica al menos tres tipos de servicios de Kubernetes y explica para qué sirven
y en qué se diferencian.

Lectura adicional
Aquí tienes una lista de artículos con información más detallada sobre algunos de los
temas explicados en este capítulo (pueden estar en inglés):

• El algoritmo de consenso Raft (https://raft.github.io/)


• Docker Compose y Kubernetes con Docker para el escritorio (https://dockr.
ly/2G8Iqb9)

[ 214 ]
Implementación,
actualización y protección
de una aplicación
con Kubernetes
En el último capítulo, hemos aprendido los aspectos básicos del organizador de
contenedores, Kubernetes. Vimos un resumen general de la arquitectura de Kubernetes
y conocimos los objetos más importantes usados por Kubernetes para definir y gestionar
una aplicación en contenedor.
En este capítulo, aprenderemos a implementar, actualizar y escalar aplicaciones
en un clúster de Kubernetes. También explicaremos cómo conseguir implementaciones
sin tiempo de inactividad para permitir las actualizaciones y las reversiones de las
aplicaciones críticas. Por último, en este capítulo, presentaremos los secretos de
Kubernetes como forma de configurar servicios y proteger los datos confidenciales.
En el capítulo, abordaremos los siguientes temas:

• Implementación de una primera aplicación


• Implementaciones sin tiempo de inactividad
• Secretos de Kubernetes
Una vez que leas este capítulo, podrás:
• Implementar una aplicación multiservicio en un clúster de Kubernetes
• Actualizar un servicio de aplicación ejecutándose en Kubernetes sin provocar
interrupciones
• Definir secretos en un clúster de Kubernetes
• Configurar un servicio de aplicación para usar secretos de Kubernetes

[ 215 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Requisitos técnicos
En este capítulo, vamos a usar Minikube en nuestro ordenador local. Consulta el Capítulo
2, Configuración de un entorno de trabajo para obtener más información sobre cómo instalar
y usar Minikube.

El código para este capítulo puede encontrarse en la subcarpeta ch11 de la carpeta


labs. Comprueba que has clonado el repositorio GitHub en https://github.com/
appswithdockerandkubernetes/labs, como se describe en el Capítulo 2, Configuración
de un entorno de trabajo.

En tu Terminal, desplázate hasta la carpeta labs/ch11.

Implementación de una primera


aplicación
Vamos a coger nuestra aplicación pets, que hemos explicado en el Capítulo 8, Docker
Compose y vamos a implementarla en un clúster de Kubernetes. Nuestro clúster será
Minikube que, como sabemos, es un clúster de nodo único. Pero, desde la perspectiva de la
implementación, no importa lo grande que sea el clúster y dónde esté situado: en el cloud,
el centro de datos de la empresa o tu estación de trabajo personal.

Implementación de un componente web


Hay que recordar que nuestra aplicación está formada por dos servicios de aplicación:
el componente web basado en Node.js y la base de datos de respaldo PostgreSQL.
En el capítulo anterior, aprendimos que era necesario definir un objeto Deployment de
Kubernetes para cada servicio de aplicación que quisiéramos desplegar. Vamos a hacerlo
primero para el componente web. Como siempre hemos hecho en esta guía, vamos
a escoger la forma declarativa de definir nuestros objetos. Aquí tenemos el YAML que
define un objeto Deployment para el componente web:

[ 216 ]
Capítulo 11

Definición de la implementación de Kubernetes para el componente web

La definición de implementación anterior puede encontrarse en el archivo web-


deployment.yaml de la carpeta de prueba ch11. Las líneas de código son las siguientes:

• En la línea 4: definimos el nombre de nuestro objeto Deployment como web


• En la línea 6: declaramos que queremos tener una sola instancia del componente
web en ejecución
• De la línea 8 a la 10: definimos los pods que formarán parte de nuestra
implementación, principalmente aquellos que tienen las etiquetas app and
service con los valores pets y web, respectivamente
• En la línea 11: en la plantilla para los pods que empieza en la línea 11, definimos
que cada pod tendrá dos etiquetas app y service aplicadas
• A partir de la línea 17: definimos el contenedor único que se ejecutará en el pod.
La imagen del contenedor es nuestra imagen appswithdockerandkubernetes/
ch08-web:1.0 ya conocida y el nombre del contenedor será web
• Puertos: por último, declararemos que el contenedor expone el puerto 3000 para
el tráfico de tipo TCP

[ 217 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Asegúrate de que has definido el contexto de kubectl como Minikube.


Consulta el Capítulo 2, Configuración de un entorno de trabajo, para obtener
información sobre cómo hacerlo.

Podemos implementar este objeto Deployment usando kubectl:


$ kubectl create -f web-deployment.yaml

Podemos hacer doble clic en la implementación que se ha creado de nuevo usando


nuestra CLI de Kubernetes y deberíamos ver el siguiente resultado:

Enumeración de todos los recursos que se ejecutan en Minikube

En el momento de redactar este documento, parece haber un error


en Minikube o kubectl que muestra algunos recursos dos veces
cuando se usa el comando kubectl get all. Puedes obviar el
resultado duplicado.

En el resultado anterior, vemos que Kubernetes ha creado tres objetos: la


implementación, un ReplicaSet y un pod único (recuerda que hemos especificado que
solo queremos una réplica). El estado actual corresponde al estado deseado para los tres
objetos, por lo que todo es correcto hasta ahora.

Ahora, tenemos que exponer el servicio web al público. Para ello, tenemos que definir
un objeto Service de Kubernetes del tipo NodePort. Aquí tenemos la definición,
que puede encontrarse en el archivo web-service.yaml de la carpeta labs ch11:

[ 218 ]
Capítulo 11

Definición del objeto Service para nuestro componente web

Las líneas de código anteriores son las siguientes:

• En la línea 4: establecemos el nombre de este objeto Service en web.


• En la línea 6: definimos el tipo del objeto Service que estamos usando. Dado
que el componente web tiene que ser accesible desde fuera del clúster, no puede
ser un objeto Service del tipo ClusterIP, sino que debe ser del tipo NodePort
o LoadBalancer. Ya hemos explicado los distintos tipos de servicios de
Kubernetes en el capítulo anterior, por lo que no nos detendremos mucho en ello.
En nuestro ejemplo, estamos usando un tipo de servicio NodePort.
• En las líneas 8 y 9: especificamos que queremos exponer el puerto 3000 para
su acceso a través del protocolo TCP. Kubernetes puede asignar el puerto del
contenedor 3000 a un puerto del host libre en el intervalo de 30.000 a 32.768.
El puerto que escoja Kubernetes en última instancia puede determinarse usando
el comando kubectl get service o kubectl describe para el servicio una
vez creado.
• Desde la línea 10 a la 12: definimos los criterios del filtro para los pods para los
cuales este servicio será un punto de conexión estable. En este caso, serán todos
los pods que tienen las etiquetas app y service con los valores pets y web,
respectivamente.

Una vez que tenemos esta especificación para el objeto Service, podemos crearlo
usando kubectl:
$ kubectl create -f web-service.yaml

[ 219 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Podemos enumerar todos los servicios para ver el resultado del comando anterior:

El objeto Service creado para el componente web

En el resultado, vemos que se ha creado un servicio llamado web. Se ha asignado una IP


de clúster única 10.103.113.40 a este servicio, y el puerto del contenedor 3000 se
ha publicado en el puerto 30125 en todos los nodos del clúster.

Si queremos probar esta implementación, primero tenemos que averiguar qué dirección
IP tiene Minikube y después usar esta dirección IP para acceder a nuestro servicio web.
Este es el comando que podemos usar para hacerlo:
$ IP=$(minikube ip)
$ curl -4 $IP:30125/
Pets Demo Application

Bien, la respuesta es Pets Demo Application, que es lo que esperábamos. El servicio web
está ejecutándose en el clúster de Kubernetes. A continuación, queremos implementar
la base de datos.

Implementación de la base de datos


Una base de datos es un componente con estado y debe tratarse de forma diferente
que los componentes sin estado, como nuestro componente web. Ya hemos explicado
la diferencia entre los componentes con y sin estado en una arquitectura de aplicaciones
distribuidas en el Capítulo 6, Arquitectura de aplicaciones distribuidas, y en el
Capítulo 9, Orquestadores.

Kubernetes tiene un tipo especial de objeto ReplicaSet definido para los componentes
con estado. El objeto se denomina StatefulSet. Ahora vamos a usar este tipo
de objeto para implementar nuestra base de datos. La definición puede encontrarse
en el archivo labs/ch11/db-stateful-set.yaml. Los detalles son los siguientes:

[ 220 ]
Capítulo 11

Un StatefulSet para el componente de base de datos

Puede parecer un poco intimidante pero no lo es. Es un poco más largo que la definición
de la implementación para el componente web porque también tenemos que definir un
volumen donde la base de datos PostgreSQL pueda almacenar los datos. La definición
de solicitud de volumen está en las líneas 25-33. Queremos crear un volumen con el
nombre pets-data y un tamaño máximo igual a 100 MB. En las líneas 22-24, utilizamos
este volumen y lo montamos en el contenedor en /var/lib/postgresql/data
donde PostgreSQL lo espera. En la línea 21, también declaramos que PostgreSQL está
escuchando en el puerto 5432.
Como siempre, utilizamos kubectl para implementar el StatefulSet:
$ kubectl create -f db-stateful-set.yaml

[ 221 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Si ahora enumeramos todos los recursos del clúster, veremos los objetos adicionales
creados:

StatefulSet y su pod

Vemos que se ha creado un StatefulSet y un pod. Para ambos, el estado actual


se corresponde con el estado deseado y, por consiguiente, el estado del sistema es
correcto. Pero esto no significa que el componente web pueda acceder a la base de datos
en ese momento. La detección del servicio no funcionaría. Recuerda que el componente
web quiere acceder al servicio db bajo el nombre db.

Para que la detección del servicio funcione dentro del clúster, tenemos que definir
también un objeto Service de Kubernetes para el componente de la base de datos.
Dado que la base de datos solo debe estar accesible desde dentro del clúster, el tipo de
objeto Service que necesitamos es ClusterIP. Aquí está la especificación que podemos
encontrar en el archivo labs/ch11/db-service.yaml:

[ 222 ]
Capítulo 11

Definición del objeto Service de Kubernetes para la base de datos

El componente de la base de datos estará representado por este objeto Service y podrá
buscarse por el nombre db, que es el nombre del servicio, según se define en la línea 4.
El componente de la base de datos no tiene que ser accesible públicamente, por lo que
hemos decidido usar un objeto Service del tipo ClusterIP. El selector en las líneas
10-12 define que este servicio representa un punto de conexión estable para todos
los pods que tienen las etiquetas correspondientes definidas, es decir, app: pets
y service: db.

Ahora vamos a implementar este servicio con el siguiente comando:


$ kubectl create -f db-service.yaml

[ 223 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Y ahora ya podemos probar la aplicación. Esta vez podemos usar el navegador para ver
las divertidas imágenes del gatito:

Prueba de la aplicación pets ejecutándose en Kubernetes

192.168.99.100 es la dirección IP de mi Minikube. Comprueba


tu dirección usando el comando minikube ip. El número de
puerto 30125 es el número que Kubernetes ha seleccionado
automáticamente para mi objeto Service de la web. Sustituye este
número por el puerto que Kubernetes haya asignado a tu servicio.
Puedes obtener el número usando el comando kubectl get
services.

Ahora ya hemos implementado correctamente la aplicación pets en Minikube, que es


un clúster de Kubernetes de nodo único. Hemos tenido que definir cuatro artefactos
para ello, que son los siguientes:

• Un objeto Deployment y un objeto Service para el componente web


• Un StatefulSet y un objeto Service para el componente de la base de datos

Para eliminar la aplicación del clúster, podemos usar el siguiente script:


kubectl delete svc/web
kubectl delete deploy/web
kubectl delete svc/db
kubectl delete statefulset/db

[ 224 ]
Capítulo 11

Optimización de la implementación
Hasta ahora, hemos creado cuatro artefactos que tenían que implementarse en el clúster.
Y se trata de una aplicación muy sencilla, formada por dos componentes. Imagina
que tuviéramos una aplicación mucho más compleja. Sería una pesadilla realizar
su mantenimiento. Por suerte, tenemos varias opciones para poder simplificar la
implementación. El método que vamos a explicar ahora es la posibilidad de definir todos
los componentes que forman una aplicación en Kubernetes en un único archivo.

Otras soluciones que no vamos a explicar en este documento podrían ser la inclusión
de un gestor de paquetes, como Helm.

Si tenemos una aplicación formada por muchos objetos de Kubernetes como los objetos
Deployment y Service , podemos guardarlos todos en un único archivo y separar las
definiciones individuales de los objetos con tres guiones. Por ejemplo, si quisiéramos
tener la definición de los objetos Deployment y Service para el componente web en
un único archivo, el resultado sería este:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
selector:
matchLabels:
app: pets
service: web
template:
metadata:
labels:
app: pets
service: web
spec:
containers:
- image: appswithdockerandkubernetes/ch08-web:1.0
name: web
ports:
- containerPort: 3000
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: NodePort
ports:

[ 225 ]
Implementación, actualización y protección de una aplicación con Kubernetes

- port: 3000
protocol: TCP
selector:
app: pets
service: web

Hemos recopilado las cuatro definiciones de objeto para la aplicación pets en el archivo
labs/ch11/pets.yaml y ahora podemos implementar la aplicación con una sola pasada:

Utilización de un script único para implementar la aplicación pets

De forma similar, hemos creado un script, labs/ch11/remove-pets.sh, para eliminar


todos los artefactos de la aplicación pets del clúster de Kubernetes:

Eliminación de pets del clúster de Kubernetes

Hemos cogido nuestra aplicación pets que explicamos en el Capítulo 8, Docker Compose,
y hemos definido todos los objetos de Kubernetes que son necesarios para implementar
esta aplicación en un clúster de Kubernetes. En cada paso, hemos comprobado que
obteníamos el resultado esperado y, una vez que todos los artefactos existían en el
clúster, hemos mostrado la aplicación en ejecución.

Implementaciones sin tiempo de inactividad


En un entorno de misión crítica, es importante que la aplicación siempre esté activa
y en funcionamiento. En la actualidad no podemos permitirnos interrupciones del
servicio. Kubernetes nos ofrece varias formas para conseguirlo. Una actualización
de una aplicación en el clúster que no provoca interrupciones se conoce como una
implementación sin interrupciones. En este capítulo, explicaremos dos formas de
conseguirlo. Son las siguientes:

• Actualizaciones graduales.
• Implementaciones blue-green

Empecemos hablando de las actualizaciones graduales.


[ 226 ]
Capítulo 11

Actualizaciones graduales.
En el capítulo anterior, hemos aprendido que el objeto Deployment de Kubernetes se
diferencia del objeto ReplicaSet en que añade actualizaciones graduales y reversiones
encima de la funcionalidad de este último. Vamos a usar nuestro componente web para
demostrarlo. Evidentemente, tendremos que modificar el manifiesto o la descripción de
la implementación para el componente web.

Utilizaremos la misma definición de implementación que en la sección anterior, con


una diferencia importante: tendremos cinco réplicas del componente web en ejecución.
La siguiente definición también puede encontrarse en el archivo labs/ch11/web-
deploy-rolling-v1.yaml:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: web
spec:
replicas: 5
selector:
matchLabels:
app: pets
service: web
template:
metadata:
labels:
app: pets
service: web
spec:
containers:
- image: appswithdockerandkubernetes/ch08-web:1.0
name: web
ports:
- containerPort: 3000
protocol: TCP

Ahora podemos crear esta implementación normalmente y, al mismo tiempo, el servicio


que hace que nuestro componente sea accesible:
$ kubectl create -f web-deploy-rolling-v1.yaml
$ kubectl create -f web-service.yaml

Después de implementar los pods y el servicio, podemos probar nuestro componente


web con el siguiente comando:
$ PORT=$(kubectl get svc/web -o yaml | grep nodePort | cut -d' ' -f5)
$ IP=$(minikube ip)
$ curl -4 ${IP}:${PORT}/
Pets Demo Application

[ 227 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Como podemos ver, la aplicación está activa y en funcionamiento, y nos devuelve


el mensaje esperado, Pets Demo Application.

Ahora los desarrolladores han creado una nueva versión, 2.0, del componente web. El
código de la nueva versión del componente web puede encontrarse en la carpeta labs/
ch11/web/src, y el único cambio lo encontramos en la línea 12 del archivo server.js:

Cambio de código para la versión 2.0 del componente web

Los desarrolladores han creado la nueva imagen de la forma siguiente:


$ docker image build -t appswithdockerandkubernetes/ch11-web:2.0 web

Posteriormente, han publicado la imagen en Docker Hub:


$ docker image push appswithdockerandkubernetes/ch11-web:2.0

Ahora queremos actualizar la imagen usada por nuestros pods que forman parte del
objeto Deployment web. Podemos hacerlo usando el comando set image de kubectl:
$ kubectl set image deployment/web \
web=appswithdockerandkubernetes/ch11-web:2.0

Si ahora volvemos a probar la aplicación, obtendremos la confirmación de que la


actualización se ha realizado correctamente:
curl -4 ${IP}:${PORT}/
Pets Demo Application v2

Entonces, ¿cómo sabemos si se ha producido alguna interrupción durante esta actualización?


¿La actualización se ha realizado realmente de forma gradual? ¿Qué significa que la actualización
sea gradual? Vamos a investigar un poco. Primero, podemos obtener una confirmación
de Kubernetes de que la implementación se ha realizado correctamente usando el
comando rollout status:
$ kubectl rollout status deploy/web
deployment "web" successfully rolled out

Si describimos la web de implementación con kubectl describe deploy/web,


obtendremos la siguiente lista de eventos al final del resultado:

[ 228 ]
Capítulo 11

Lista de eventos encontrados en el resultado de la descripción de implementación del componente web

El primer evento nos dice que cuando creamos la implementación, se creó


un ReplicaSet web-769b88f67 con cinco réplicas. A continuación, el comando
de actualización y el segundo evento de la lista nos dicen que se ha creado un nuevo
ReplicaSet llamado web-55cdf67cd con una réplica inicial. De esta forma, en ese
momento específico existían seis pods en el sistema: los cinco pods iniciales y un pod
con la nueva versión. Pero, dado que el estado deseado del objeto Deployment dice
que queremos solamente cinco réplicas, Kubernetes reduce el ReplicaSet anterior a
cuatro instancias, que podemos ver en el tercer evento. De nuevo, el nuevo ReplicaSet
se aumenta a dos instancias y, posteriormente, el ReplicaSet anterior se reduce a tres
instancias, y así sucesivamente hasta que tengamos cinco instancias nuevas y todas las
instancias anteriores se hayan retirado. Aunque no podemos ver el momento preciso
en que ha ocurrido, el orden de los eventos nos dice que la actualización de todo el
nodo se ha producido de forma gradual.

Durante un breve periodo de tiempo, algunas de las llamadas al servicio web habrían
tenido una respuesta de la versión anterior del componente y algunas llamadas habrían
recibido una respuesta de la nueva versión del componente. Pero en ningún caso
el servicio habrá dejado de funcionar.

También podemos enumerar los objetos Recordset del clúster y obtendremos


la confirmación que he explicado en la sección anterior:

Enumeración de todos los objetos Recordset del clúster

[ 229 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Hemos visto que el nuevo recordset tiene cinco instancias en ejecución y que el anterior
se ha escalado a cero instancias. El motivo por el que el objeto Recordset anterior sigue
existiendo es que Kubernetes nos ofrece la posibilidad de revertir la actualización y,
en ese caso, reutilizar el Recordset.

Para revertir la actualización de la imagen si se ha encontrado algún error no detectado


en el nuevo código, podemos usar el comando rollout undo:
$ kubectl rollout undo deploy/web
deployment "web"
$ curl -4 ${IP}:${PORT}/
Pets Demo Application

También he enumerado el comando de prueba usando curl en el fragmento de código


anterior para verificar que se ha producido la reversión. Si enumeramos los recordsets,
veremos el siguiente resultado:

Enumeración de los objetos RecordSet después de la reversión

Esto confirma que el objeto RecordSet (web-769b88f67) anterior se ha reutilizado


y que el nuevo se ha escalado a cero instancias.

Sin embargo, algunas veces no podemos o no queremos tolerar el estado combinado de


una versión anterior coexistiendo con una nueva versión. Queremos aplicar la estrategia
de todo o nada. Aquí entran en juego las implementaciones blue-green, que explicaremos
a continuación.

Implementación blue-green
Si queremos aplicar una implementación de estilo blue-green para nuestro componente
web de la aplicación pets, podemos hacerlo usando etiquetas de forma creativa.
Recordemos primero cómo funcionan las implementaciones blue-green. A continuación
podrás encontrar instrucciones paso a paso:

1. Implementa una primera versión del componente web como blue. Para ello,
etiquetaremos los pods con una etiqueta color: blue.
2. Implementa un servicio de Kubernetes para estos pods con la etiqueta, color:
blue en la sección del selector.
3. Ahora podemos implementar una versión 2 del componente web, pero esta vez
los pods tendrán una etiqueta, color: green.

[ 230 ]
Capítulo 11

4. Ahora podemos probar la versión green del servicio que funciona de la forma
prevista.
5. Ahora vamos a cambiar el tráfico de blue a green actualizando el servicio
de Kubernetes para el componente web. Modificamos el selector para que
use la etiqueta color: green.

Ahora vamos a definir el objeto Deployment para la versión 1, blue:

Especificación de la implementación blue para el componente web

La definición anterior puede encontrarse en el archivo labs/ch11/web-deploy-blue.


yaml. Observa la línea 4 donde definimos el nombre de la implementación como web-blue
para distinguirla de la siguiente implementación web-green. Observa también que hemos
añadido la etiqueta color: blue en las líneas 11 y 17. Todo lo demás queda igual que antes.

[ 231 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Ahora vamos a definir el objeto Service para el componente web. Será el mismo que
hemos usado antes con un cambio menor, como podrás ver en la siguiente captura
de pantalla:

Servicio de Kubernetes para el componente web que admite las implementaciones blue–green

La única diferencia en la definición del objeto service que hemos usado en este capítulo
es la línea 13, que añade la etiqueta color: blue al selector. Podemos encontrar
la definición anterior en el archivo labs/ch11/web-svc-blue-green.yaml.

A continuación, podemos implementar la versión blue del componente web con


el siguiente comando:
$ kubectl create -f web-deploy-blue.yaml
$ kubectl create -f web-svc-blue-green.yaml

Una vez que el servicio esté activo y ejecutándose, podemos determinar su dirección
IP y el número de puerto, y hacer una prueba:
$ PORT=$(kubectl get svc/web -o yaml | grep nodePort | cut -d' ' -f5)
$ IP=$(minikube ip)
$ curl -4 ${IP}:${PORT}/
Pets Demo Application

Como podíamos esperar, obtenemos la respuesta Pets Demo Application.

Ahora podemos implementar la versión green del componente web. La definición de su


objeto Deployment puede encontrarse en el archivo labs/ch11/web-deploy-green.
yaml y tiene el siguiente aspecto:

[ 232 ]
Capítulo 11

Especificación de la implementación green para el componente web

Las líneas interesantes son las siguientes:

• Línea 4: con el nombre web-green para distinguirlo de web-blue y permitir


la instalación paralela
• Líneas 11 y 17: tiene el color green
• Línea 20: ahora se usa la versión 2.0 de la imagen

Ahora estamos listos para implementar esta versión green del servicio, y debería
ejecutarse de forma separada del servicio blue:
$ kubectl create -f web-deploy-green.yaml

[ 233 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Podemos comprobar que las dos implementaciones coexisten:

Visualización de la lista de objetos Deployment ejecutándose en el clúster

Como esperábamos, tenemos los dos objetos blue y green ejecutándose.


Podemos comprobar que blue sigue siendo el servicio activo:
$ curl -4 ${IP}:${PORT}/
Pets Demo Application

Ahora viene la parte interesante. Podemos cambiar el tráfico de blue a green editando
el servicio existente para el componente web. Ejecutamos el siguiente comando:
$ kubectl edit svc/web

Cambiamos el valor del color de etiqueta de blue a green. A continuación, guardamos


los cambios y salimos del editor. La CLI de Kubernetes actualizará automáticamente
el servicio. Cuando volvemos a solicitar el servicio web, obtenemos esto:
$ curl -4 ${IP}:${PORT}/
Pets Demo Application v2

Esto confirma que el tráfico ha cambiado a la versión green del componente web
(observa el v2 al final de la respuesta al comando curl).

Si nos damos cuenta de que algo ha ido mal con nuestra implementación green y la
nueva versión tiene un defecto, podemos volver a cambiar fácilmente a la versión blue
editando el servicio web de nuevo y cambiando el valor de la etiqueta color de green
a blue. Esta reversión es inmediata y siempre debería funcionar. Ahora podemos
eliminar la implementación green errónea y reparar el componente. Cuando hayamos
corregido el problema, podemos implementar la versión green otra vez.

Una vez que la versión green del componente esté ejecutándose como se esperaba
y funcione correctamente, podemos cancelar la versión blue:
$ kubectl delete deploy/web-blue

Cuando estemos listos para implementar una nueva versión, 3.0, pasará a ser la versión
blue. Actualizamos el archivo labs/ch11/web-deploy-blue.yaml en consonancia
y lo implementamos. A continuación, cambiamos el servicio web de green a blue,
y así sucesivamente.

Hemos demostrado, con nuestro componente web de la aplicación pets, cómo conseguir
una implementación blue-green en un clúster de Kubernetes.

[ 234 ]
Capítulo 11

Secretos de kubernetes
Algunas veces, los servicios que queremos ejecutar en el clúster de Kubernetes tienen que
usar datos confidenciales como contraseñas, claves secretas de API o certificados, entre
otros. Queremos asegurarnos de que esta información confidencial solo puede ser vista
por el servicio autorizado o dedicado. El resto de los servicios que se ejecutan en el
clúster no deberían tener acceso a estos datos.

Por este motivo, se han creado los secretos de Kubernetes. Un secreto es un par
de clave-valor donde la clave es el nombre único del secreto y el valor es la información
confidencial. Los secretos se guardan en etcd. Kubernetes puede configurarse de forma
que esos secretos se cifren cuando estén en reposo, es decir, en etcd, y en tránsito,
es decir, cuando los secretos pasan por el canal de un nodo maestro a los nodos de trabajo
donde se ejecutan los pods del servicio que están usando este secreto.

Definición manual de secretos


Podemos crear un secreto declarativamente de la misma forma que creamos cualquier
otro objeto en Kubernetes. Aquí tenemos el YAML de este secreto:
apiVersion: v1
kind: Secret
metadata:
name: pets-secret
type: Opaque
data:
username: am9obi5kb2UK
password: c0VjcmV0LXBhc1N3MHJECg==

La definición anterior puede encontrarse en el archivo labs/ch11/pets-secret.


yaml. Ahora seguramente te estés preguntando qué son estos valores. ¿Son valores
reales (no cifrados)? No, no lo son. Y tampoco son valores cifrados realmente; son
valores codificados en base64. Por lo tanto, no son verdaderamente seguros, ya que los
valores codificados en base64 pueden revertirse fácilmente a valores de texto sin cifrar.
¿Cómo obtengo estos valores? Es sencillo:

Creación de valores codificados en base64 para el secreto

[ 235 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Ahora podemos crear el secreto y describirlo:

Creación y descripción de secretos de Kubernetes

En la descripción del secreto, los valores están ocultos y solo se muestra su longitud.
¿Entonces los secretos están seguros? No, no lo están. Podemos descodificar fácilmente
este secreto usando el comando kubectl get:

Secretos de Kubernetes descodificados

Como podemos ver en la captura de pantalla anterior, tenemos nuestros valores de


secretos originales. Y podemos descodificarlos:
$ echo "c0VjcmV0LXBhc1N3MHJECg==" | base64 --decode
sEcret-pasSw0rD

Por lo tanto, la conclusión es que este método de crear un Kubernetes no debe usarse en
ningún otro entorno que no sea el de desarrollo, donde tratamos con datos no confidenciales.
En el resto de entornos, necesitamos una forma mejor de gestionar los secretos.

[ 236 ]
Capítulo 11

Creación de secretos con kubectl


Una forma mucho más segura de definir secretos es usar kubectl. En primer lugar,
creamos los archivos que contengan los valores de secretos codificados en base-64 de
forma similar a como lo hicimos en la sección anterior, pero esta vez guardamos los
valores en archivos temporales:
$ echo "sue-hunter" | base64 > username.txt
$ echo "123abc456def" | base64 > password.txt

Ahora podemos usar kubectl para crear un secreto a partir de esos archivos
de la forma siguiente:
$ kubectl create secret generic pets-secret-prod \
--from-file=./username.txt \
--from-file=./password.txt
secret "pets-secret-prod" created

Posteriormente, el secreto puede usarse de la misma forma que los secretos creados
manualmente.

¿Por qué este método es más seguro que el otro? Bueno, lo primero de todo es que no hay un
YAML que defina un secreto y lo guarde en un sistema de control de versiones de código
fuente, como GitHub, al que muchas personas tienen acceso para poder ver y descodificar
los secretos. Solo el administrador autorizado para conocer los secretos verá estos valores
y los usará para crear directamente los secretos en el clúster (producción). El propio clúster
está protegido por control de acceso basado en roles para que las personas no autorizadas
no puedan acceder ni descodificar los secretos definidos en el clúster.

Ahora vamos a ver cómo podemos usar los secretos que hemos definido.

Utilización de secretos en un pod


Imaginemos que queremos crear un objeto Deployment donde el componente web
utilice nuestro secreto llamado pets-secret que hemos explicado en la sección
anterior. Utilizamos el siguiente comando para crear el secreto en el clúster:
$ kubectl create -f pets-secret.yaml

[ 237 ]
Implementación, actualización y protección de una aplicación con Kubernetes

En el archivo labs/ch11/web-deploy-secret.yaml, podemos encontrar la definición


del objeto Deployment. Hemos tenido que añadir la parte que empieza en la línea
23 a la definición original del objeto Deployment:

Objeto Deployment para el componente con un secreto

[ 238 ]
Capítulo 11

En las líneas 27-30 definimos un volumen llamado secrets desde nuestro secreto
pets-secret. A continuación, usamos este volumen en el contenedor, como se describe
en las líneas 23-26. Montamos los secretos en el sistema de archivos del contenedor
en /etc/secrets y montamos el volumen en modo de solo lectura. De esta forma,
los valores secret estarán disponibles para el contenedor como archivos en dicha
carpeta. Los nombres de los archivos se corresponderán con los nombres de las claves,
y el contenido de los archivos serán los valores de las claves correspondientes. Los
valores se proporcionarán en un formato no cifrado a la aplicación que se ejecuta dentro
del contenedor.

En nuestro caso, tenemos las claves username y password en el secreto, y tendremos


dos archivos llamados username y password, en la carpeta /etc/secrets del
sistema de archivos del contenedor. El archivo username debe contener el valor john.
doe y la contraseña del archivo contendrá el valor sEcret-pasSw0rD. Aquí tenemos
la confirmación:

Confirmación de que los secretos están disponibles dentro del contenedor

En la línea 1 del resultado anterior, ejecutamos el comando exec en el contenedor donde


se ejecuta el componente web. Entonces, en las líneas 2-5, enumeramos los archivos de la
carpeta /etc/secrets y, finalmente, en las líneas 6-8 mostramos el contenido de los dos
archivos que, sorprendentemente, muestran los valores del secreto en texto visible.

Este mecanismo de usar los secretos tiene una compatibilidad inversa, ya que cualquier
aplicación escrita en cualquier lenguaje puede leer archivos simples. Incluso una
aplicación COBOL antigua puede leer archivos de texto visible del sistema de archivos.

No obstante, algunas veces las aplicaciones esperan que los secretos están disponibles
en las variables de entorno. Veamos lo que Kubernetes nos ofrece en este caso.

[ 239 ]
Implementación, actualización y protección de una aplicación con Kubernetes

Valores secretos en variables de entorno


Imaginemos que nuestro componente web espera el nombre de usuario en la variable
de entorno, PETS_USERNAME y la contraseña en PETS_PASSWORD. En ese caso, podemos
modificar nuestra implementación YAML para que tenga el siguiente aspecto:

Implementación con asignación de valores secretos a variables de entorno

En las líneas 23-33, definimos las dos variables de entorno, PETS_USERNAME y PETS_
PASSWORD, y asignamos el par de clave-valor correspondiente de pets-secret
a las mismas.

[ 240 ]
Capítulo 11

Ahora ya no necesitamos un volumen, sino que asignamos directamente las claves


individuales a nuestro pets-secret en las variables de entorno correspondientes
que son válidas dentro del contenedor. La siguiente secuencia de comandos muestra
que los valores de los secretos están disponibles dentro del contenedor en las variables
de entorno respectivas:

Valores secretos asignados a variables de entorno

En esta sección, hemos explicado cómo definir secretos en un clúster kubernetes y cómo
usar esos secretos en contenedores que se ejecutan como parte de los pods de una
implementación. Hemos mostrado dos variantes de cómo asignar secretos dentro
de un contenedor: la primera usando archivos y la segunda usando variables de entorno.

Resumen
En este capítulo hemos aprendido a implementar una aplicación en un clúster
de Kubernetes y cómo configurar un enrutamiento de nivel de aplicación para esta
aplicación. Además, hemos aprendido nuevas formas de actualizar los servicios de
la aplicación que se ejecutan en un clúster de Kubernetes sin provocar interrupciones.
Por último, hemos usado los secretos para proporcionar información confidencial
a servicios de aplicación que se ejecutan en el clúster.

En el siguiente y último capítulo, vamos a aprender a implementar, ejecutar, controlar


y depurar una aplicación de ejemplo en contenedor en el cloud usando la solución
Azure Kubernetes Service (AKS) de Microsoft. ¡Sigue leyendo!

Preguntas
Para evaluar el progreso de tu aprendizaje, responde a las siguientes preguntas:

1. Tenemos una aplicación formada por dos servicios, el primero es una API web
y el segundo es una base de datos como Mongo. Queremos implementar esta
aplicación en un clúster de Kubernetes. En unas pocas frases breves, explica qué
harías.
2. Describe con tus propias palabras en unas pocas frases los componentes que
necesitas para establecer un enrutamiento de capa 7 (o nivel de aplicación)
para tu aplicación.
3. Enumera los principales pasos para desplegar implementaciones blue–green para
un servicio de aplicación simple. Evita dar demasiados detalles innecesarios.

[ 241 ]
Implementación, actualización y protección de una aplicación con Kubernetes

4. Nombra tres o cuatro tipos de información que proporcionarías a un servicio


de aplicación a través de los secretos de Kubernetes.
5. Nombra los orígenes que acepta Kubernetes durante la creación de un secreto.

Lectura adicional
Aquí encontrarás algunos enlaces que proporcionan información adicional sobre los
temas que hemos tratado en este capítulo (pueden estar en inglés):
• Cómo realizar una actualización gradual en https://bit.ly/2o2okEQ
• Implementación blue–green en https://bit.ly/2r2IxNJ
• Secretos en Kubernetes en https://bit.ly/2C6hMZF

[ 242 ]
Ejecución de una aplicación
en contenedor desde el cloud
En el capítulo anterior, hemos aprendido cómo implementar una aplicación multiservicio
en un clúster de Kubernetes. Configuramos una ruta de nivel de aplicación para esa
aplicación y actualizamos sus servicios usando una estrategia de cero interrupciones.
Por último, proporcionamos datos confidenciales a los servicios de ejecución usando los
secretos de Kubernetes.

En este capítulo, te explicaremos cómo implementar una aplicación en contenedor


compleja en un clúster de Kubernetes alojado en Microsoft Azure. Para ello, usaremos
el servicio Azure Kubernetes Service (AKS). AKS gestiona completamente el clúster
de Kubernetes por nosotros para que podamos centrarnos en implementar, ejecutar y,
si es necesario, actualizar nuestra aplicación.

Estos son los temas que trataremos en este capítulo:

• Creación de un clúster de Kubernetes completamente gestionado en Azure


• Envío de imágenes de Docker al registro de contenedores de Azure
• Despliegue de la aplicación en el clúster de Kubernetes
• Escalado de la aplicación Pets
• Supervisión del clúster y la aplicación
• Actualización de la aplicación con cero interrupciones
• Actualización de Kubernetes
• Depuración de una aplicación mientras se ejecuta en AKS
• Limpieza

[ 243 ]
Ejecución de una aplicación en contenedor desde el cloud

Después de leer este capítulo, serás capaz de:

• Aprovisionar un clúster de Kubernetes completamente gestionado en AKS


• Implementar una aplicación basada en varios contenedores en tu clúster
• Supervisar el estado de tu clúster de Kubernetes y la aplicación donde se ejecuta
• Ejecutar una actualización gradual de tu aplicación en AKS
• Depurar interactivamente una aplicación Node.js que se ejecuta en un clúster
de Kubernetes en AKS

Requisitos técnicos
Vamos a utilizar AKS alojado en Microsoft Azure y para ello es necesario tener una
cuenta en Azure. Si no tienes una cuenta, puedes pedir una cuenta de prueba gratuita en
https://azure.microsoft.com/en-us/services/kubernetes-service/.

Para acceder a Azure, también utilizaremos la CLI de Azure. Asegúrate de que tienes
la última versión instalada en tu ordenador. Como este es un libro sobre contenedores,
no instalaremos de forma nativa la CLI, sino que usaremos una versión en contenedor
de ella.

También utilizaremos las carpetas ch12 y ch12-dev-spaces para nuestro


repositorio labs de GitHub. El enlace a los archivos de código puede encontrarse en
https://github.com/appswithdockerandkubernetes/labs/tree/master.

Creación de un clúster de Kubernetes


completamente gestionado en Azure
Hasta ahora, hemos usado un clúster de Kubernetes local de nodo 1 para implementar
nuestras aplicaciones. Esta es una buena solución para el desarrollo, la realización de
pruebas y la depuración en una aplicación en contenedor destinada a ejecutarse en
Kubernetes. Sin embargo, a la larga querremos implementar la aplicación terminada en
un entorno de producción. Existen varias formas de hacerlo. Podríamos, por ejemplo,
autoalojar un clúster de Kubernetes escalable y altamente disponible, pero esa no es tarea
fácil. En este capítulo vamos a delegar el trabajo duro de aprovisionar y alojar un clúster
de Kubernetes escalable de forma masiva y altamente disponible en Microsoft y vamos
a usar su AKS ejecutándose en Azure.

[ 244 ]
Capítulo 12

A continuación, implementaremos nuestra aplicación Pets en este clúster. Una vez


implementada, querremos supervisar el comportamiento del clúster y de nuestra
aplicación en el mismo. Después, escalaremos la parte web de la aplicación y
actualizaremos la parte web y activaremos una actualización gradual que genere cero
interrupciones.

Por último, explicaremos cómo podemos usar Dev Spaces en AKS para depurar
remotamente una aplicación, como nuestro frontend Node.js, mientras se ejecuta
en el cloud.

AKS puede usarse de tres formas distintas. La primera y más importantes es usar una
IU web gráfica proporcionada por el portal de Azure. Las siguientes dos formas están
pensadas para la automatización:

• Podemos usar la CLI de Azure para aprovisionar y gestionar el clúster


de Kubernetes combinado con kubectl para implementar, escalar o actualizar
la aplicación en el clúster, y muchas otras cosas.
• Podemos utilizar herramientas como Terraform y las plantillas de recursos
de Azure para hacer lo mismo.

En este capítulo, nos centraremos en la CLI de Azure y kubectl.

Ejecución de la CLI de Azure


Como hemos mencionado anteriormente, necesitaremos la CLI de Azure para poder
acceder a Microsoft Azure. Es importante que utilicemos una versión nueva o la más
reciente de la CLI para poder acceder a todos los recursos relacionados con AKS.
No ejecutaremos la CLI de forma nativa en nuestros ordenadores esta vez, sino que
utilizaremos una versión en contenedor de ella. Microsoft ha creado un contenedor
con la CLI instalada. En nuestro caso, queremos usar la CLI de Docker (docker) y de
Kubernetes (kubectl) desde el interior del contenedor, pero esas herramientas no están
instaladas de forma predeterminada. Por ello, he creado un Dockerfile que puede usarse
para crear nuevas imágenes de Docker basadas en las imágenes de Microsoft que tendrán
las herramientas instaladas. Sigue estos pasos:

1. Abre una nueva ventana del Terminal y ve hasta la subcarpeta ch12 dentro
de la carpeta labs:
cd ~/labs/ch12

2. Ejecuta el siguiente comando para crear nuestro contenedor de CLI de Azure


personalizado:
docker image build -t custom-azure-cli:v1 azure-cli

[ 245 ]
Ejecución de una aplicación en contenedor desde el cloud

3. Después de crear la imagen, podemos ejecutar el siguiente comando para


ejecutar interactivamente un contenedor con la versión más reciente de la CLI
de Azure, Docker y Kubernetes:
docker container run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd):/src \
custom-azure-cli:v1

Observa cómo montamos el socket de Docker en el contenedor para que podamos


ejecutar cualquier comando de Docker desde el contenedor. También vamos a montar
la carpeta actual en la carpeta /src dentro del contenedor para tener acceso al código
fuente de este capítulo.

Después de iniciar la instancia del contenedor, aparecerá el siguiente símbolo del sistema:
bash-4.4#

Podemos probar el contenedor usando los siguientes comandos:


bash-4.4# docker version
Client:
Version: 18.06.0-ce
API version: 1.38
Go version: go1.10.3
Git commit: 0ffa825
Built: Wed Jul 18 19:04:39 2018
OS/Arch: linux/amd64
Experimental: false
Server:
Engine:
Version: 18.06.0-ce
API version: 1.38 (minimum version 1.12)
Go version: go1.10.3
Git commit: 0ffa825
Built: Wed Jul 18 19:13:46 2018
OS/Arch: linux/amd64
Experimental: true

El resultado que obtenemos al ejecutar el comando kubectl es el siguiente:


bash-4.4# kubectl version
Client Version: version.Info{Major:”1”, Minor:”11”,
GitVersion:”v1.11.2”, GitCommit:”bb9ffb1654d4a729bb4cec18ff088eac
c153c239”, GitTreeState:”clean”, BuildDate:”2018-08-07T23:17:28Z”,
GoVersion:”go1.10.3”, Compiler:”gc”, Platform:”linux/amd64”}
Server Version: version.Info{Major:”1”, Minor:”9”,
GitVersion:”v1.9.9”, GitCommit:”57729ea3d9a1b75f3fc7bbbadc597ba70
7d47c8a”, GitTreeState:”clean”, BuildDate:”2018-06-29T01:07:01Z”,
GoVersion:”go1.9.3”, Compiler:”gc”, Platform:”linux/amd64”}

[ 246 ]
Capítulo 12

El resultado muestra que estamos ejecutando Docker 18.06.0-ce y Kubernetes


v1.11.2 en el cliente y v1.9.9 en el servidor.

Para poder usar la CLI de Azure para trabajar con nuestra cuenta en Microsoft Azure,
primero tenemos que iniciar sesión en la cuenta. Para ello, introduce lo siguiente
en el símbolo del sistema bash-4.4#:
az login

Aparecerá un mensaje similar a este:


Para iniciar sesión, utiliza un explorador web para abrir la página
https://microsoft.com/devicelogin e introduce el código CMNB2TSND
para autenticarte.

Cuando te hayas autenticado correctamente, deberías ver una respuesta similar


a la siguiente en la ventana del Terminal:

[
{
“cloudName”: “AzureCloud”,
“id”: “186760ad-9152-4499-b317-xxxxxxxxxxxx”,
“isDefault”: true,
“name”: “xxxxxxxxx”,
“state”: “Enabled”,
“tenantId”: “f5e90e29-00df-4ea6-b8a4-xxxxxxxxxxxx”,
“user”: {
“name”: “[email protected]”,
“type”: “user”
}
}
]

Ahora ya podemos empezar a trabajar con Azure en general y AKS específicamente.

Grupos de recursos de Azure


En Azure, un concepto importante son los grupos de recursos. Un grupo de recursos
es un contenedor para todo tipo de recursos del cloud que pertenece a la misma unidad
lógica. En nuestro caso, todos los elementos que conforman el clúster de Kubernetes
que vamos a aprovisionar en AKS pertenecen al mismo grupo de recursos. Empecemos
creando dicho grupo. Utilizaremos la CLI de Azure para ello. Llamaremos pets-group
al grupo y lo colocaremos en la región westeurope. Utiliza el siguiente comando para
crear el grupo de recursos:
az group create --name pets-group --location westeurope

[ 247 ]
Ejecución de una aplicación en contenedor desde el cloud

La respuesta de este comando tiene un aspecto similar a este:


{
“id”: “/subscriptions/186760ad-9152-4499-b317-xxxxxxxxxxxx/
resourceGroups/pets-group”,
“location”: “westeurope”,
“managedBy”: null,
“name”: “pets-group”,
“properties”: {
“provisioningState”: “Succeeded”
},

Una vez que hemos creado un grupo de servicios, tenemos que crear la entidad principal
del servicio para que nuestro clúster de AKS pueda interactuar con los otros recursos
de Azure. Para ello, utiliza el siguiente comando:
az ad sp create-for-rbac \
--name pets-principal \
--password adminadmin \
--skip-assignment

Esto genera un resultado similar a este:


{
“appId”: “a1a2bdbc-ba07-49bd-ae77-fb8b6948869d”,
“displayName”: “azure-cli-2018-08-27-19-26-20”,
“name”: “http://pets-principal”,
“password”: “adminadmin”,
“tenant”: “f5e90e29-00df-4ea6-b8a4-ce8553f10be7”
}

En el resultado anterior, appId (ID de aplicación) y password (contraseña) son


importantes. Debes anotarlos para poder consultarlos para ejecutar otros comandos
que se explican en las secciones siguientes.

Aprovisionamiento del clúster de Kubernetes


Por fin ya estamos listos para aprovisionar nuestro clúster de Kubernetes en AKS.
Podemos usar el siguiente comando para hacerlo:
az aks create \
--resource-group pets-group \
--name pets-cluster \
--node-count 1 \
--generate-ssh-keys \
--service-principal <appId> \
--client-secret <password>

[ 248 ]
Capítulo 12

No olvides sustituir los marcadores <appId> y <password> por los valores que has
anotado después de crear la entidad principal del servicio. También debes tener en
cuenta que, de momento, hemos creado un clúster de Kubernetes con un único nodo
de trabajo. Posteriormente, podemos escalar este clúster usando la CLI de Azure.

La respuesta al comando anterior sería como esta:


SSH key files ‘/root/.ssh/id_rsa’ and ‘/root/.ssh/id_rsa.pub’ have
been generated under ~/.ssh to allow SSH access to the VM. If using
machines without permanent storage like Azure Cloud Shell without
an attached file share, back up your keys to a safe location
- Running ..

El aprovisionamiento completo solo tarda unos minutos (como se indica en el estado -


Running..).

Mientras estamos esperando a que se complete el comando, podemos abrir una nueva
ventana del navegador e ir hasta el portal de Azure en https://portal.azure.com
e iniciar sesión en nuestra cuenta. Después de autenticarnos correctamente, podemos
ir a la opción Grupos de recursos y veremos el grupo de recursos pets-group en la
lista de grupos de recursos. Si hacemos clic en este grupo, veremos pets-cluster
enumerado como recurso en el grupo:

Clúster de Kubernetes en AKS

Después de crear el clúster, veremos el resultado final en la ventana del Terminal


de esta forma:
{ “aadProfile”: null,
“addonProfiles”: null,
“agentPoolProfiles”: [
{

[ 249 ]
Ejecución de una aplicación en contenedor desde el cloud

“count”: 1,
“maxPods”: 110,
“name”: “nodepool1”,
“osDiskSizeGb”: null,
“osType”: “Linux”,
“storageProfile”: “ManagedDisks”,
“vmSize”: “Standard_DS1_v2”,
“vnetSubnetId”: null
}
],
“dnsPrefix”: “pets-clust-pets-group-186760”,
“enableRbac”: true,
“fqdn”: “pets-clust-pets-group-186760-d706beb4.hcp.westeurope.
azmk8s.io”, “id”: “/subscriptions/186760ad-9152-4499-b317-
c9bff441fb9d/resourcegroups/pets-group/providers/Microsoft.
ContainerService/managedClusters/pets-cluster”,
“kubernetesVersion”: “1.9.9”,
“linuxProfile”: {
“adminUsername”: “azureuser”,
“ssh”: {
“publicKeys”: [
{
“keyData”: “ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMp
2BFCRUo7v1ktVQa57ep7zLg7HEjsRQAkb7UnovDXrLg1nBuzMslHZY3mJ5ulxU00
YWeUuxObeHjRh+ZJHc4+xKaDV8M6GmuHjD8HJnw5tsCbV8w/A+5oUOECaeJn5sQMCkmS
DovmDQZchAjLjVHQLSTiEqjLYmjjqYmhqYpO2vRsnZXpelRrlmfNWoSV5J3L7/
hayI2fg35X/H4xnx1sm403O9pwyEKYYBFfNzCXigNnqyBvxOqwURZUW/caIpTqAhS6
K+D1xPa2w7y1A5qcZS++SnJOHCHyRKZ3UQ4BVZTSejBhxYTr5/dgJE+LEvLk2i
YUo4kUmbxDSVssnWJ”
}
]
}
},
“location”: “westeurope”,
“name”: “pets-cluster”,
“networkProfile”: {
“dnsServiceIp”: “10.0.0.10”,
“dockerBridgeCidr”: “172.17.0.1/16”,
“networkPlugin”: “kubenet”,
“networkPolicy”: null,
“podCidr”: “10.244.0.0/16”,
“serviceCidr”: “10.0.0.0/16”
},
“nodeResourceGroup”: “MC_pets-group_pets-cluster_westeurope”,
“provisioningState”: “Succeeded”,
“resourceGroup”: “pets-group”,
“servicePrincipalProfile”: {
“clientId”: “a1a2bdbc-ba07-49bd-ae77-fb8b6948869d”,
“secret”: null
},

[ 250 ]
Capítulo 12

“tags”: null,
“type”: “Microsoft.ContainerService/ManagedClusters”
}

Para acceder al clúster, tenemos que configurar kubectl usando el siguiente comando:
az aks get-credentials --resource-group pets-group --name pets-cluster

Si es correcto, esta acción generará la siguiente respuesta en el Terminal:


Merged “pets-cluster” as current context in /root/.kube/config

Ahora intentaremos usar kubectl para obtener todos los nodos del clúster:
kubectl get nodes

Esto genera un resultado similar a este:


NAME STATUS ROLES AGE VERSION
aks-nodepool1-54489083-0 Ready agent 13m v1.9.9

Vemos que nuestro clúster está formado por un nodo de trabajo cuya versión de
Kubernetes es 1.9.9 y que ha estado en funcionamiento durante 13 minutos. Es posible
que hayas notado que la versión de Kubernetes está un poco anticuada. La versión más
reciente disponible en AKS en el momento de escribir este documento es 1.11.2. Esto
es correcto y nos permite mostrar cómo podemos actualizar el clúster a una versión más
reciente de Kubernetes.

Envío de imágenes de Docker al registro


de contenedores de Azure (ACR)
Al implementar una aplicación en contenedor en un clúster de Kubernetes alojado
en Azure, resulta útil tener las imágenes subyacentes de Docker almacenadas en el
mismo contexto de seguridad y cerca del clúster. Por ello, ACR es una buena opción.
Antes de implementar nuestra aplicación en el clúster, vamos a enviar las imágenes
de Docker necesarias al ACR.

[ 251 ]
Ejecución de una aplicación en contenedor desde el cloud

Creación de un ACR
Una vez más, usaremos la CLI de Azure para crear un registro para nuestras imágenes
de Docker en Azure. El registro también pertenecerá al grupo de recursos pet-group
que hemos creado en la sección Grupos de recursos de Azure de este capítulo. Podemos
usar el siguiente comando para crear un registro con el nombre <registry-name>:
az acr create --resource-group pets-group --name <registry-name>
--sku Basic

Ten en cuenta que el nombre del registro es <registry-name> y tiene que ser único
en Azure. Por ello, tienes que crear un nombre único y no podrás usar el mismo nombre
que el autor de esta publicación. Por este motivo, aquí estamos utilizando un marcador
de posición <registry-name> en lugar de un nombre real. En mi caso, he dado el
siguiente nombre al registro: gnsPetsRegistry.

Ten en cuenta que el comando anterior tarda un poco en completarse. La respuesta


del comando será similar a la siguiente:
{
“adminUserEnabled”: false,
“creationDate”: “2018-08-27T19:57:01.434521+00:00”,
“id”: “/subscriptions/186760ad-9152-4499-b317-xxxxxxxxxxxx/
resourceGroups/pets-group/providers/Microsoft.ContainerRegistry/
registries/gnsPetsRegistry”,
“location”: “westeurope”,
“loginServer”: “gnspetsregistry.azurecr.io”,
“name”: “gnsPetsRegistry”,
“provisioningState”: “Succeeded”,
“resourceGroup”: “pets-group”,
“sku”: {
“name”: “Basic”,
“tier”: “Basic”
},
“status”: null,
“storageAccount”: null,
“tags”: {},
“type”: “Microsoft.ContainerRegistry/registries”
}

Observa la entrada loginServer del resultado anterior. El valor de esa clave


corresponde a la URL que tenemos que usar como prefijo en las imágenes
de Docker cuando las etiquetamos para poder enviarlas a ACR.

Después de crear el registro, podemos iniciar sesión usando este comando:


az acr login --name <registry-name>

[ 252 ]
Capítulo 12

La respuesta debería ser:


Login Succeeded

En el portal de Azure, deberíamos ver nuestro registro enumerado como recurso


adicional de pets-group:

Registro Pets de ACR

Ahora ya estamos listos para enviar las imágenes a nuestro ACR.

Etiquetado y envío de imágenes de Docker


En los capítulos anteriores, hemos creado las imágenes de Docker que usaremos en este
capítulo. Pero para poder enviar esas imágenes a ACR, tenemos que volver a etiquetar las
imágenes con el prefijo de URL correcto para el ACR. Para obtener este prefijo (o el servidor
de inicio de sesión de ACR, como también se llama, tal y como vimos en el resultado del
comando que usamos para crear el registro), podemos usar el siguiente comando:
az acr list --resource-group pets-group --query “[].
{acrLoginServer:loginServer}” --output table

El comando anterior debería generar un resultado como este:


AcrLoginServer
--------------------------
gnspetsregistry.azurecr.io

[ 253 ]
Ejecución de una aplicación en contenedor desde el cloud

Bien, ahora que ya tenemos un registro en ACR, podemos etiquetar nuestras imágenes
de Docker en consonancia. En mi caso, usando la URL gnspetsregistry.azurecr.io,
el comando para la imagen pets-web:v1 Docker tiene el siguiente aspecto:
docker image tag pets-web:v1 gnspetsregistry.azurecr.io/pets-web:v1

Después de hacer esto, podemos enviar la imagen con el siguiente comando:


docker image push gnspetsregistry.azurecr.io/pets-web:v1

Si todo es correcto, deberíamos ver algo similar a esto:


The push refers to repository [gnspetsregistry.azurecr.io/pets-web]
9d5c9e1e5f97: Pushed
39f3a72e04a3: Pushed
3177c088200b: Pushed
5f896b8130b3: Pushed
287ef32bfa90: Pushed
ce291010afac: Pushed
73046094a9b8: Mounted from alpine
v1: digest: sha256:9a32931874f4fdf5... size: 1783
Ahora haremos lo mismo para la imagen pets-db:v1 de Docker:
docker image tag pets-db:v1 gnspetsregistry.azurecr.io/pets-db:v1
docker image push gnspetsregistry.azurecr.io/pets-db:v1

Configuración de la entidad principal del


servicio
Como paso final, tenemos que dar a la entidad principal del servicio AKS que hemos
creado anteriormente en este capítulo los derechos necesarios para acceder a las imágenes
y enviarlas desde nuestro registro de contenedores. Primero, obtenemos el ID del recurso
de ACR (<acrId>) usando el siguiente comando:
az acr show --resource-group pets-group \
--query “id” \
--output tsv \
--name <registry-name>

Esto debería generar un resultado similar a este:


/subscriptions/186760ad-9152-4499-b317-xxxxxxxxxxxx/resourceGroups/
pets-group/providers/Microsoft.ContainerRegistry/registries/
gnsPetsRegistry

Con esta información, podemos asignar a la entidad principal del servicio identificado
por <appId> el acceso de lectura necesario al registro de contenedores identificado por
<acrId>:

az role assignment create --assignee <appId> --scope <acrId>


--role Reader
[ 254 ]
Capítulo 12

Aquí, <appId> es el ID de aplicación de la entidad principal que hemos guardado


anteriormente y <acrId> es el ID del recurso de ACR. El resultado sería similar a este:
{
“canDelegate”: null,
“id”: “/subscriptions/.../roleAssignments/1b7c2a63-c4d3-41a9-a1bc-
bd9d65966f43”,
“name”: “1b7c2a63-c4d3-41a9-a1bc-bd9d65966f43”,
“principalId”: “ab5fe519-3982-4aac-95e0-761c242aa61b”,
“resourceGroup”: “pets-group”,
“roleDefinitionId”: “/subscriptions/.../roleDefinitions/acdd72a7-
3385-48ef-bd42-f606fba81ae7”,
“scope”: “/subscriptions/.../registries/gnspetsregistry”,
“type”: “Microsoft.Authorization/roleAssignments”
}

Implementar una aplicación en el clúster


de Kubernetes
Después de enviar correctamente nuestras imágenes de Docker a ACR, estaremos listos
para implementar la aplicación en el clúster de Kubernetes en AKS. Para ello, podemos
usar el archivo de manifiesto, pets.yaml, que forma parte del código fuente en la
carpeta ch12. Abre este archivo con tu editor y modifica el archivo para que se adapte
a tu entorno. Cambia la URL del repositorio incluida delante de las imágenes de Docker
por tu URL específica.

En el siguiente fragmento de código, vemos el valor predeterminado, gnsPetsRegistry,


que tienes que sustituir por tu valor <nombre de registro>:
containers:
- name: web
image: gnsPetsRegistry.azurecr.io/pets-web:v1

Guarda los cambios. Después, usa kubectl para aplicar el manifiesto:


kubectl apply -f /src/pets.yaml

El resultado del comando anterior debería ser similar a este:


deployment.apps/db created
service/db created
deployment.apps/web created
service/web created

[ 255 ]
Ejecución de una aplicación en contenedor desde el cloud

Estamos creando un servicio de tipo LoadBalancer para el frontend de la aplicación


Pets que ha recibido una dirección IP publicada asignada por AKS. Esta operación
tarda unos minutos y podemos ver el estado con el siguiente comando:
kubectl get service web --watch

Inicialmente, el resultado será similar a este (consulta el resultado pending en la columna


EXTERNAL-IP):

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE


web LoadBalancer 10.0.49.36 <pending> 80:31035/TCP 41s

Una vez que se ha completado la implementación, <pending> se reemplazará por la


dirección IP pública y podremos usar nuestra aplicación Pets:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web LoadBalancer 10.0.49.36 40.114.197.39 80:31035/TCP 2m

Abre una ventana del navegador en http://<IP address>/pet y deberías ver la


imagen de un gatito:

Aplicación Pets ejecutándose en Kubernetes en AKS

[ 256 ]
Capítulo 12

Con esta acción, hemos implementado correctamente una aplicación compleja en nuestro
clúster de Kubernetes en AKS.

Escalado de la aplicación Pets


Hay dos formas de escalar nuestra aplicación Pets:

• La primera es escalar el número de instancias de la aplicación


• La segunda es escalar el número de nodos de trabajo del clúster

El segundo método, naturalmente, solo tiene sentido si escalamos el número de


instancias de aplicación al mismo tiempo, de forma que las instancias adicionales
aprovechen los recursos de los nodos del nuevo clúster que hemos añadido.

Escalar el número de instancias de aplicación


Escalar el número de instancias que un servicio de aplicación está ejecutando es sencillo
y podemos usar kubectl para hacerlo. Vamos a imaginarnos que nuestra aplicación Pets
tiene tanto éxito que tenemos que aumentar el número de instancias web a tres mientras
que la base de datos pueda seguir gestionando el tráfico entrante con una única instancia.
Primero veamos lo que están ejecutando los pods actualmente en nuestro clúster:
bash-4.4# kubectl get pods
NAME READY STATUS RESTARTS AGE
db-6746668f6c-wdscl 1/1 Running 0 2h
web-798745b679-8kh2j 1/1 Running 0 2h

Y podemos ver que tenemos un pod para cada uno de los dos servicios, web y db, en
funcionamiento. Hasta ahora no se ha reiniciado ningún pod. Ahora escalaremos nuestra
implementación web a tres instancias:
bash-4.4# kubectl scale --replicas=3 deployment/web
deployment.extensions/web scaled

Y este es el resultado de la operación de escalado:


bash-4.4# kubectl get pods
NAME READY STATUS RESTARTS AGE
db-6746668f6c-wdscl 1/1 Running 0 2h
web-74dbc994bc-6f7qh 1/1 Running 0 14s
web-74dbc994bc-l99bh 1/1 Running 0 1m
web-74dbc994bc-rz8vs 1/1 Running 0 14s

[ 257 ]
Ejecución de una aplicación en contenedor desde el cloud

En el navegador que está ejecutando la aplicación Pets, actualiza la vista unas pocas
veces y observa cómo el ID de la instancia del contenedor va cambiando con frecuencia.
Se trata del equilibrador de carga de Kubernetes en acción, distribuyendo las llamadas
a las distintas instancias web.

La mejor forma que obtener las distintas instancias del servicio web
es abriendo varias ventanas del navegador ocultas.

Escalar el número de nodos del clúster


Hasta ahora, solo hemos trabajado con un nodo de trabajo del clúster. Podemos
usar la CLI de Azure para aumentar el número de nodos y ofrecer mayor potencia
de computación a nuestra aplicación. Vamos a escalar nuestro clúster a tres nodos:
az aks scale --resource-group=pets-group --name=pets-cluster
--node-count 3

Esta acción tardará unos minutos y, cuando se complete, deberíamos ver una respuesta
en el Terminal similar a esta:
{
“aadProfile”: null,
“addonProfiles”: null,
“agentPoolProfiles”: [
{
“count”: 3,
“maxPods”: 110,
“name”: “nodepool1”,
“osDiskSizeGb”: null,
“osType”: “Linux”,
“storageProfile”: “ManagedDisks”,
“vmSize”: “Standard_DS1_v2”,
“vnetSubnetId”: null
}
],
“dnsPrefix”: “pets-clust-pets-group-186760”,
“enableRbac”: true,
“fqdn”: “pets-clust-pets-group-186760-d706beb4.hcp.westeurope.azmk8s.
io”,
“id”: “/subscriptions/186760ad-9152-4499-b317-xxxxxxxxxxxx/
resourcegroups/pets-group/providers/Microsoft.ContainerService/
managedClusters/pets-cluster”,
“kubernetesVersion”: “1.9.9”,
“linuxProfile”: {
“adminUsername”: “azureuser”,
“ssh”: {

[ 258 ]
Capítulo 12

“publicKeys”: [
{
“keyData”: “ssh-rsa AAAAB3NzaC...”
}
]
}
},
“location”: “westeurope”,
“name”: “pets-cluster”,
“networkProfile”: {
“dnsServiceIp”: “10.0.0.10”,
“dockerBridgeCidr”: “172.17.0.1/16”,
“networkPlugin”: “kubenet”,
“networkPolicy”: null,
“podCidr”: “10.244.0.0/16”,
“serviceCidr”: “10.0.0.0/16”
},
“nodeResourceGroup”: “MC_pets-group_pets-cluster_westeurope”,
“provisioningState”: “Succeeded”,
“resourceGroup”: “pets-group”,
“servicePrincipalProfile”: {
“clientId”: “a1a2bdbc-ba07-49bd-ae77-xxxxxxxxxxxx”,
“secret”: null
},
“tags”: null,
“type”: “Microsoft.ContainerService/ManagedClusters”
}

Podemos verificar el progreso del aprovisionamiento de los nodos de trabajo usando


el siguiente comando:
bash-4.4# kubectl get nodes --watch
aks-nodepool1-54489083-0 Ready agent 1d v1.9.9
aks-nodepool1-54489083-2 NotReady agent 0s v1.9.9
...
aks-nodepool1-54489083-0 Ready agent 1d v1.9.9
aks-nodepool1-54489083-2 Ready agent 2m v1.9.9
aks-nodepool1-54489083-1 Ready agent 20s v1.9.9

[ 259 ]
Ejecución de una aplicación en contenedor desde el cloud

Naturalmente, la adición de nodos de trabajo adicionales no redistribuye


automáticamente las instancias del servicio para nuestra aplicación. Para
conseguirlo, tenemos que escalar, por ejemplo, el servicio web otra vez:
bash-4.4# kubectl scale --replicas=5 deployment/web
deployment.extensions/web scaled

Ahora podemos usar el resultado completo para ver en qué nodos han aterrizado
los pods:
bash-4.4# kubectl get pods --output=’wide’
NAME READY STATUS RESTARTS AGE IP
NODE ...
db-6746668f6c-wdscl 1/1 Running 0 3h 10.244.0.24
aks-nodepool1-54489083-0 ...
web-59545bb958-2v4zp 1/1 Running 0 2m 10.244.1.3
aks-nodepool1-54489083-2 ...
web-59545bb958-7mpfx 1/1 Running 0 31m 10.244.0.31
aks-nodepool1-54489083-0 ...
web-59545bb958-9mc6m 1/1 Running 0 2m 10.244.1.2
aks-nodepool1-54489083-2 ...
web-59545bb958-sbctd 1/1 Running 0 35m 10.244.0.29
aks-nodepool1-54489083-0 ...
web-59545bb958-tvthv 1/1 Running 0 31m 10.244.0.30
aks-nodepool1-54489083-0 ...

Podemos ver que tres pods están en el primero y que dos (los adicionales) se han
implementado en el tercer nodo.

Supervisión del clúster y la aplicación


En esta sección, mostraremos tres elementos diferentes de nuestro clúster de Kubernetes.
Haremos lo siguiente:

• Supervisar el estado del contenedor


• Ver los registros de los nodos maestros de Kubernetes
• Ver los registros de los kublets instalados en cada nodo de trabajo

[ 260 ]
Capítulo 12

Creación de un espacio de trabajo de análisis


de registros
Para guardar los datos de registro y supervisión generados por nuestro clúster, tenemos
un espacio de trabajo de análisis de registros. Utilizaremos la GUI del portal del Azure
para crear uno en nuestro grupo de recursos pets-group con los pasos siguientes:

1. En el portal, haz clic en la opción +Crear un recurso.


2. Selecciona la opción Análisis de registros y después Crear. Introduce los datos
requeridos, similares a los mostrados en la captura de pantalla siguiente:

Creación de un espacio de trabajo de análisis de registros

[ 261 ]
Ejecución de una aplicación en contenedor desde el cloud

Después de crear el espacio de trabajo, deberíamos ver un resumen de nuestro


grupo de recursos pets-group:

Espacio de trabajo de análisis de registros en el grupo de recursos pets-group

3. Haz clic en la entrada pets-oms-workspace para ver los detalles del espacio
de trabajo:

Vista detallada del espacio de trabajo de análisis de registros pets-oms-workspace

Anota el campo <ID de espacio de trabajo> que acabas de crear para usarlo
en la sección siguiente.

[ 262 ]
Capítulo 12

Supervisión del estado del contenedor


Ahora usaremos la CLI de Azure para poder supervisar nuestro pets-cluster.
Podríamos haberlo hecho cuando creamos el clúster, pero también podemos hacerlo
posteriormente.

Volvemos a utilizar la CLI de Azure para poder supervisar nuestro clúster. Utiliza
el siguiente comando para hacerlo y asegúrate de cambiar el campo <workspace ID>
por el ID de tu espacio de trabajo que acabas de crear:
az aks enable-addons \
-a monitoring \
-g pets-group \
-n pets-cluster \
--workspace-resource-id <workspace ID>

Debes tener paciencia porque este comando tarda un tiempo en completarse. Finalmente
generará la siguiente respuesta (resumida para facilitar su lectura):
...
“properties”: {
“provisioningState”: “Succeeded”
},
...

También podríamos haber usado la GUI del portal de Azure o una plantilla de Azure
Resource Manager para la supervisión.

La supervisión se basa en un agente Log Analytics, que se ejecuta en cada nodo del
clúster de Kubernetes y que recopila métricas del procesador y la memoria de todos los
controladores, nodos y contenedores que proporciona la API de métricas de Kubernetes
nativa. Los agentes recopilan las métricas resultantes y estas se envían y almacenan
en el área de trabajo de Log Analytics.

Después de habilitar la supervisión de nuestro clúster, podemos verificar que el agente


Log Analytics se ha implementado en el espacio de nombres kube-system una vez para
cada nodo de trabajo (3):
bash-4.4# kubectl get ds omsagent --namespace=kube-system
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE
NODE SELECTOR AGE
omsagent 3 3 3 3 3 beta.kubernetes.io/
os=linux 1d

[ 263 ]
Ejecución de una aplicación en contenedor desde el cloud

Ahora podemos abrir el portal de Azure para ver las métricas de nuestro clúster.
Ve a Grupos de recursos | pets-group | pets-cluster y haz clic en la opción Estado
(vista previa) para ver una pantalla similar a la siguiente captura de pantalla:

Vista del estado de Microsoft Azure AKS

En esta vista, podemos ver las métricas agregadas de uso de la CPU y la memoria,
así como el recuento de nodos y el recuento de pods activos por clúster, nodo,
controlador y contenedor.

Visualización de los registros de los nodos


maestros de Kubernetes
De forma predeterminada, los registros de diagnóstico están desactivados y primero
tenemos que activarlos. Podemos usar nuestro espacio de trabajo pets-oms-workspace
de Log Analytics como destino para los registros. Para activar los registros de diagnóstico,
utilizaremos el portal de Azure esta vez. Sigue estos pasos:

[ 264 ]
Capítulo 12

1. Desplázate hasta el grupo de recursos pets-group y selecciona la opción


Configuración de diagnóstico en el margen izquierdo. Deberías ver algo
como esto:

Configuración del registro de diagnóstico para el grupo de recursos pets-group

[ 265 ]
Ejecución de una aplicación en contenedor desde el cloud

2. Selecciona la entrada pets-cluster y después haz clic en el enlace


Activar diagnósticos. Rellena el formulario con valores similares a los
mostrados en la captura de pantalla siguiente:

Configuración de los registros de diagnóstico para el clúster de Kubernetes

[ 266 ]
Capítulo 12

3. Comprueba que has seleccionado la casilla de verificación Enviar a Log


Analytics y selecciona pets-oms-workspace como el destino para los registros.
4. Haz clic en el botón Guardar para actualizar la configuración.

Espera unos minutos o unas pocas horas para recopilar toda la información relevante
antes de continuar.

Para analizar los registros, desplázate hasta el grupo de recursos pets-group


y selecciona la entrada pets-cluster. A continuación, selecciona la opción Registros
en el lado izquierdo y verás una vista similar a la que aparece en la siguiente captura
de pantalla:

Análisis de los registros generados por el clúster de Kubernetes y los contenedores que lo ejecutan

En la captura de pantalla anterior, podemos ver la consulta predeterminada que


ha generado entradas del registro producidas por el contenedor ssh-helper que
ejecuta Kubernetes en el nodo aks-nodepool1-54489083-0. Este contenedor
se explicará en la siguiente sección.

Ahora podemos usar el lenguaje de consulta enriquecido que nos facilita Azure Log
Analytics para obtener más información sobre la gran cantidad de datos de registro.

Visualización de los registros del contenedor


y kublet
Algunas veces es necesario investigar los registros de un kubelet en un nodo específico
del clúster de Kubernetes. Para ello, tenemos que establecer una conexión SSH a ese nodo
específico y después podemos usar la herramienta de Linux, journalctl, para acceder
a esos registros.

[ 267 ]
Ejecución de una aplicación en contenedor desde el cloud

Primero tenemos que encontrar el nodo (o máquina virtual) del que queremos investigar
los registros del kublet. Vamos a enumerar todas las máquinas virtuales que forman
parte de nuestro clúster pets-cluster en el grupo de recursos, pets-group, de la
región westeurope. Azure ha creado implícitamente un grupo de recursos llamado
MC_<group name> _<cluster name>_<region name> donde coloca todos los
recursos (incluidas las MV) de nuestro clúster de Kubernetes. En mi caso, el nombre del
grupo es MC_pets-group_pets-cluster_westeurope. Aquí tenemos el comando
que proporciona la lista de MV:
bash-4.4# az vm list --resource-group MC_pets-group_pets-cluster_
westeurope -o table

Name ResourceGroup
Location Zones
------------------------ ------------------------------------- ------
---- -------
aks-nodepool1-54489083-0 MC_pets-group_pets-cluster_westeurope
westeurope

Ahora podemos añadir las claves SSH públicas que utilizamos para conectarnos al clúster
a través de la CLI de Azure a esta MV (con el nombre aks-nodepool1-54489083-0)
con el siguiente comando:
bash-4.4# az vm user update \
--resource-group MC_pets-group_pets-cluster_westeurope \
--name aks-nodepool1-54489083-0 \
--username azureuser \
--ssh-key-value ~/.ssh/id_rsa.pub

Ahora tenemos que obtener la dirección de esta MV y podemos hacerlo con este
comando:
bash-4.4# az vm list-ip-addresses --resource-group MC_pets-group_pets-
cluster_westeurope -o table

VirtualMachine PrivateIPAddresses
------------------------ --------------------
aks-nodepool1-54489083-0 10.240.0.4

Con toda esta información, ahora necesitamos una forma de establecer una conexión
SSH con esta MV. No podemos hacerlo directamente desde nuestra estación de trabajo
sin realizar tareas adicionales, pero una forma sencilla de hacerlo sería ejecutar un
contenedor auxiliar (denominado ssh-helper) en el clúster de Kubernetes de forma
interactiva desde donde podemos establecer una conexión SSH con la MV. Empecemos
con este contenedor auxiliar usando el comando kubectl:
bash-4.4# kubectl run -it --rm ssh-helper --image=debian
root@ssh-helper-86966767d-v2xqg:/#

[ 268 ]
Capítulo 12

Este contenedor no tiene un cliente SSH instalado. Vamos a hacerlo ahora. Dentro de este
contenedor helper ejecuta el siguiente comando:
root@ssh-helper-86966767d-v2xqg:/# apt-get update && apt-get install
openssh-client -y

En otro Terminal, podemos conectar con nuestro contenedor de CLI de Azure de


la forma siguiente:
$ docker container exec azure-cli /bin/bash

A continuación, dentro del contenedor, ejecuta el siguiente comando para ver todos
los pods que se están ejecutando en nuestro clúster:
bash-4.4# kubectl get pods

Aquí mostramos el resultado de ejecutar el comando anterior:

Pod auxiliar SSH ejecutándose en nuestro clúster de Kubernetes

El siguiente paso es copiar nuestra clave SSH privada en el pod en la ubicación prevista.
Podemos usar este comando para hacerlo:
bash-4.4# kubectl cp ~/.ssh/id_rsa ssh-helper-86966767d-v2xqg:/id_rsa

Desde el contenedor helper, ahora tenemos que cambiar los derechos de acceso a esta
clave SSH usando este comando:
root@ssh-helper-86966767d-v2xqg:/# chmod 0600 id_rsa

Finalmente, ya estamos listos para establecer una conexión SSH con la MV de destino:
root@ssh-helper-86966767d-v2xqg:/# ssh -i id_rsa [email protected]

Deberíamos ver una pantalla similar a esta:


The authenticity of host ‘10.240.0.4 (10.240.0.4)’ can’t be
established.
ECDSA key fingerprint is SHA256:pl03ZLFd0pkkPTtzDphSXCuNl0npBJO1JmU
iLI5aSzY.Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added ‘10.240.0.4’ (ECDSA) to the list of known
hosts.Welcome to Ubuntu 16.04.5 LTS (GNU/Linux 4.15.0-1021-azure
x86_64)

[ 269 ]
Ejecución de una aplicación en contenedor desde el cloud

* Documentation: https://help.ubuntu.com * Management: https://


landscape.canonical.com
* Support: https://ubuntu.com/advantage

Get cloud support with Ubuntu Advantage Cloud Guest:


http://www.ubuntu.com/business/services/cloud

3 packages can be updated.


0 updates are security updates.

*** System restart required ***

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by


applicable law.

To run a command as administrator (user “root”), use “sudo <command>”.


See “man sudo_root” for details.

azureuser@aks-nodepool1-54489083-0:~$
¡Y ya lo tenemos! Ahora podemos acceder al nodo deseado de nuestro clúster
de Kubernetes de forma remota.

Lo que acabamos de mostrar, acceder al clúster con un contenedor o


un pod auxiliar, es una forma muy común de depurar un clúster de
Kubernetes remoto y protegido.

Ahora que ya estamos en la MV, usando SSH, podemos acceder a los registros del
kubelet local. Podemos usar el siguiente comando para hacerlo:
azureuser@aks-nodepool1-54489083-0:~$ sudo journalctl -u kubelet -o
cat

Y deberíamos ver una pantalla como esta (acortada):


Stopped Kubelet.
Starting Kubelet...
net.ipv4.tcp_retries2 = 8
Bridge table: nat
Bridge chain: PREROUTING, entries: 0, policy: ACCEPT
Bridge chain: OUTPUT, entries: 0, policy: ACCEPT
Bridge chain: POSTROUTING, entries: 0, policy: ACCEPT
Chain PREROUTING (policy ACCEPT)
...

[ 270 ]
Capítulo 12

I0831 08:30:52.872882 8787 server.go:182] Version: v1.9.9


I0831 08:30:52.873306 8787 feature_gate.go:226] feature gates: &{{}
map[]}
I0831 08:30:54.082665 8787 mount_linux.go:210] Detected OS with
systemd
W0831 08:30:54.083717 8787 cni.go:171] Unable to update cni config: No
networks found in /etc/cni/net.d
I0831 08:30:54.091357 8787 azure.go:249] azure: using client_
id+client_secret to retrieve access token
I0831 08:30:54.091777 8787 azure.go:382] Azure cloudprovider using
rate limit config: QPS=3, bucket=10
...

De forma similar, ahora podremos acceder a los registros de cualquier contenedor que
se ejecute en este nodo. Para enumerar todos los contenedores que se ejecutan en nuestro
frontend de la web, ejecuta el siguiente comando:
azureuser@aks-nodepool1-54489083-0:~$ docker container ls | grep pets-
web

614b6d27dc13 gnspetsregistry.azurecr.io/pets-web@
sha256:43d3f3b3...
493341aff54a gnspetsregistry.azurecr.io/pets-web@
sha256:43d3f3b3...
f5b730aa1449 gnspetsregistry.azurecr.io/pets-web@
sha256:43d3f3b3...

Aparentemente, tenemos tres instancias ejecutándose en este nodo. Ahora vamos


a analizar los registros de la primera:
azureuser@aks-nodepool1-54489083-0:~$ docker container logs
614b6d27dc13

Listening at 0.0.0.0:80
Connecting to DB
Connected!
http://upload.wikimedia.org/wikipedia/commons/d/dc/Cats_Petunia_and_
Mimosa_2004.jpg
Connecting to DB
Connected!
https://upload.wikimedia.org/wikipedia/commons/9/9e/Green_eyes_kitten.
jpg
...

Debemos tener en cuenta, no obstante, que la técnica mostrada


en este punto también puede ser peligrosa, ya que tenemos acceso
root al nodo del clúster. Utiliza esta técnica solo en sistemas que
no estén en producción o en casos de emergencia si no hay otra forma
de acceder a la información relevante.

[ 271 ]
Ejecución de una aplicación en contenedor desde el cloud

Actualización de la aplicación con cero


interrupciones
Ahora que tenemos la aplicación Pets ejecutándose correctamente en el cloud, recibimos
algunas solicitudes de cambio. Parece que a los usuarios no les gusta el color de fondo
de nuestra aplicación. Vamos a cambiarlo y después desplegar los cambios de forma
que no haya interrupciones en la aplicación.

En los pasos siguientes, primero haremos el cambio de código en nuestro proyecto


y después crearemos una nueva versión de la imagen del contenedor correspondiente,
la enviaremos a ACR y después la implementaremos desde allí:

1. En la carpeta ch12/web/src de la carpeta labs, localiza el archivo main.css


y ábrelo en tu editor
2. Cambia el color de fondo del elemento del cuerpo por el color que más te guste,
por ejemplo, lightgreen
3. Guarda los cambios
4. Desde el contenedor de CLI de Azure que has creado, etiqueta y lanza la nueva
versión v2 del contenedor pets-web:
docker image build -t pets-web:v2 /src/web
docker image tag pets-web:v2 gnspetsregistry.azurecr.io/pets-
web:v2
docker push gnspetsregistry.azurecr.io/pets-web:v2

5. Cuando la nueva versión de la imagen de Docker del servicio web esté en el ACR,
podemos emitir un comando de actualización para el servicio web:
kubectl set image deployment web web=gnspetsregistry.azurecr.
io/pets-web:v2

6. Durante la actualización (gradual), podemos supervisar los pods:


kubectl get pods --output=’wide’ --watch

En el manifiesto pets.yaml, hemos definido que los valores maxSurge


y maxUnavailable sean 1. Esto significa que se actualiza un pod cada vez
para que siempre haya al menos cuatro (5-1=4) pods disponibles en cualquier
momento y, de esta forma, la aplicación sea completamente funcional.

7. Actualiza la ventana del navegador con la aplicación Pets en ejecución


y comprueba que el color de fondo ha cambiado al nuevo valor.

Los nuevos pods ahora están distribuidos entre los tres nodos de trabajo
del clúster de Kubernetes.

[ 272 ]
Capítulo 12

Actualización de Kubernetes
Antes nos hemos dado cuenta de que la versión de Kubernetes (v1.9.9) instalada en
nuestros nodos del clúster está bastante anticuada. Ahora demostraremos cómo podemos
actualizar Kubernetes sin provocar interrupciones en la aplicación Pets. Solo podemos
actualizar Kubernetes en fases; es decir, una versión secundaria cada vez. Podemos
averiguar qué versiones están disponibles para nuestra actualización con este comando:
az aks get-upgrades --resource-group pets-group --name pets-cluster
--output table

El resultado generado por el comando debería tener un aspecto similar a este:


Name ResourceGroup MasterVersion NodePoolVersion Upgrades
------- --------------- --------------- ----------------- ------------
------------------
default pets-group 1.9.9 1.9.9 1.9.10,
1.10.3, 1.10.5, 1.10.6

Como ya estamos en la versión v1.9.9, solo podemos actualizar en la misma versión


secundaria 9 o a una de las versiones v1.10.x. Cuando estemos en la versión v1.10.x,
podremos actualizar a v1.11.x.

Ahora vamos a actualizar nuestro clúster a la versión v1.10.6:


az aks upgrade --resource-group pets-group --name pets-cluster
--kubernetes-version 1.10.6

La actualización se realizará nodo por nodo, garantizando que las aplicaciones que
se ejecutan en el clúster siempre estén operativas. La actualización completa tarda
unos minutos. Podemos observar el progreso usando el siguiente comando:
kubectl get nodes --watch

Deberíamos poder ver cómo se vacía un nodo cada vez y después se desactiva antes
de actualizarse y que esté disponible de nuevo.

Cuando se haya completado el proceso, aparecerá un resultado similar a este:


{
“aadProfile”: null,
“addonProfiles”: null,
“agentPoolProfiles”: [
{
“count”: 3,
“maxPods”: 110,
“name”: “nodepool1”,
“osDiskSizeGb”: null,
“osType”: “Linux”,

[ 273 ]
Ejecución de una aplicación en contenedor desde el cloud

“storageProfile”: “ManagedDisks”,
“vmSize”: “Standard_DS1_v2”,
“vnetSubnetId”: null
}
],
“dnsPrefix”: “pets-clust-pets-group-186760”,
“enableRbac”: true,
“fqdn”: “pets-clust-pets-group-186760-d706beb4.hcp.westeurope.
azmk8s.io”,
“id”: “/subscriptions/186760ad-9152-4499-b317-xxxxxxxxxxxx/
resourcegroups/pets-group/providers/Microsoft.ContainerService/
managedClusters/pets-cluster”,
“kubernetesVersion”: “1.10.6”,
“linuxProfile”: {
“adminUsername”: “azureuser”,
“ssh”: {
“publicKeys”: [
{
“keyData”: “ssh-rsa ...”
}
]
}
},
“location”: “westeurope”,
“name”: “pets-cluster”,
“networkProfile”: {
“dnsServiceIp”: “10.0.0.10”,
“dockerBridgeCidr”: “172.17.0.1/16”,
“networkPlugin”: “kubenet”,
“networkPolicy”: null,
“podCidr”: “10.244.0.0/16”,
“serviceCidr”: “10.0.0.0/16”
},
“nodeResourceGroup”: “MC_pets-group_pets-cluster_westeurope”,
“provisioningState”: “Succeeded”,
“resourceGroup”: “pets-group”,
“servicePrincipalProfile”: {
“clientId”: “a1a2bdbc-ba07-49bd-ae77-xxxxxxxxxxxx”,
“secret”: null
},
“tags”: null,
“type”: “Microsoft.ContainerService/ManagedClusters”
}

Para verificar la actualización, podemos usar este comando:


az aks show --resource-group pets-group --name pets-cluster --output
table

[ 274 ]
Capítulo 12

Esto debería generar el siguiente resultado:


Name Location ResourceGroup KubernetesVersion
ProvisioningState Fqdn
------------ ---------- --------------- ------------------- ----------
--------- -------------
pets-cluster westeurope pets-group 1.10.6
Succeeded pets-clust...

Hemos actualizado correctamente nuestro clúster de tres nodos de Kubernetes v1.9.9


a v1.10.6. Esta actualización incluye los modos maestros. Durante todo el proceso,
la aplicación implementada en el clúster ha estado completamente operativa.

Depuración de la aplicación mientras


se ejecuta en AKS
Hasta ahora hemos visto cómo podemos implementar y ejecutar aplicaciones
en Kubernetes en AKS. Esto resulta interesante para los ingenieros de operaciones que
trabajan con aplicaciones terminadas. No obstante, algunas veces los desarrolladores
quieren desarrollar y depurar aplicaciones de forma interactiva y directamente en
el cloud, especialmente si estas aplicaciones constan de servicios o componentes
individuales que se ejecutan en contenedores en Kubernetes. En el momento de redactar
este documento, Microsoft ofrece una preview de Azure Dev Spaces en AKS, que permite
a los desarrolladores hacer exactamente este tipo de tareas interactivas de desarrollo
y depuración.

Creación de un clúster de Kubernetes para


desarrollo
En esta sección vamos a crear un clúster de Kubernetes en Azure AKS que pueda usarse
para fines de desarrollo. Utilizaremos este clúster para mostrar cómo depurar de forma
remota una aplicación en ejecución. Utilizaremos el portal de Azure para aprovisionar
el clúster:

1. Abre el portal de Azure en https://portal.azure.com/ e inicia sesión


en tu cuenta
2. Selecciona la opción + Crear recurso y selecciona Servicio de Kubernetes
3. Rellena los campos necesarios para crear un clúster de Kubernetes para fines
de desarrollo.

[ 275 ]
Ejecución de una aplicación en contenedor desde el cloud

En la siguiente captura de pantalla podemos ver un ejemplo de configuración que


el autor ha usado al escribir esta sección:

Configuración de un clúster de desarrollo de Kubernetes en AKS

He creado un nuevo grupo de recursos, pets-dev-group, he seleccionado el valor


pets-dev-cluster para el nombre del clúster y he creado un nuevo espacio de trabajo
de Log Analytics, pets-dev-workspace, para capturar todos los registros y datos
de monitorización producidos por el clúster y las aplicaciones que lo ejecutan. También
es importante haber activado el enrutamiento de aplicaciones HTTP.

Después de hacer clic en el botón Crear, todo el aprovisionamiento tardará unos pocos
minutos. Puedes seguir con la instalación de la CLI de Azure en tu estación de trabajo
paralelamente, como se describe en la sección siguiente.

[ 276 ]
Capítulo 12

Configuración del entorno


En esta sección vamos a configurar nuestro entorno de trabajo (es decir, nuestro
ordenador) para poder trabajar con Azure desde la línea de comandos. Sigue estos pasos:

1. Para trabajar con Azure Dev Spaces, instala la CLI de Azure de forma nativa
en tu estación de trabajo. Si trabajas en un Mac, tendrás que hacer lo siguiente:
$ brew install azure-cli

2. Comprueba que tienes la versión más reciente de la CLI. La versión mostrada


debería ser 2.0.44 o superior:
$ az --version
azure-cli (2.0.45)
...

3. Inicia sesión en Azure:


$ az login

No continúes hasta que se haya completado el aprovisionamiento de tu clúster


de desarrollo de Kubernetes.

4. Para configurar Azure Dev Space en nuestro clúster de desarrollo de Kubernetes,


utiliza el siguiente comando:
$ az aks use-dev-spaces -g pets-dev-group -n pets-dev-cluster

The installed extension ‘dev-spaces-preview’ is in preview.


Installing Dev Spaces (Preview) commands...
Installing Azure Dev Spaces (Preview) client components...

By continuing, you agree to the Microsoft Software License Terms


(https://aka.ms/azds-LicenseTerms) and Microsoft Privacy Statement
(https://aka.ms/privacystatement). Do you want to continue? (Y/n):

You may be prompted for your administrator password to authorize


the installation process.
Password:

[INFO] Downloading Azure Dev Spaces (Preview) Package...


[INFO] Downloading Bash completion script...

Successfully installed Azure Dev Spaces (Preview) to /usr/local/


bin/azds.

An Azure Dev Spaces Controller will be created that targets


resource ‘pets-dev-cluster’ in resource group ‘pets-dev-group’.
Continue? (y/N): Y

[ 277 ]
Ejecución de una aplicación en contenedor desde el cloud

Creating and selecting Azure Dev Spaces Controller ‘pets-dev-


cluster’ in resource group ‘pets-dev-group’ that targets resource
‘pets-dev-cluster’ in resource group ‘pets-dev-group’...

Select a dev space or Kubernetes namespace to use as a dev space.


[1] default
Type a number or a new name: pets

Dev space ‘pets’ does not exist and will be created.

Select a parent dev space or Kubernetes namespace to use as a


parent dev space.
[0] <none>
[1] default
Type a number: 0

Creating and selecting dev space ‘pets’...2s

Managed Kubernetes cluster ‘pets-dev-cluster’ in resource group


‘pets-dev-group’ is ready for development in dev space ‘pets’.
Type `azds prep` to prepare a source directory for use with Azure
Dev Spaces and `azds up` to run.

Implementación y ejecución de un servicio


Ahora ya estamos preparados para crear, implementar y ejecutar nuestro primer servicio
en el clúster de Kubernetes en AKS.

Desplázate hasta la subcarpeta ch12-dev-spaces/web en la carpeta labs:


$ cd ~/labs/ch12-dev-spaces/web

Ejecuta el comando azds prep como se indica en el resultado anterior. Esta acción
creará los gráficos Helm para este componente:
$ azds prep --public

Preparing ‘web’ of type ‘node.js’ with files:


/.dockerignore
/azds.yaml
/charts/web/.helmignore
/charts/web/Chart.yaml
/charts/web/templates/_helpers.tpl
/charts/web/templates/deployment.yaml
/charts/web/templates/ingress.yaml
/charts/web/templates/NOTES.txt
/charts/web/templates/secrets.yaml
/charts/web/templates/service.yaml
/charts/web/values.yaml
Type ‘azds up’ to run.

[ 278 ]
Capítulo 12

Para crear los artefactos y ejecutarlos en AKS, podemos usar el siguiente comando:
$ azds up

Using dev space ‘pets’ with target ‘pets-dev-cluster’


Synchronizing files...1s
Installing Helm chart...10s
Waiting for container image build...7s
Building container image...
Step 1/8 : FROM node:10.9-alpine
Step 2/8 : RUN mkdir /app
Step 3/8 : WORKDIR /app
Step 4/8 : COPY package.json /app/
Step 5/8 : RUN npm install
Step 6/8 : COPY ./src /app/src
Step 7/8 : EXPOSE 80
Step 8/8 : CMD node src/server.js
Built container image in 51s
Waiting for container...8s
(pending registration) Service ‘web’ port ‘http’ will be available at
http://web.2785a289211f45f6a8fa.westeurope.aksapp.io/
Service ‘web’ port 80 (TCP) is available at http://localhost:52353
press Ctrl+C to detach
web-6488b5585b-c9cg5: Listening at 0.0.0.0:80

Podemos abrir inmediatamente una ventana del navegador en http://


localhost:52353, como se indica en el resultado anterior para acceder a nuestro
servicio que ahora se ejecuta en AKS. Unos minutos más tarde, deberíamos poder usar
el DNS público (http://web.2785a289211f45f6a8fa.westeurope.aksapp.io)
proporcionado en el resultado anterior para acceder al servicio. Lo que deberíamos ver
es la siguiente captura de pantalla:

Aplicación Pets ejecutándose en Azure Dev Spaces

[ 279 ]
Ejecución de una aplicación en contenedor desde el cloud

En cualquier momento, podemos detener (o desconectar) el servicio pulsando Ctrl + C


en el Terminal y después podemos actualizar el código. Cuando se haya hecho,
solo tendremos que iniciar la nueva versión con $ azds up otra vez. Haz esto ahora
y pulsa Ctrl + C. Después, cambia el mensaje en el archivo server.js para que sea
Mi aplicación Pets. Guarda los cambios y ejecuta el siguiente comando:

$ azds up

Cuando la aplicación esté lista, actualiza el navegador y deberías poder ver el mensaje
modificado.

Para prepararte para el siguiente ejercicio, pulsa Ctrl + C y ejecuta el siguiente comando
para parar y eliminar el componente de nuestro clúster de Kubernetes:
$ azds down

‘web’ identifies Helm release ‘pets-web-9c1bf6d2’ which includes


these services:
web
Are you sure you want to delete this release? (y/N): Y

Deleting Helm release ‘pets-web-9c1bf6d2’...19s

Depuración remota de un servicio usando


Visual Studio Code
Naturalmente, este proceso es un poco lento y podemos hacerlo mejor si usamos
la depuración remota de Visual Studio Code (VS Code):

1. Abre VS Code, y descarga e instala la extensión Azure Dev Spaces.


A continuación, reinicia VS Code.
2. Abre VS Code desde la carpeta ch12-dev-spaces/web. El sistema te preguntará
si la extensión de Azure Dev Spaces debe crear los recursos necesarios para crear
y depurar el componente web. Selecciona la opción Sí:

Visual Studio Code creando recursos de compilación y depuración

[ 280 ]
Capítulo 12

Notarás que se ha creado una carpeta .vscode en tu proyecto que contiene


los dos archivos, launch.json y tasks.json. Estos archivos se utilizan
para la depuración remota del servicio web.

3. En VS Code, pulsa F5 y observa cómo se crea el servicio y cómo se ejecuta en


AKS en el modo de depuración. Esto utilizará la tarea de lanzamiento llamada
Launch Server (AZDS), como podemos ver en el panel de depuración de
VS Code:

Lanzamiento del servicio web en el modo de depuración en AKS

4. Define un punto de interrupción en la línea 13 del archivo server.json:

Definición de un punto de interrupción en el código del servicio web

Si ahora actualizas el navegador en http://localhost:52353, la ejecución


del código se detendrá en el punto de interrupción.

[ 281 ]
Ejecución de una aplicación en contenedor desde el cloud

Puedes usar la barra de herramientas de depuración de VS Code para recorrer


el código línea por línea o para realizar una ejecución continua:

Depuración paso a paso

Activación del desarrollo de estilo "editar


y continuar" en el cloud
Para permitir una experiencia de "editar y continuar", podemos hacerlo mejor. Para ello,
tenemos una segunda tarea de lanzamiento denominada Attach (AZDS), que se basa
en nodemon. Cada vez que actualicemos el código localmente, se refleja en el contenedor
que se ejecuta de forma remota en AKS y nodemon, tras detectar los cambios, reiniciará
automáticamente la aplicación en el contenedor. En consecuencia, podemos cambiar
y actualizar el código en VS Code, guardar los cambios y, unos segundos más tarde,
usar y depurar el nuevo código en AKS.

Si tu aplicación sigue ejecutándose desde el ejercicio anterior, para la depuración ahora,


pulsa Ctrl + C en la consola y después ejecuta el comando $ azds down para detener
y eliminar el servicio.

En la vista de depuración de VS Code, selecciona la tarea de lanzamiento Attach to


Server (AZDS) y pulsa F5. Una vez que el servicio se ha implementado, puedes definir
puntos de interrupción como antes y navegar por el código, pero además también podrás
cambiar el código en server.js.

[ 282 ]
Capítulo 12

Después de guardar los cambios, los archivos modificados se sincronizarán con AKS
y nodemon reiniciará la aplicación que se ejecuta dentro del contenedor en Kubernetes.
Intenta esto añadiendo el siguiente fragmento de código al archivo server.js:

Adición de código a un componente en ejecución en AKS

Después de guardar el archivo y esperar unos segundos, ve a la URL http://


localhost:52353/pet. Deberías poder ver la imagen de un gatito.

Limpieza
Para evitar costes innecesarios, deberíamos limpiar (es decir, eliminar) todos los recursos
que hemos creado en Microsoft Azure. Este proceso es bastante sencillo porque hemos
agrupado todos nuestros recursos en los grupos de recursos pets-group y pets-dev-
group, y solo tenemos que eliminar los grupos para deshacernos de todos los recursos
incluidos. Podemos hacerlo con la CLI de Azure:
bash-4.4# az group delete --name pets-group
Are you sure you want to perform this operation? (y/n): y

bash-4.4# az group delete --name pets-dev-group


Are you sure you want to perform this operation? (y/n): y

El proceso completo solo tardará unos pocos minutos en completarse.

Es posible que también quieras comprobar en el portal de Azure que se han eliminado
realmente todos los recursos.

Resumen
En este capítulo hemos aprendido a aprovisionar un clúster de Kubernetes
completamente alojado en la solución de cloud de Microsoft AKS. Además, hemos
aprendido a implementar, ejecutar, supervisar, actualizar e incluso depurar de manera
interactiva una aplicación que se ejecuta en este clúster en AKS. También hemos
aprendido los conceptos básicos sobre cómo actualizar la versión de Kubernetes en AKS
sin interrumpir las aplicaciones que se ejecutan en el clúster.

[ 283 ]
Ejecución de una aplicación en contenedor desde el cloud

Con esto, hemos completado el trabajo de esta publicación y llega el momento de


agradecerte que hayas escogido esta guía para convertirte en un experto en Docker, capaz
de incluir aplicaciones complejas y críticas en contenedores, y después implementarlas
en Kubernetes ejecutándose en el cloud.

Preguntas
Para evaluar tus conocimientos, responde a las siguientes preguntas:

1. ¿Qué tres opciones existen para aprovisionar un clúster de Kubernetes


completamente alojado en AKS?
2. Nombra dos o tres razones por las que tiene sentido alojar tus imágenes
de Docker en ACR cuando se utiliza AKS.
3. Explica en tres o cuatro frases los principales pasos necesarios para ejecutar
tu aplicación en contenedor en AKS.
4. Explica cómo puedes acceder a los archivos del registro del contenedor
de tu aplicación que se ejecuta en AKS.
5. Explica cómo puedes escalar el número de nodos de tu clúster de Kubernetes
en AKS.

Lectura adicional
Los siguientes artículos ofrecen más información sobre los temas que hemos tratado
en este capítulo (pueden estar en inglés):

• Azure Kubernetes Service (AKS) en https://bit.ly/2JglX9d


• Actualizar un clúster de Azure Kubernetes Service (AKS) en
https://bit.ly/2wCYA4P
• ¿Qué es Azure Log Analytics? en https://bit.ly/2LN4Tbr
• Introducción a Dev Spaces para AKS en https://bit.ly/2NljFba

[ 284 ]
Evaluación

Capítulo 1: ¿Qué son los contenedores


y por qué debo usarlos?
1. Las respuestas correctas son: 4, 5.
2. Un contenedor Docker es en TI lo que sería un contenedor de envío en el sector del
transporte. Define un estándar sobre cómo embalar las mercancías. En este caso, las
mercancías son las aplicaciones que escriben los desarrolladores. Los proveedores
(en este caso, los desarrolladores) son responsables de embalar las mercancías
en el contenedor y de comprobar que todo cabe perfectamente. Una vez que las
mercancías se embalan en un contenedor, podrán ser enviadas. Dado que se trata
de un contenedor estándar, los transportistas pueden personalizar su medio de
transporte como en camiones, trenes o barcos. El transportista no se preocupa por
los productos que hay dentro del contenedor. Del mismo modo, el proceso de carga
y descarga de un medio de transporte a otro (por ejemplo, del tren al barco) también
está muy estandarizado. Esto aumenta drásticamente la eficacia del transporte.
Paralelamente a este proceso, existe un ingeniero de operaciones en el sector de TI
que puede coger un contenedor de software creado por un desarrollador y enviarlo
a un sistema de producción y ejecutarlo allí de forma estandarizada, sin preocuparse
por los elementos que están dentro del contenedor. Simplemente funciona.
3. Algunos de los motivos que explican por qué los contenedores son capaces
de cambiar las reglas del juego son:
°° Los contenedores son autosuficientes y, por ello, si se ejecutan en un sistema,
se ejecutarán en cualquier parte donde pueda ejecutarse un contenedor.
°° Los contenedores se ejecutan on-premises y en el cloud, así como en
entornos híbridos. Esto es muy importante para las empresas actuales, ya
que permite realizar una transición fluida del entorno on-premises al cloud.
°° Las personas que mejor conocen las imágenes de los contenedores
se encargan de crearlas o integrarlas: los desarrolladores.
[ 285 ]
Evaluación

°° Las imágenes de los contenedores son inmutables, lo que resulta


importante para gestionar correctamente las versiones.
°° Los contenedores permiten obtener una cadena de suministro
de software segura basada en la encapsulación (usando espacios de
nombres y cgroups de Linux), secretos, confianza en el contenido
y análisis de vulnerabilidad de las imágenes.

4. Un contenedor se ejecuta en cualquier parte donde puede ejecutarse porque:


°° Los contenedores son cajas negras autosuficientes. Encapsulan no solo
una aplicación, sino todas sus dependencias, tales como bibliotecas
y plataformas, datos de configuración, certificados, etc.
°° Los contenedores se basan en estándares ampliamente aceptados
como OCI.
°° TAREA: añadir más motivos.

5. ¡Falso! Los contenedores resultan útiles para aplicaciones modernas y para


incluir en contenedores las aplicaciones tradicionales. Los beneficios que aporta
la última opción a las empresas son enormes. Se han registrado ahorros de
costes en el mantenimiento de aplicaciones heredadas del 50 % o más. El tiempo
transcurrido entre las nuevas versiones de dichas aplicaciones heredadas puede
reducirse hasta el 90 %. Estas cifras han sido publicadas por clientes reales
de varias empresas.
6. 50 % o más.
7. Los contenedores se basan en espacios de nombres de Linux
(red, proceso, usuario, etc.) y en grupos de control (cgroups).

Capítulo 2: Configuración de un entorno


de trabajo
1. docker-machine puede usarse para hacer lo siguiente:
°° Crear una MV configurada como host de Docker en distintos entornos,
como VirtualBox
°° Establecer una conexión SSH en un host de Docker
°° Configurar la CLI de Docker local para el acceso de un host de Docker
remoto
°° Enumerar todos los hosts de un entorno específico
°° Eliminar o destruir los hosts existentes

2. Verdadero. Docker para Windows crea una MV de Linux en Hyper-V, donde


después ejecuta contenedores Linux.

[ 286 ]
Apéndice

3. El contenedor se utiliza de forma óptima en CI/CD, que se basa en la


automatización. Cada paso, desde la creación de una imagen de contenedor al
envío de la imagen y finalmente la ejecución de contenedores desde esta imagen,
se programa de forma idónea para lograr una productividad máxima. Con ello
se consigue un proceso repetible y auditable.
4. Ubuntu 17.4 o posterior, CentOS 7.x, Alpine 3.x, Debian, Suse Linux,
RedHat Linux, etc.
5. Windows 10 Professional o Enterprise Edition, Windows Server 2016.

Capítulo 3: Trabajar con contenedores


1. Los estados de los contenedores son los siguientes:
°° Created
°° Running
°° Exited
2. El comando siguiente nos ayuda a averiguar qué se está ejecutando actualmente
en nuestro host:
$ docker container ls

3. El comando siguiente se utiliza para enumerar los ID de todos los contenedores:


$ docker container ls -q

Capítulo 4: Creación y gestión


de imágenes de contenedor
Aquí mostramos algunas respuestas posibles a las preguntas:
1. Dockerfile:
FROM ubuntu:17.04
RUN apt-get update
RUN apt-get install -y ping
ENTRYPOINT ping
CMD 127.0.0.1

2. Para conseguir el resultado puedes ejecutar los siguientes pasos:


$ docker container run -it --name sample \
alpine:latest /bin/sh
/ # apk update && \
apk add -y curl && \
[ 287 ]
Evaluación

rm -rf /var/cache/apk/*
/ # exit
$ docker container commit sample my-alpine:1.0
$ docker container rm sample

3. A modo de ejemplo, podemos ver la aplicación Hello World en C:


1. Crea un archivo hello.c con este contenido:
#include <stdio.h>
int main()
{
printf("Hello, World!");
return 0;
}

2. Crea un Dockerfile con este contenido:


FROM alpine:3.5 AS build
RUN apk update && \
apk add --update alpine-sdk
RUN mkdir /app
WORKDIR /app
COPY hello.c /app
RUN mkdir bin
RUN gcc -Wall hello.c -o bin/hello

FROM alpine:3.5
COPY --from=build /app/bin/hello /app/hello
CMD /app/hello

4. Algunas de las características de una imagen de Docker son:


°° Es inmutable
°° Se compone de capas inmutables
°° Cada capa solo contiene lo que ha cambiado (el delta) en las capas
inferiores
°° Una imagen es un tarball (grande) de archivos y carpetas
°° Una imagen es una plantilla para contenedores

5. La opción 3 es correcta. Primero tenemos que comprobar que hemos iniciado


sesión y después etiquetar la imagen para enviarla finalmente. Dado que se
trata de una imagen, estamos usando una docker image... y no docker
container... (como en el número 4).

[ 288 ]
Apéndice

Capítulo 5: Administración de volúmenes


y sistemas de datos
La forma más sencilla de jugar con los volúmenes es usar Docker Toolbox
como cuando se usa directamente Docker para Mac o Docker para Windows.
A continuación, los volúmenes se guardan dentro de una MV de Linux (oculta de
alguna forma) que Docker para Mac/Win utiliza de forma transparente.

Por consiguiente, aconsejamos lo siguiente:


$ docker-machine create --driver virtualbox volume-test
$ docker-machine ssh volume-test

Ahora que ya estamos dentro de una MV de Linux llamada volume-test, podemos


realizar los siguientes ejercicios:
1. Para crear un volumen con nombre, ejecuta el comando siguiente:
$ docker volume create my-products

2. Ejecuta el siguiente comando:


$ docker container run -it --rm \
-v my-products:/data:ro \
alpine /bin/sh

3. Para obtener la ruta en el host para la utilización del volumen ejecuta, por
ejemplo, este comando:
$ docker volume inspect my-products | grep Mountpoint

Que (si se está usando docker-machine y VirtualBox) daría como resultado:


"Mountpoint": "/mnt/sda1/var/lib/docker/volumes/my-products/_
data"

Ahora ejecuta el comando siguiente:


$ sudo su
$ cd /mnt/sda1/var/lib/docker/volumes/my-products/_data
$ echo "Hello world" > sample.txt
$ exit

4. Ejecuta el siguiente comando:


$ docker run -it --rm -v my-products:/data:ro alpine /bin/sh
# / cd /data
# / cat sample.txt

[ 289 ]
Evaluación

En otro terminal ejecuta:


$ docker run -it --rm -v my-products:/app-data alpine /bin/sh
# / cd /app-data
# / echo "Hello other container" > hello.txt
# / exit

5. Ejecuta un comando como este:


$ docker container run -it --rm \
-v $HOME/my-project:/app/data \
alpine /bin/sh

6. Sal de los dos contenedores y cuando vuelvas al host, ejecuta este comando:
$ docker volume prune

7. Ejecuta el siguiente comando:


$ docker system info | grep Version

Deberías ver algo similar a esto:


Server Version: 17.09.1-ce
Kernel Version: 4.4.104-boot2docker

Si has usado docker-machine para crear y usar una MV de Linux en


VirtualBox, no olvides realizar una limpieza cuando hayas acabado:
$ docker-machine rm volume-test

Capítulo 6: Arquitectura de aplicaciones


distribuidas
1. En un sistema formado por muchos elementos, el fallo de un componente
ocurrirá tarde o temprano. Para evitar interrupciones si se produce un fallo, es
necesario ejecutar varias instancias de cada componente. Si una de las instancias
falla, seguirá habiendo otras que puedan atender las solicitudes.
2. En una arquitectura de aplicaciones distribuidas, tenemos muchos elementos
móviles. Si el Servicio A necesita acceso a una instancia del Servicio B, no podrá
saber dónde encontrar dicha instancia. Las instancias pueden encontrarse en un
nodo aleatorio del clúster y pueden entrar y salir en la medida que el motor de
orquestación lo crea oportuno, por lo que no identificamos la instancia de destino
por su dirección IP y su puerto, sino que lo hacemos por su nombre y puerto.
Un servicio DNS sabe cómo resolver un nombre de servicio en una dirección IP,
ya que tiene toda la información sobre las instancias del servicio que se ejecutan
en el clúster.

[ 290 ]
Apéndice

3. Un cortacircuitos es un mecanismo que ayuda a evitar fallos en cascada en


una aplicación distribuida provocados por el fallo de un solo servicio. El
cortacircuitos observa cómo una solicitud pasa de un servicio a otro y mide la
latencia a lo largo del tiempo y el número de fallos de solicitud o tiempos de
espera agotados. Si una instancia de destino específica provoca muchos fallos,
las llamadas a esa instancia se interceptan y se envía un código de error al autor
de la llamada, dando tiempo al destino para recuperarse si es posible, y el autor
de la llamada, a cambio, sabe al instante que debe degradar su propio servicio
o intentarlo con otra instancia del servicio de destino.
4. Un monolito es una aplicación formada por una base de código único que está
estrechamente acoplada. Si se hacen cambios en el código, aunque sean mínimos,
será necesario compilar, empaquetar y volver a implementar toda la aplicación.
Los monolitos pueden implementarse y supervisarse fácilmente en producción
gracias al hecho de que tienen muy pocos elementos móviles. Los monolitos son
difíciles de mantener y ampliar. Una aplicación distribuida consta de muchos
servicios con acoplamiento laxo. Cada servicio se origina en su propia base de
código fuente independiente. Los distintos servicios pueden tener, y a menudo
tienen, ciclos de vida independientes. Pueden implementarse y revisarse de
forma independiente. Las aplicaciones distribuidas son más difíciles de gestionar
y supervisar.
5. Una implementación blue-green es cuando una versión actual de un servicio
en ejecución, llamada blue, se sustituye por una nueva versión del mismo
servicio, llamada green. La sustitución se produce sin interrupciones: mientras
que la versión blue está en ejecución, la versión green se instala en el sistema y,
cuando está lista, se necesita un simple cambio en la configuración del router que
distribuye el tráfico hacia el servicio para que el tráfico ahora se dirija a la versión
green en lugar de a la blue.

Capítulo 7: Conexión en red con


un solo host
1. Los tres elementos principales son: sandbox, punto de conexión y red
2. Ejecuta este comando:
$ docker network create --driver bridge frontend

3. Ejecuta este comando:


$ docker container run -d --name n1 \
--network frontend -p 8080:80 \
nginx:alpine
$ docker container run -d --name n2 \
--network frontend -p 8081:80 \
nginx:alpine

[ 291 ]
Evaluación

Comprueba que las dos instancias de Nginx estén en funcionamiento:


$ curl -4 localhost:8080
$ curl -4 localhost:8081

Deberías ver una página de bienvenida de Nginx en ambos casos.


4. Para obtener las direcciones IP de todos los contenedores conectados, ejecuta:
$ docker network inspect frontend | grep IPv4Address

Deberías ver algo similar a lo siguiente:


"IPv4Address": "172.18.0.2/16",
"IPv4Address": "172.18.0.3/16",

Para obtener la subred utilizada por la red, usa lo siguiente (por ejemplo):
$ docker network inspect frontend | grep subnet

Deberías recibir algo similar a las líneas siguientes (obtenido del ejemplo
anterior):
"Subnet": "172.18.0.0/16",

5. La red del host nos permite ejecutar un contenedor en el espacio de nombres


de red del host.
6. Utiliza solo esta red para fines de depuración o para crear una herramienta
de nivel de sistema. Nunca utilices la red del host para un contenedor
de aplicación que se ejecute en producción.
7. La red none lo que indica básicamente es que el contenedor no se ha conectado a
ninguna red. Debe usarse para contenedores que no tienen que comunicarse con
otros contenedores y a los que se no se tiene que acceder desde el exterior.
8. La red none podría usarse, por ejemplo, para ejecutar los procesos en lotes en un
contenedor que solo necesite acceso a recursos locales tales como archivos a los
que se podría acceder a través de un volumen montado en el host.

Capítulo 8: Docker Compose


1. El código siguiente puede utilizarse para ejecutar la aplicación en el modo
daemon.
$ docker-compose up -d

2. Ejecuta el siguiente comando para mostrar los detalles del servicio en ejecución.
$ docker-compose ps

[ 292 ]
Apéndice

Esto debería generar el siguiente resultado:


Name Command State Ports
--------------------------------------------------------------
----
mycontent_nginx_1 nginx -g daemon off; Up 0.0.0.0:3000-
>80/tcp

3. El comando siguiente puede usarse para escalar el servicio web:


$ docker-compose up --scale web=3

Capítulo 9: Orquestadores
Aquí podrás encontrar las respuestas a las preguntas de este capítulo:
1. A continuación, indicamos algunos motivos por los que necesitamos un motor
de orquestación:
°° Los contenedores son efímeros y solo un sistema automatizado
(el orquestador) puede gestionarlos de forma eficaz.
°° Por motivos de alta disponibilidad, queremos ejecutar varias instancias
de cada contenedor. El número de contenedores que deben gestionarse
aumenta rápidamente.
°° Para cubrir la demanda actual de Internet, tenemos que escalar
rápidamente en sentido ascendente y descendente.
°° Los contenedores, al contrario que las MV, no se tratan como mascotas
ni se fijan o reparan cuando tienen un comportamiento inadecuado, sino
que se tratan como ganado. Si uno tiene un comportamiento inadecuado,
lo eliminamos y sustituimos por una nueva instancia. El orquestador
rápidamente termina un contenedor erróneo y programa una nueva
instancia.

2. Las responsabilidades del motor de orquestación de un contenedor son:


°° Gestionar un grupo de nodos de un clúster
°° Programar cargas de trabajos en los nodos con recursos libres suficientes
°° Supervisar el estado de los nodos y la carga de trabajo
°° Conciliar el estado actual con el estado deseado de las aplicaciones y los
componentes
°° Proporcionar enrutamiento y detección de servicios
°° Responder a solicitudes del equilibrador de carga
°° Proteger los datos confidenciales ofreciendo respaldo para los secretos

[ 293 ]
Evaluación

3. A continuación incluimos una lista (incompleta) de orquestadores, ordenados


por popularidad:
°° Kubernetes de Google, donado a CNCF
°° SwarmKit de Docker, es decir, Operations Support System (OSS)
°° AWS ECS de Amazon
°° Azure AKS de Microsoft
°° Mesos de Apache (OSS)
°° Cattle de Rancher
°° Nomad de HashiCorp

Capítulo 10: Orquestación de aplicaciones


en contenedores con Kubernetes
1. El nodo maestro de Kubernetes es responsable de gestionar el clúster. Todas
las solicitudes para crear objetos, la programación de pods, la gestión de
ReplicaSets y mucho más ocurre en el nodo maestro. El nodo maestro no
ejecuta una carga de trabajo de aplicación en un clúster en producción o similar.
2. En cada nodo de trabajo, tenemos el kubelet, el proxy y un runtime
de contenedor.
3. La respuesta es Sí. No puedes ejecutar contenedores independientes en un
clúster de Kubernetes. Los pods son unidades atómicas de implementación
en un clúster.
4. Todos los contenedores que se ejecutan dentro de un pod comparten el mismo
espacio de nombres de red del kernel Linux. Por ello, todos los procesos que se
ejecutan dentro de esos contenedores pueden comunicarse entre sí a través de
localhost, de forma similar a la manera en que los procesos o las aplicaciones
que se ejecutan directamente en el host pueden comunicarse entre sí a través
de localhost.
5. La única función del contenedor pause es reservar los espacios de nombres del
pod para los contenedores que se ejecutan en el pod.
6. Esta no es buena idea, ya que todos los contenedores de un pod están
coubicados, lo que significa que se ejecutan en el mismo nodo del clúster.
Pero los distintos componentes de la aplicación (web, inventario y base
de datos) suelen tener requisitos muy distintos de escalabilidad o consumo
de recursos. El componente web puede tener que escalarse dependiendo del
tráfico y el componente de la base de datos tiene requisitos especiales para
el almacenamiento que los otros componentes no tienen. Si ejecutamos todos los
componentes en su propio pod, seremos mucho más flexibles en este sentido.

[ 294 ]
Apéndice

7. Necesitamos un mecanismo para ejecutar varias instancias de un pod en un


clúster y asegurarnos de que el número real de pods que se ejecutan siempre
se corresponde con el número deseado, incluso cuando un pod individual se
corrompe o desaparece a causa de un fallo del nodo del clúster o una partición
de red. El ReplicaSet (conjunto de réplicas) es este mecanismo que proporciona
escalabilidad y reparación automática a cualquier servicio de aplicación.
8. Necesitamos objetos de implementación allí donde queramos actualizar un
servicio de aplicación en un clúster de Kubernetes sin provocar interrupciones
en el servicio. Los objetos de implementación añaden funciones de
actualizaciones graduales y reversión a los ReplicaSets.
9. Los objetos de servicio de Kubernetes se utilizan para que los servicios
de la aplicación participen en la detección del servicio. Proporcionan un punto
de conexión estable a un conjunto de pods (normalmente regulados por un
ReplicaSet o una implementación). Los servicios Kube son abstracciones que
definen un conjunto lógico de pods y una política para poder acceder a ellos.
Hay cuatro tipos de servicios Kube:
°° ClusterIP: expone el servicio en una dirección IP a la que solo se puede
acceder desde dentro del clúster; es una IP virtual (VIP)
°° NodePort: publica un puerto en el intervalo 30.000–32767 en cada nodo
del clúster
°° LoadBalancer: este tipo expone el servicio de la aplicación externamente
usando un equilibrador de carga del proveedor del cloud como ELB en AWS
°° ExternalName: se utiliza para definir un proxy para un servicio externo
del clúster como una base de datos

Capítulo 11: Implementación,


actualización y protección de una
aplicación con Kubernetes
1. Si tenemos una imagen de Docker en un registro para los dos servicios de
aplicación, la API web y Mongo DB, entonces tendremos que hacer lo siguiente:
°° Definir una implementación para Mongo DB usando un StatefulSet;
llamaremos a esta implementación db-deployment. El StatefulSet
debería tener una réplica (replicar Mongo DB es más complejo y queda
fuera del ámbito de este libro).
°° Definir un servicio de Kubernetes denominado db del tipo ClusterIP
para db-deployment.
°° Definir una implementación de la API web; la llamaremos
web-deployment. Ahora escalaremos este servicio a tres instancias.
°° Definir un servicio de Kubernetes llamado api del tipo NodePort para
web-deployment.
[ 295 ]
Evaluación

°° Si utilizamos secretos, definiremos estos secretos directamente


en el clúster usando kubectl.
°° Implementar la aplicación usando kubectl.

2. Para implementar el enrutamiento de capa 7 para una aplicación, lo ideal sería


utilizar IngressController. IngressController es un proxy invertido como
Nginx que tiene un sidecar escuchando en la API del servidor de Kubernetes
para detectar cambios relevantes y actualizar la configuración del proxy invertido
y poder reiniciarla si detecta un cambio de este tipo. Tenemos que definir recursos
de Ingress en el clúster que definan el enrutamiento, por ejemplo desde una ruta
basada en contexto como https://example.com/pets a un par <nombre de
servicio>/<puerto> como api/32001. En el momento en que kubernetes
crea o cambia este objeto de Ingress, el sidecar de IngressController lo recoge
y actualiza la configuración de enrutamiento del proxy.
3. Si presuponemos que se trata de un servicio de inventario interno del clúster:
°° Al implementar la versión 1.0 definimos una implementación
denominada inventory-deployment-blue y etiquetamos los pods con
una etiqueta color: blue.
°° Implementamos el servicio de Kubernetes de tipo ClusterIP llamado
inventory para la implementación anterior con el selector que contiene
color: blue.
°° Cuando estemos listos para implementar la nueva versión del servicio
de pagos, primero definimos una implementación para la versión 2.0 del
servicio y la llamamos inventory-deployment-green. Añadimos una
etiqueta color: green a los pods.
°° Ahora ya podemos probar el servicio "green" y cuando todo esté
correcto, podremos actualizar el servicio de inventario ya que el selector
contiene color: green.

4. Parte de la información es confidencial y, por tanto, debe proporcionarse a los


servicios a través de secretos de Kubernetes: contraseñas, certificados, clave de
API, identificadores, secretos de clave de API y tokens.
5. La fuente de los valores de secretos puede ser un archivo o un valor codificado
en base-64.

[ 296 ]
Apéndice

Capítulo 12: Ejecución de una aplicación


en contenedor desde el cloud
1. Puedes usar la interfaz de usuario gráfica del portal de Azure, la CLI de Azure
o Azure Resource Templates combinados con una herramienta como Terraform
para aprovisionar un clúster completamente alojado en AKS.
2. Aquí explicamos algunos motivos por los que tendría sentido utilizar Azure
Container Registry para almacenar imágenes:
3. Para evitar una alta latencia al implementar imágenes en un sistema de
producción, es importante almacenarlas en un registro de contenedores
cerca del clúster de Kubernetes, donde se ejecutarán los contenedores
creados a partir de las imágenes.
4. Por motivos de seguridad, es posible que no queramos que los hosts
del clúster de Kubernetes puedan acceder desde el exterior de la red
de Azure para descargar imágenes desde un registro de contenedores
externos como Docker Hub. El ACR puede estar en la misma red
(interna) que el clúster de Kubernetes.
5. El ancho de banda usado para descargar imágenes del contenedor es más
económico si está dentro del mismo centro de datos que cuando se
utilizan conexiones de datos externas.
6. Si queremos centrarnos en un único proveedor y no tratar con varios
proveedores.

3. Para ejecutar una aplicación en contenedor en AKS tenemos que:


1. Aprovisionar un clúster de Kubernetes completamente gestionado en AKS
2. Crear y enviar las imágenes del contenedor al ACR
3. Usar kubectl para implementar la aplicación en el clúster

4. Una forma de visualizar los registros del contenedor o los registros de kubelet
de cualquier nodo de trabajo es establecer una conexión SSH con ese nodo.
Para ello, tenemos que ejecutar un contenedor especial en ese host desde donde
estableceremos una conexión SSH con el host. Posteriormente, podremos usar
una herramienta como journalctl para analizar los registros del sistema
o simplemente ejecutar los comandos normales de Docker en el host para
recuperar los registros del contenedor.
5. Podemos utilizar la CLI de Azure para aumentar o reducir el número de nodos
de trabajo. El comando para hacerlo es:
az aks scale --resource-group=<group-name> --name=<cluster-
name> --node-count <num-nodes>

[ 297 ]
Otros libros que te podrían
gustar
Si te ha gustado este libro, puede que te interesen estos otros libros de Packt:

Docker on Windows
Elton Stoneman
ISBN: 978-1-78528-165-5
ff Comprender los conceptos clave de Docker: imágenes, contenedores, registros
y swarms
ff Ejecutar Docker en Windows 10, Windows Server 2016 y en el cloud
ff Implementar y supervisar soluciones distribuidas en múltiples contenedores Docker
ff Ejecutar contenedores con alta disponibilidad y conmutación por error con
Docker Swarm
ff Dominar la seguridad en profundidad con la plataforma Docker para que tus
aplicaciones sean más seguras
ff Crear un proceso de implementación continua ejecutando Jenkins en Docker
ff Depurar aplicaciones que se ejecutan en contenedores Docker mediante Visual Studio
ff Planificar la adopción de Docker en tu propia organización
299
Otro libro que te podría gustar

Docker for Serverless Applications

Chanwit Kaewkasi

ISBN: 978-1-78883-526-8

ff Aprender qué son las aplicaciones sin servidor y FaaS


ff Conocer las arquitecturas de los tres principales sistemas sin servidor
ff Averiguar cómo las tecnologías de Docker pueden ayudar a desarrollar
aplicaciones sin servidor
ff Crear y mantener infraestructuras FaaS
ff Configurar infraestructuras de Docker para que sirvan como infraestructuras
FaaS on-premises
ff Definir funciones para aplicaciones sin servidor con contenedores Docker

300
Otro libro que te podría gustar

Deja un comentario para que otros


lectores sepan lo que opinas
Comparte tu opinión sobre este libro con otras personas dejando un comentario en el
sitio en el que lo compraste. Si lo compraste en Amazon, danos tu sincera opinión sobre
el libro en la página de Amazon. Esto es muy importante, porque de esta forma otros
lectores potenciales podrán ver y utilizar tu opinión para decidir si lo compran. También
nos sirve para saber qué opinan nuestros clientes sobre nuestros productos y para que
los autores lean tus comentarios sobre el título que han creado en colaboración con Packt.
Solo te llevará unos minutos, pero resultará muy útil para otros clientes potenciales,
nuestros autores y Packt. ¡Gracias!

301

También podría gustarte