1. Introducción
Este artículo (1/3) explica de forma más detallada cómo crear una aplicación multi-container con Docker Compose basada en un Tutorial AKS original de Microsoft. Además, proporciona aclaraciones de conceptos fundamentales de Docker más allá de imágenes (images) y contenedores (containers). Entender qué es un servicio en Docker, cómo se comunican los diferentes contenedores o qué infraestructura interna a nivel de networking es creada para interconectar los containers son parte de los objetivos fundamentales de este artículo.
2. Entorno de Trabajo
El entorno en el que se han realizado todos los ejemplos es una Máquina Virtual Windows 2019 Datacenter de Azure con las siguientes características:
- Funcionalidades de Hyper-V y Contenedores de Windows habilitadas.
- Docker Desktop for Windows instalado.
- Máquina virtual con tamaño Standard D2s v3. Docker Desktop for Windows instala una máquina virtual denominada DockerDesktopVM sobre la que se ejecuta el daemon de Docker. Por tanto, para que Docker funcione se debe ejecutar una máquina virtual "anidada" en otra. Esto exige hacer uso de capacidades de virtualización anidada lo cual restringe el tamaño de máquina virtual que debemos elegir. Las series Dv3 y Ev3 admiten esta característica. Por otro lado y como se muestra en el primer punto, Hyper-V debe estar habilitado ya que este componente es el encargado de interactúar con la máquina virtual DockerDesktopVM. Puedes comprobar como esta máquina virtual se inicia o detiene cada vez que inicias o detienes Docker Desktop.
- Linux Subsystem for Windows habilitado. Docker permite seleccionar el tipo de contenedores a utilizar (opciones Switch to Linux Containers o Switch to Windows Containers). Se debe elegir una de ellas, no es posible la selección de ambas al mismo tiempo. El uso de contenedores Linux es masivamente superior al uso de contenedores Windows. Linux Subsystem for Windows permite que los contenedores Linux se ejecuten directamente (sin sobrecargas de máquinas virtuales adicionales) en un subsistema Linux dentro de Windows. Docker Desktop for Windows no debe ser utilizado en entornos de producción. Por tanto, se usará esta configuración solamente en nuestra máquina de "desarrollo" Windows. En "producción", los contenedores serán desplegados en clusters de kubernetes (AKS) sobre máquinas Linux.
3. Consideraciones Generales
A continuación se listan las siguientes consideraciones generales respecto a todo lo explicado en este artículo:
- Se trabajará con Linux containers y Docker Desktop for Windows en nuestra máquina de desarrollo. Por tanto, si no lo has hecho ya, arranca Docker y ejecuta la opción Switch to Linux containers. Ten en cuenta las condiciones explicadas en el capítulo anterior para la correcta ejecución de Linux containers en máquinas Windows.
- La aplicación demo está basada en un Tutorial AKS original de Microsoft al cual se le han modificado fragmentos de código y añadido comentarios adicionales. Cuando lo leí por primera vez, con conocimientos básicos de Docker y Kubernetes, pude completarlo y desplegar la aplicación en Azure Kubernetes Service (AKS) pero eché en falta explicaciones más extensas sobré qué era cada componente, cómo se configuraba o cómo interactuaba con el entorno. El artículo original no se extiende en ese aspecto y a mi entender da por hecho un conocimiento medio de Docker y Kubernetes. Tal vez, la configuración de los ficheros YAML fue la parte que más lagunas me ocasionaba en aquel momento ya que no hay ninguna explicación al respecto más alla de su uso en los despliegues. El objetivo del artículo es proporcionar explicaciones más detalladas de dicha configuración y de los componentes implicados (images, containers, ficheros YAML, services, networks, etc.)
- La aplicación consiste en un frontal web creado con Python sobre un servidor web Ngynx que irá incluida en un primer contenedor de Docker y un backend soportado por una Redis Cache para almacenar datos de forma temporal que irá en un segundo contenedor Docker. La creación de los contenedores y los servicios que permiten su ejecución y comunicación en nuestra máquina de desarrollo se realizará mediante docker-compose.
- Una vez verificado el correcto funcionamiento en desarrollo, se copiaran las imagénes necesarias a un Azure Container Registry propio (artículo 2 de 3) y posteriormente se desplegarán los contenedores en un cluster de Azure Kubernetes Service (artículo 3 de 3).
4. Comandos Básicos
Estos son algunos de los comandos básicos que se usaran en este artículo al usar los diferentes componentes:
Docker
docker images
: permite listar todas las imagenes existentes.
docker ps -a
: permite listar todos (-a
) los contenedores existentes.
docker exec -it <container_name> <command_name>
: permite ejecutar (exec
) comandos (<command_name>
), de forma interactiva (-i
) con pseudoterminal (-t
) dentro de un contenedor (<container_name>
). Lo usaremos para ejecutar comandos shell (sh
) sobre los contenedores Linux.
docker exec <container_id> env
: permite mostrar todas las variables de entorno asociadas al contenedor <container_id>
. Las variables de entorno son usadas para inyectar datos en los contenedores como entrada de variables internas.
docker exec <container_id> env | find "<env_var_name>"
: filtra la variable de entorno <env_var_name>
sobre la lista asociada al contenedor <container_id>
.
docker network ls -f <filter_condition>
: permite listar las redes existentes y filtrar según <filter_condition>
.
docker network inspect <network_name>
: permite obtener detalles de la red <network_name>
.
Docker-Compose
docker-compose up -d
: construye, re-crea e inicia contenedores y servicios (up
) en segundo plano (-d
). Si no se especifica, como en este caso, lee la configuración de un fichero con nombre y ruta por defecto ./docker-compose.yml
.
Redis Cache
docker exec -it <container_name> sh
: permite abrir un shell (sh
) de Linux sobre el contenedor (container_name
) donde se encuentra incluida la Redis Cache. Es el primer paso antes de acceder a redis-cli
.
redis-cli:
command line interface para Redis. Permite ejecutar comandos en Redis y leer las respuestas desde el servidor, todo ello desde un shell o terminal.
keys *:
devuelve la lista de claves incluida en la Redis Cache.
get "key_name":
devuelve el valor asociado a la clave key_name
en la Redis Cache
Redis Command Reference
Otros Comandos
curl <url>
: permite ejecutar conexiones usando diferentes protocolos (HTTP, HTTPs, FTP, etc.) en una red. Lo ejecutaremos sobre un shell para verificar la conexión entre contenedores y servicios disponibles.
5. Configuración, Despliegue y Verificación en local
Una vez descargado el código fuente desde el Tutorial AKS original de Microsoft he modificado la configuración de la aplicación (fichero config_file.cfg), el fichero python main.py y los ficheros de estilos (default.css) para añadir nuevos botones, personalizar los textos asociados a los elementos del interfaz de usuario y los estilos de los componentes visuales. Es una tarea muy sencilla ya que la programación de la aplicación es muy simple y fácil de entender. No requiere de comentarios adicionales e incluso puedes utilizar la aplicación exactamente con el código original.
5.1 Descripción General de la Aplicación
Sin más, si ejecutamos directamente el comando docker-compose up -d
desde la carpeta donde se encuentra el fichero docker-compose.yml
y abrimos el browser con la url http://localhost:8080
podemos acceder a la aplicación:
Haciendo uso del frontend, cada vez que pulsamos en un botón se incrementa su contador asociado o bien se resetean todos ellos en caso de usar la opción Reset. Los valores de los contadores se almacenan en el backend de la Redis Cache.
Si ejecutamos el comando docker ps -a
podemos ver nuestros contenedores junto con la información asociada a cada uno de ellos (Ids, imágenes, estado, etc.). Los nombres de los contenedores, como puede verse en la imagen posterior, son en mi caso jm-inn-azure-vote-front-container
y jm-inn-azure-vote-back-container
. Como se verá posteriormente, entre las modificaciones realizadas en el fichero docker-compose.yml
, está la de asignar sufijos a los nombres de cada componente para saber a qué tipo concreto (imagen, contenedor, servicio, etc.) nos estamos refiriendo en cada momento tan solo viendo el nombre del componente. Asignar nombres idénticos a componentes de distinto tipo es perfectamente válido pero en caso de empezar a aprender una nueva tecnología, a mi parecer, causa confusión.
En resumen, el comando docker-compose up -d
a través del fichero de configuración docker-compose.yml
ha generado todos los componentes incluidos en el frontend y el backend, tanto imágenes (jm-inn-azure-vote-front-image
, redis
), contenedores (jm-inn-azure-vote-front-container
, jm-inn-azure-vote-back-container
), configuración de puertos (ports
), redes (network jm-inn-aks-azure-voting-app-redis_default
) y...servicios, que no se nombran en ningún momento en el artículo original y que son imprescindibles para entender como el contenedor que incluye el frontend puede comunicarse con el contenedor del backend.
5.2 Configuración General de la Aplicación
Entre los aspectos a destacar de la configuración de la aplicación a nivel de código interno para entender la comunicación entre el frontend y el backend está la configuración del cliente de Redis en el fichero python main.py. Unicamente voy a mostrar y comentar las líneas de código relacionadas con este punto:
...
redis_server = os.environ['REDIS']
...
try:
...
r = redis.Redis(redis_server)
r.ping()
except redis.ConnectionError:
exit('Failed to connect to Redis')
- El nombre de host o hostname donde se encuentra instalado el servidor de la Redis Cache es inyectado en la aplicación por medio de la variable de entorno con nombre
REDIS
. Por tanto, una vez llevada la aplicación a un escenario de contenedores, dicha variable de entorno deberá ser creada en el contenedor del frontend y su valor ser una referencia al servidor Redis Cache incluido en el contenedor del backend. Como adelanto, esta referencia es el nombre del servicio que expone el contenedor de backend al resto de contenedores en la red creada por defecto por Docker Compose.
- Conocida la referencia al nombre de servidor de la Redis Cache, se utilizan las librerías de cliente de Redis para python para crear la conexión y gestionar su contenido.
El otro aspecto a comentar es la configuración del fichero Dockerfile para construir la imagen del frontend :
# Imagen base con uWSGI y Nginx para Flask en Python que se ejecuta en un único contenedor.
FROM tiangolo/uwsgi-nginx-flask:python3.6
# PIP es un gestor de instalación de paquetes o módulos en Python
# Instala el cliente de Redis para Python
RUN pip install redis
# Añade el contenido de la carpeta "/azure-vote" del host al directorio "/app" del contenedor
ADD /azure-vote /app
Respecto al
backend servirá una imagen base de
Redis Cache descargada directamente de
Docker Hub por lo que no es necesario ningún fichero
dockerfile adicional.
El resto de código es realmente sencillo y no requiere de explicaciones adicionales.
5.3 Configuración de Servicios y Contenedores
Toda la configuración de servicios y contenedores se realiza en el fichero docker-compose.yml
, por lo que su (casi) completo entendimiento es crucial para comprender todos los componentes y configuraciones que se generan a partir de él. Este es mi fichero modificado con los comentarios oportunos:
version: '3'
# Se crea una red interna para hospedar los servicios configurados.
services:
# Servicio jm-inn-azure-vote-back-service. Se le asigna IP privada y host name.
jm-inn-azure-vote-back-service:
# Descarga la imagen https://hub.docker.com/r/_/redis con la última versión
image: redis
# Establece el nombre del contenedor que incluye el backend
container_name: jm-inn-azure-vote-back-container
# Las peticiones recibidas por la máquina host en el puerto 6379 (6379:)
# son redirigidas al contenedor sobre el puerto 6379 (:6379)
# Redis Cache escucha en el puerto 6379
ports:
- "6379:6379"
# Servicio jm-inn-azure-vote-front-service. Se le asigna IP privada y host name.
jm-inn-azure-vote-front-service:
# Carpeta con el código para construir la imagen del frontend
build: ./azure-vote
# Establece nombre asignado a la nueva imagen
image: jm-inn-azure-vote-front-image
# Establece nombre del contenedor que incluye el frontend
container_name: jm-inn-azure-vote-front-container
# Establece variables de entorno asociadas al contenedor frontend
environment:
# Establece la variable de entorno REDIS
# Asigna como valor el nombre del servicio que expone el contenedor backend
# El nombre del servicio coincide con el hostname asignado en la red interna creada por defecto
REDIS: jm-inn-azure-vote-back-service
# Las peticiones recibidas por la máquina host en el puerto 8080
# son redirigidas al contenedor sobre el puerto 80
# El servidor web escucha en el puerto 80
ports:
- "8080:80"
Como aspecto a destacar docker-compose
crea una red virtual interna donde hospeda los servicios que exponen los contenedores. A cada servicio se le asigna una dirección IP privada y un nombre de host que coincide con el nombre del servicio. Esta infraestructura hace posible la comunicación entre contenedores. Por ejemplo, el primer mensaje de log al ejecutar docker-compose up -d
es Creating network "jm-inn-aks-azure-voting-app-redis_default" with the default driver
. Ahora vamos a ver cómo podemos extraer más información al respecto:
- Con
docker network ls
podemos obtener la lista de redes. Si lo ejecutamos veremos una red con nombre igual al de nuestro proyecto más el sufijo "_default
", en mi caso, jm-inn-aks-azure-voting-app-redis_default
.
- Conocido el nombre de red, ejecutamos
docker network inspect <network_name>
. Obtenemos una salida similar a la mostrada a continuación. En mi caso, se crea una subred con rango de direcciones 172.18.0.0/16
y asigna a los contenedores endpoints con las direcciones IP 172.18.0.3/16
(backend) y 172.18.0.2/16
(frontend):
[
{
"Name": "jm-inn-aks-azure-voting-app-redis_default",
"Id": "6f2359df901d39691ed2c35a7d7ae02d95f45b5701b8711fbbca32695db81440",
"Created": "2020-09-07T09:56:08.85861365Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"3dacd55d97dadb6e61524cf3bcc393f332c78a951876e01c4f092f48eb147dcb": {
"Name": "jm-inn-azure-vote-back-container",
"EndpointID": "df580037f34932b83163c1efd11556361679ff37be901f5face1792402dbb1bb",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
},
"5db8bfab8adec7fd1f702866d6a9a62d83749e2cc3eb8b518921271f32c931fb": {
"Name": "jm-inn-azure-vote-front-container",
"EndpointID": "12720995757e5ba51d2109c66d09a857bbc5f541c8cacd87700ea93caa345618",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "default",
"com.docker.compose.project": "jm-inn-aks-azure-voting-app-redis",
"com.docker.compose.version": "1.26.2"
}
}
]
- Si ejecutamos
docker exec -it <frontend_container_name> sh
para acceder al contenedor que incluye nuestro frontend y a continuación hacemos desde dicho contenedor un ping 172.18.0.2
o ping jm-inn-azure-vote-back-service
para verificar si podemos comunicarnos con el contenedor de backend obtenemos una respuesta positiva:
- También podemos ejecutar desde el contenedor de frontend
curl jm-inn-azure-vote-front-service:80
o curl 172.18.0.3:80
y comprobaremos qué el contenido de la página html es devuelta correctamente verificando el correcto acceso al servicio de frontend.
- Evidentemente si tratas de ejecutar los comandos
ping
o curl
anteriores fuera del scope de los contenedores no funcionarán ya que las direcciones IP y hostnames utilizados sólo tienen sentido en el interior de la red de contenedores. Para acceder desde la máquina host puedes usar curl 127:0:0:1:8080
:
Llegados a este punto, quedaría explicada la infraestructura creada por docker para posibilitar la intercomunicación entre contenedores.
5.4 Verificación de Redis Cache
Ahora vamos a acceder al contenedor que incluye el backend mediante docker exec -it <backend_container_name> y
a continuación redis-cli
para acceder al interfaz de Redis Cache. Una vez en el CLI de Redis podemos usar los comandos keys *
o get "Key_Name"
para listar las claves o consultar el valor de una clave concreta "Key_Name"
. Verificamos que las claves y sus valores asociados son almacenados de forma correcta:
En siguientes artículos se mostrará como subir nuestras imágenes a Azure Container Registry (2/3) y desplegar la aplicación en Azure Kubernetes Service (3/3).
Referencias