ES-ES-Containerize Your Apps With Docker and Kubernetes PDF
ES-ES-Containerize Your Apps With Docker and Kubernetes PDF
ES-ES-Containerize Your Apps With Docker and Kubernetes PDF
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
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.
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.
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.
[i]
Contenido
[ ii ]
Contenido
[ iii ]
Contenido
[ iv ]
Contenido
[v]
Contenido
[ vi ]
Contenido
[ vii ]
Contenido
[ 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 final del libro, podrás practicar con algunos temas más avanzados para
profundizar en tu conocimiento de Docker y Kubernetes.
[ ix ]
Prefacio
[ 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/.
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.
[ xii ]
Prefacio
Convenciones utilizadas
Hay una serie de convenciones de texto que se utilizan a lo largo de este libro.
[ xiii ]
Prefacio
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".
[ 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!
[ 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.
[1]
¿Qué son los contenedores y por qué debo usarlos?
• 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.
[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?
[4]
Capítulo 1
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.
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?
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.
[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 %.
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.
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.
[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.
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:
[ 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.
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.
[ 11 ]
¿Qué son los contenedores y por qué debo usarlos?
Preguntas
Responde a las siguientes preguntas para evaluar tus conocimientos:
[ 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)
[ 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.
[ 15 ]
Configuración de un entorno de trabajo
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.
[ 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:
[ 17 ]
Configuración de un entorno de trabajo
$ 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
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
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.
[ 19 ]
Configuración de un entorno de trabajo
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
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
[ 21 ]
Configuración de un entorno de trabajo
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:
[ 22 ]
Capítulo 2
[ 23 ]
Configuración de un entorno de trabajo
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
[ 24 ]
Capítulo 2
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
[ 25 ]
Configuración de un entorno de trabajo
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.
[ 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.
Inicio de Minikube
2. Ahora, escribe kubectl version y pulsa Intro para ver algo parecido a la
siguiente captura de pantalla:
[ 27 ]
Configuración de un entorno de trabajo
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:
[ 28 ]
Capítulo 2
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.
Preguntas
Tras leer este capítulo, responde a las siguientes preguntas:
Lectura adicional
En el siguiente enlace, encontrarás bibliografía adicional (puede estar en inglés):
[ 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:
[ 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.
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
[ 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.
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.
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:
[ 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
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.
[ 34 ]
Capítulo 3
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.
[ 35 ]
Trabajar con contenedores
Cada respuesta es una cadena con formato JSON con la cita, su autor y su categoría.
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
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.
[ 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:
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).
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.
Ahora, si queremos detener este contenedor , podemos hacerlo con este comando:
$ docker container stop quotes
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.
[ 38 ]
Capítulo 3
Una vez que hemos detenido el contenedor, su estado cambia a Exited (abandonado).
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>
[ 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
[ 40 ]
Capítulo 3
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
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
[ 41 ]
Trabajar con contenedores
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".
[ 42 ]
Capítulo 3
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
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.
[ 43 ]
Trabajar con contenedores
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
Sal del contenedor pulsando Ctrl + C. Esto te desconectará del Terminal y, al mismo
tiempo, detendrá el contenedor 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.
Esto recuperará todo el registro producido por la aplicación desde que existe.
[ 45 ]
Trabajar con contenedores
Este comando solo recuperará los últimos cinco elementos del proceso que se ejecuta
dentro del contenedor producido.
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
El resultado es el siguiente:
Error response from daemon: configured logging driver does not support
reading
Guarda y sal de Vi pulsando primero Esc, escribiendo :w:q y pulsando la tecla Intro.
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)
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:
[ 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.
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:
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
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.
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.
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
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.
Preguntas
Para evaluar el progreso de tu aprendizaje, responde a las siguientes preguntas:
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):
[ 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.
[ 55 ]
Creación y gestión de imágenes de contenedores
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.
[ 56 ]
Capítulo 4
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.
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
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
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.
[ 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".
[ 60 ]
Capítulo 4
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:
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
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
[ 62 ]
Capítulo 4
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
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.
[ 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:
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.
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.
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
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.
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.
• 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.
[ 66 ]
Capítulo 4
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.
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
[ 67 ]
Creación y gestión de imágenes de contenedores
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
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
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.
Enseguida nos encontraremos dentro del contenedor. Escribe exit para salir del contenedor.
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
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.
[ 71 ]
Creación y gestión de imágenes de contenedores
[ 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
[ 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:
[ 74 ]
Capítulo 4
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
[ 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.
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
• 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"]
[ 77 ]
Creación y gestión de imágenes de contenedores
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.
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
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
[ 79 ]
Creación y gestión de imágenes de contenedores
• <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
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.
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.
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
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.
Preguntas
Intenta responder a las siguientes preguntas para evaluar el progreso de tu aprendizaje:
[ 83 ]
Creación y gestión de imágenes de contenedores
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):
[ 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.
[ 85 ]
Arquitectura de aplicaciones distribuidas
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.
[ 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
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 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
[ 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
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
[ 88 ]
Capítulo 5
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:
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
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.
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.
[ 90 ]
Capítulo 5
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)
[ 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
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
[ 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
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
[ 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>
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.
[ 94 ]
Capítulo 5
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?
VOLUME /app/data
VOLUME /app/data, /app/profiles, /app/config
VOLUME ["/app/data", "/app/profiles", "/app/config"]
[ 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
{
"/data/configdb": {},
"/data/db": {}
}
Como se puede ver, el Dockerfile para MongoDB define dos volúmenes en /data/
configdb y /data/db.
[
{
"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.
[ 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:
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.
[ 98 ]
Capítulo 5
[ 99 ]
Arquitectura de aplicaciones distribuidas
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:
[ 100 ]
Capítulo 5
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:
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.
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.
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.
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.
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.
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"
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
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.
Preguntas
Intenta responder a las siguientes preguntas para evaluar el progreso de tu aprendizaje:
[ 106 ]
Capítulo 5
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.
[ 109 ]
Arquitectura de aplicaciones distribuidas
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:
[ 110 ]
Capítulo 6
[ 111 ]
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
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.
[ 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:
[ 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:
[ 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
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.
[ 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.
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.
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.
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
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.
Preguntas
Responde a las siguientes preguntas para evaluar tu comprensión sobre el contenido
de este capítulo.
[ 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.
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.
[ 128 ]
Capítulo 7
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:
[ 129 ]
Conexión en red con un solo host
Redes Docker
¿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:
[ 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
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:
[ 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
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
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
[ 135 ]
Conexión en red con un solo host
[ 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:
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.
[ 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.
[ 138 ]
Capítulo 7
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:
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
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'
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
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
[ 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.
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
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:
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.
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
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.
[ 145 ]
Conexión en red con un solo host
• 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 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:
• 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
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.
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.
[ 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
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):
[ 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.
• 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.
• Declarativo: es una forma en la que podemos resolver problemas sin que sea
necesario que el programador especifique un procedimiento exacto a seguir.
[ 150 ]
Capítulo 8
volumes:
pets-data:
[ 151 ]
Docker Compose
[ 152 ]
Capítulo 8
[ 153 ]
Docker Compose
• 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.
[ 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.
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.
[ 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.
¿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
Si hacemos esto, nos espera una sorpresa. El resultado será similar a la siguiente captura
de pantalla:
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
[ 157 ]
Docker Compose
image: appswithdockerandkubernetes/ch08-db:1.0
volumes:
- pets-data:/var/lib/postgresql/data
volumes:
pets-data:
Resultado de docker-compose ps
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
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.
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>
[ 159 ]
Docker Compose
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.
Preguntas
Para evaluar el progreso de tu aprendizaje, responde a las siguientes preguntas:
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):
[ 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.
[ 161 ]
Orquestadores
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:
[ 162 ]
Capítulo 9
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.
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.
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.
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 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.
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.
[ 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.
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
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.
[ 169 ]
Orquestadores
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.
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
[ 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.
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
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).
[ 173 ]
Orquestadores
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.
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
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
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.
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.
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.
[ 177 ]
Orquestadores
[ 178 ]
Capítulo 9
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.
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.
Preguntas
Para evaluar el progreso de tu aprendizaje, responde a las siguientes preguntas:
Lectura adicional
Los siguientes enlaces proporcionan información más detallada sobre temas relacionados
con la orquestación (pueden estar en inglés):
[ 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
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.
[ 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.
[ 183 ]
Orquestación de aplicaciones en contenedores con Kubernetes
[ 184 ]
Capítulo 10
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:
[ 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.
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.
[ 186 ]
Capítulo 10
En cada nodo, tenemos tres servicios que necesitan ejecutarse, los cuales se describen
de la siguiente manera:
[ 187 ]
Orquestación de aplicaciones en contenedores con 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.
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:
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.
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
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
[ 189 ]
Orquestación de aplicaciones en contenedores con Kubernetes
[ 190 ]
Capítulo 10
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.
[ 191 ]
Orquestación de aplicaciones en contenedores con Kubernetes
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:
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:
[ 192 ]
Capítulo 10
Ahora podemos usar Kubectl para acceder al clúster que Docker para Mac acaba de crear.
Deberíamos ver esto:
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:
• 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.
Podemos probar la aplicación usando, por ejemplo, curl, y veremos que se está
ejecutando según lo previsto:
[ 194 ]
Capítulo 10
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.
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
[ 197 ]
Orquestación de aplicaciones en contenedores con Kubernetes
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.
[ 198 ]
Capítulo 10
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
[ 199 ]
Orquestación de aplicaciones en contenedores con Kubernetes
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:
[ 200 ]
Capítulo 10
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:
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:
[ 201 ]
Orquestación de aplicaciones en contenedores con Kubernetes
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:
[ 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:
[ 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.
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
[ 204 ]
Capítulo 10
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
[ 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:
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.
[ 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
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:
[ 208 ]
Capítulo 10
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
Implementación de Kubernetes
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.
[ 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:
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.
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):
[ 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:
[ 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.
[ 216 ]
Capítulo 11
[ 217 ]
Implementación, actualización y protección de una aplicación con Kubernetes
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
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:
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.
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
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
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
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.
[ 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:
[ 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:
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.
• Actualizaciones graduales.
• Implementaciones blue-green
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.
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
[ 227 ]
Implementación, actualización y protección de una aplicación con Kubernetes
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:
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
[ 228 ]
Capítulo 11
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.
[ 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.
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.
[ 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.
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
[ 232 ]
Capítulo 11
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
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
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.
[ 235 ]
Implementación, actualización y protección de una aplicación con 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:
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
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.
[ 237 ]
Implementación, actualización y protección de una aplicación con Kubernetes
[ 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.
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
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
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.
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
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.
[ 243 ]
Ejecución de una aplicación en contenedor desde el cloud
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.
[ 244 ]
Capítulo 12
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:
1. Abre una nueva ventana del Terminal y ve hasta la subcarpeta ch12 dentro
de la carpeta labs:
cd ~/labs/ch12
[ 245 ]
Ejecución de una aplicación en contenedor desde el cloud
Después de iniciar la instancia del contenedor, aparecerá el siguiente símbolo del sistema:
bash-4.4#
[ 246 ]
Capítulo 12
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
[
{
“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”
}
}
]
[ 247 ]
Ejecución de una aplicación en contenedor desde el cloud
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
[ 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.
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:
[ 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
Ahora intentaremos usar kubectl para obtener todos los nodos del clúster:
kubectl get nodes
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.
[ 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.
[ 252 ]
Capítulo 12
[ 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
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>:
[ 255 ]
Ejecución de una aplicación en contenedor desde el cloud
[ 256 ]
Capítulo 12
Con esta acción, hemos implementado correctamente una aplicación compleja en nuestro
clúster de Kubernetes en AKS.
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
[ 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.
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”
}
[ 259 ]
Ejecución de una aplicación en contenedor desde el cloud
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.
[ 260 ]
Capítulo 12
[ 261 ]
Ejecución de una aplicación en contenedor desde el cloud
3. Haz clic en la entrada pets-oms-workspace para ver los detalles del espacio
de trabajo:
Anota el campo <ID de espacio de trabajo> que acabas de crear para usarlo
en la sección siguiente.
[ 262 ]
Capítulo 12
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.
[ 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:
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.
[ 264 ]
Capítulo 12
[ 265 ]
Ejecución de una aplicación en contenedor desde el cloud
[ 266 ]
Capítulo 12
Espera unos minutos o unas pocas horas para recopilar toda la información relevante
antes de continuar.
Análisis de los registros generados por el clúster de Kubernetes y los contenedores que lo ejecutan
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.
[ 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
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
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]
[ 269 ]
Ejecución de una aplicación en contenedor desde el cloud
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.
azureuser@aks-nodepool1-54489083-0:~$
¡Y ya lo tenemos! Ahora podemos acceder al nodo deseado de nuestro clúster
de Kubernetes de forma remota.
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
[ 270 ]
Capítulo 12
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...
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
...
[ 271 ]
Ejecución de una aplicación en contenedor desde el cloud
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
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
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.
[ 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”
}
[ 274 ]
Capítulo 12
[ 275 ]
Ejecución de una aplicación en contenedor desde el cloud
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
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
[ 277 ]
Ejecución de una aplicación en contenedor desde el cloud
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
[ 278 ]
Capítulo 12
Para crear los artefactos y ejecutarlos en AKS, podemos usar el siguiente comando:
$ azds up
[ 279 ]
Ejecución de una aplicación en contenedor desde el cloud
$ 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
[ 280 ]
Capítulo 12
[ 281 ]
Ejecución de una aplicación en contenedor desde el cloud
[ 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:
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
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
Preguntas
Para evaluar tus conocimientos, responde a las siguientes preguntas:
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):
[ 284 ]
Evaluación
[ 286 ]
Apéndice
rm -rf /var/cache/apk/*
/ # exit
$ docker container commit sample my-alpine:1.0
$ docker container rm sample
FROM alpine:3.5
COPY --from=build /app/bin/hello /app/hello
CMD /app/hello
[ 288 ]
Apéndice
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
[ 289 ]
Evaluación
6. Sal de los dos contenedores y cuando vuelvas al host, ejecuta este comando:
$ docker volume prune
[ 290 ]
Apéndice
[ 291 ]
Evaluación
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",
2. Ejecuta el siguiente comando para mostrar los detalles del servicio en ejecución.
$ docker-compose ps
[ 292 ]
Apéndice
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.
[ 293 ]
Evaluación
[ 294 ]
Apéndice
[ 296 ]
Apéndice
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
Chanwit Kaewkasi
ISBN: 978-1-78883-526-8
300
Otro libro que te podría gustar
301