Docker Para El Abuelo (Parte 3).
(Docker en la Vida Real)
Pre-requisito
Haber completado la segunda parte del minicurso de Docker para principiantes, publicado en el siguiente enlace
Docker Networks
En esta sección vamos a ver un poco de NETWORKING, si… la mala palabra jaja, networking es todo un mundo, muy complejo y de mucho contenido, y resulta imposible ver todos los conceptos en un par de posts, así que vamos a simplificarlo muy a alto nivel, quedate tranqui que no vamos a ver nada de subneteo, ACL, iptables ni nada de eso. Para el desarrollo de esta sección de networking aplicado al mundo Docker, vamos a enfocarnos un hecho simple:
- Para que dos máquinas puedan comunicarse entre sí, deben estar ambas en la misma red (esto es, que tengan las dos máquinas la misma netmask), no importa si hablamos de máquinas físicas, virtualización tradicional, docker, pods etc, si no están en la misma red, no se van a poder hablar entre sí.
En el mundo Docker, podemos correr containers conectados a múltiples redes, también podemos usar los valores por default que nos ofrece Docker o crear nuestras propias redes, sean internas o externas para customizarlas.
Cuando instalamos Docker en nuestro Debian, se crearon varias redes por default, vamos a analizar esto con el siguiente comando:
$ docker network ls
La red de tipo bridge que vemos en la imagen, está relacionada a nuestra interfaz docker0 de nuestro SO, veamos tirando un ifconfig en la terminal:
Los otros dos tipos de Docker Networks (host y null) no nos interesan por el momento, lo único que tenemos que tener en cuenta, es nunca borrarlas ya que son utilizadas por Docker Daemon, la red de tipo bridge es la que se asigna a todos nuestros contenedores por default, a menos que especifiquemos otra red, vamos a mirar un poco la configuración de esta red con el siguiente comando:
$ docker network inspect bridge
Con docker network inspect [nombre-de-red] podremos ver la configuración de cualquier Docker Network que tengamos en nuestro SO, en este caso el NAME de la red que queremos ver es también bridge.
Vemos que nos arroja mucha configuración quizás a bajo nivel, lo importante en este punto es resaltar que cada vez que ejecutamos algún comando docker run docker daemon agrega nuestro container automáticamente a esta red por default; si vemos el atributo Containers notamos que está vacío, ya que en este momento no tengo ningún contenedor levantado, bueno, levantemos un container, por ejemplo un redis (una bd muy útil y liviana con motor de almacenamiento clave-valor, que actualmente es muy usado por su acceso rápido a la información), levantamos el container con:
$ docker container run --rm -itd -p 6379:6379 --name redis redis:4.0-alpine
NOTA: con el flag –rm estamos diciendo al docker daemon que cuando termine la ejecución del container, se borre toda la información relacionada con su ejecución; con el flag -itd estamos indicando que el container se ejecute en modo interactivo y en modo background de lo contrario nos aparecerá una consola en modo debug de redis; con -p estamos exponiendo el puerto 6379 puerto por default de redis, lo exponemos tanto desde el container como desde nuestro host, con name le damos un nombre a nuestro container y por último indicamos la imagen docker que queremos utilizar.
Ahora, si volvemos a tirar el network inspect a la red bridge, veremos que en el atributo Containers aparece nuestro redis que acabamos de levantar
Muy bien, vamos a agregar un jugador más levantando nuestra app flask que construimos en el post anterior con:
$ docker run --rm -itd -p 5000:5000 --name dockerflask dockerflask:latest
NOTA: si quedaron contenedores levantados del lab anterior o de alguna otra prueba que hayas hecho anteriormente, recorda hacer un clean de todo (por si llega a haber algún conflicto entre containers que se pisen) con docker container ls o con docker ps con docker container stop container-id paramos la ejecución de un container y con docker container rm container-id lo borramos (poniendo los primeros 2 o 3 caracteres del container id docker daemon ya se da cuenta que debe borrar).
Ahora deberíamos ver dos containers dentro de nuestra Docker Network llamada bridge:
Como vimos al principio de esta sección, dos containers van a poder comunicarse entre ellos si pertenecen a la misma red, en nuestro caso, tenemos ambos containers dentro de la red llamada bridge, así que vamos a ver si se pueden comunicar tirando un ping de un container al otro, para esto, vamos a obtener las IP locales de cada container por separado (vamos a hacerlo aunque ya sabemos por el inspect cuales son), lo hacemos con los comandos:
$ docker exec redis ifconfig
$ docker exec dockerflask ifconfig
IMPORTANTE: en la imagen docker que utilizamos para construir nuestra imagen dockerflask, no tenemos instalado algunos paquetes necesarios para que el exec del ifconfig funcione, ya que está basada en una imagen ligera de python (slim), por lo cual vamos a ir a nuestro dockerfile construido en el post anterior y vamos a agregar la siguiente línea debajo del RUN pip install… RUN apt-get update && apt-get install -y net-tools iputils-ping, realizamos un re-build de nuestra imagen con ese cambio, y la levantamos nuevamente, con esto debería funcionar el ifconfig en nuestro dockerflask.
En mi caso las IP de redis y dockerflask son 172.17.0.2 y 172.17.0.3 respectivamente, en tu caso pueden ser otras direcciones IP, bien, ahora vamos a verificar que nuestra red bridge está funcionando correctamente tirando un ping desde nuestro server redis a nuestro server flask y viceversa, lo hacemos con:
$ docker exec redis ping 172.17.0.3
$ docker exec dockerflask ping 172.17.0.2
Como vemos, funciona en ambas direcciones, ahora vamos a mirar el contenido de nuestro archivo hosts dentro del container redis con:
$ docker exec redis cat /etc/hosts
vemos que tenemos mapeada la ip de nuestro server redis contra un hash, en realidad este hash no es otra cosa que el layer-ID de nuestro container redis, lo podemos verificar tirando un ls y viendo que ambos ID’s coinciden:
Esta es la “magia” que relaciona nuestra docker network bridge con el container en cuestión, pero si quisiéramos usar nuestro server redis desde nuestra app flask en el mundo real deberíamos harcodear nuestro archivo hosts del container dockerflask incluyendo una línea para que cuando este llame a “redis” sepa a qué IP y layer-ID redirigir, algo muy tedioso de mantener ya que si la IP cambia, vamos a tener que realizar el cambio manualmente dentro de cada container.
Por suerte, podemos resolver esta cuestión, creando nuestra propia red de tipo bridge usando Docker-CLI, automáticamente cuando levantemos nuestra propia Bridge Network Docker Daemon va a configurar un DNS por nosotros, para conectar los containers usando solamente el NAME y no solo eso, cada vez que cambien la IP o el NAME del container, el DNS se actualizará automáticamente, de esta forma nos olvidamos de mantener registros DNS o harcodear cosas en el container, en otras palabras Docker nos provee un DNS de containers que se actualiza de manera automática sin intervención del admin, algo ESPECTACULAR.
Ahora, cuántos files .conf hay que modificar, cuántos paquetes hay que instalar, o cuantas líneas de código hay que meter para lograr todo esto? gracias a Docker, lo podemos hacer con una sola línea en nuestra terminal:
$ docker network create --driver bridge mired
NOTA: con el driver bridge solo podremos conectar nuestros containers con un único Host (nuestra laptop), para trabajar con múltiples Dockerhost’s necesitaríamos trabajar con el driver Overlay.
Con esto ya creamos nuestra propia Docker Network de tipo bridge y la llamamos “mired”, vamos a ejecutar un inspect sobre la red que acabamos de crear.
Vemos que aún no tiene ningún container asociado, entonces, vamos a parar nuestros containers que actualmente están asociados a la red bridge por default y vamos a asociarla a nuestra red “mired”, veamos el paso a paso:
Paramos los containers:
$ docker container stop redis
$ docker container stop dockerflask
Los volvemos a levantar pero esta vez agregando el flag –net y referenciando a la red que creamos anteriormente:
$ docker container run --rm -itd -p 6379:6379 --name redis --net mired redis:4.0-alpine
$ docker container run --rm -itd -p 5000:5000 --name dockerflask --net mired dockerflask:latest
Como vemos, ya tenemos nuestros containers dentro de nuestra Docker Network Bridge llamada “mired”
Ahora vamos a verificar que nuestro DNS automático funciona correctamente, haciendo ping hacia los contenedores usando su nombre en lugar de su dirección IP:
$ docker exec redis ping dockerflask
$ docker exec dockerflask ping redis
Hasta acá lo básico de Docker Networks, en este punto ya tenemos una idea de como trabaja y sabemos crear networks de tipo bridge, algo muy útil por ejemplo si quisiéramos aislar nuestra pruebas en redes distintas a la que viene por default.
Data Volumes
Un poco de teoría:
Cuando hablamos de Data Volumes en Docker, simplemente estamos hablando, por ejemplo en una de sus implementaciones, de un directorio mapeado al DockerHost que es montado y usado por un contenedor para guardar información o por el mismo host para intercambiar información con el contenedor, generalmente Data Volumes o Docker Volumes se usa en contenedores docker para la persistencia de información.
Vamos a explicar brevemente la manera en la que docker gestiona el storage de la info, veamos la siguiente imagen (fuente: Docker Oficial):
Concretamente vemos que existen tres tipos de formas de almacenamiento:
-
Tmpfs: Es simplemente un espacio de almacenamiento en memoria (almacenamiento temporal), por lo general se usa para el almacenamiento de configuraciones, si el contenedor muere, todo lo que se encuentre en tmpfs también lo hace.
-
Bind Mounts : este espacio de almacenamiento existe dentro del filesystem de nuestro host (o DockerHost) y es accedido desde el container en un punto de montaje específico, que lo indicamos directamente en nuestro comando RUN con el flag -v, se puede mapear prácticamente a cualquier sitio del filesystem de nuestro host y por ende realizar cualquier modificación siempre y cuando tengamos los permisos necesarios (a diferencia de los Volumes) se implementa de la siguiente manera:
$ docker run -v /path/de/mi/host:/path/del/contenedor ...
-
Volumes: Docker se encarga de la gestión del almacenamiento completamente, los volúmenes se almacenan en /var/lib/docker/volumes y solo el Docker Daemon tiene permisos para modificar archivos en dicho path, dentro de los Data Volumes podemos diferenciar también dos tipos:
1) Anonymous Volume : Docker se encargará de administrar este tipo de data volumes, sin embargo, si necesitas referenciar el volumen a largo plazo no te lo recomiendo, ya que al ser un volumen anónimo Docker también se encargará de ponerle nombre, el cual es un hash larguísimo que se complica a la hora de invocarlo,si esto no es un problema, o si no necesitas poner un label al volumen, podes usar data volumes anónimos con:
$ docker run -v /path/del/contenedor ...
2) Named Volume : es prácticamente lo mismo que un volumen anónimo, Docker Daemon se encargará de gestionar el almacenamiento del mismo en un path específico de Docker Daemon (lo veremos en la siguiente sección), pero si usamos Named Volumes vamos a poder referenciarlos con el label que queramos, primero debemos crearlo y posteriormente vamos a poder agregarlo al comando RUN también con el flag -v y usando el nombre que le dimos anteriormente:
$ docker volume create mivolumen
$ docker run -v mivolumen:/path/del/contenedor ...
Diferencia entre los ID’s de un Anonymous Volume y un Named Volume:
NOTA: todos los casos explicados anteriormente también se pueden definir en las instrucciones de un dockerfile, de esta forma nos ahorramos tirar comandos Docker interminables con muchos parámetros.
Un poco de práctica
En esta sección vamos a enfocarnos en dos casos prácticos, usaremos Data Volumes para tener persistencia de datos en nuestro Host y luego en otro ejemplo un Volumen Anónimo para compartir datos entre containers (sin pasar por el DockerHost) de esta manera veremos un poco mejor la utilidad de los volúmenes docker:
Ejemplo usando Bind Mounts
Vamos con un ejemplo sencillo, un apache sirviendo una página estática, pero en lugar de que apache se valga de muchos files de un proyecto web dentro del contenedor para servir el sitio (por ejemplo metiendo el codigo con un ADD o con COPY en un Dockerfile, vamos a dejar todo el código del sitio fuera del container, en la ubicación que más nos guste y vamos a mapear dicho directorio hacia el DocumentRoot de Apache usando Bind Mounts.
Primero descargamos la imagen oficial de Apache desde DockerHub:
$ docker pull httpd:latest
Después creamos un directorio donde va a vivir el código de nuestro sitio (puede ser un framework, un wordpress o lo que uds quieran) para el ejemplo voy a servir únicamente un file static html, que por ahora vamos a dejarlo vacío:
$ mkdir projects/poc-apache
$ touch projects/poc-apache/index.html
Ahora lo más importante, el comando docker run:
$ docker run -itd --name demo-apache -p 8080:80 -v /home/manu/projects/poc-apache/:/usr/local/apache2/htdocs/ httpd:latest
Que hicimos: corremos el container en segundo plano, le ponemos un label, decimos que todas las conexiones que ingresen al host en el puerto 8080 las redireccione al puerto 80 del contenedor, y luego usamos bind mounts para “relacionar” el directorio que acabamos de crear con el DocumentRoot del Apache de nuestro contenedor, finalmente, pasamos la imagen oficial de apache que acabamos de descargar.
Si probamos visualizar el sitio (en el browser con 0.0.0.0:8080) veremos que funciona, aunque la página esté vacía, bien, ahora podemos fijarnos con docker ps que nuestro container está levantado correctamente; sin parar el container vamos a editar nuestro index.html en nuestro host con el siguiente contenido:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Aprendiendo Docker desde Cero</title>
</head>
<body>
<h1>Aprendiendo a usar Data Volumes en Docker</h1>
</body>
</html>
Damos F5 en el browser y veremos que la modificación que hicimos en el host, como vimos en la teoría, de esta manera podemos referenciar absolutamente cualquier sitio dentro del filesystem de nuestro host y hacer cualquier modificación siempre y cuando tengamos los permisos de hacerlo; gracias a esto también evitamos que la información que está sirviendo apache, se pierda al morir el container, y también nos evitamos tener que estar transfiriendo files muy grandes a nuestro container o imagen desde un dockerfile, manteniendo la imagen de apache en sus 132 MB.
Ejemplo usando Named Volumes
Para este ejemplo vamos a usar la imagen oficial de Docker de Postgres, pero solo vamos a dejar dentro del container el motor de la BD, dejando las bases de datos y tablas almacenadas en un *Named Volume que crearemos a continuación.*
Creamos nuestro named volume y le damos el nombre que más nos guste con :
$ docker volume create vol-postgres
Luego vamos a listarlo y a verlo un poco más de detalle con los siguientes comandos:
$ docker volume ls
$ docker volume inspect vol-postgres
Hemos creado nuestro Named Volume, y como vemos en el output del comando inspect, el volumen tiene un Mountpoint que es donde van a vivir todos los named volumes y anonymous volumes, el path es /var/lib/docker/volumes (el _data del final es una cuestión más de postgres que de docker), este tipo de volumes resulta ideal para trabajar con bases de datos ya que en ningún momento vamos a sobrecargar al container con datos y configuraciones ni con excesivos comandos ADD o COPY en nuestros dockerfiles; y al mismo tiempo, no vamos a preocuparnos por perdida de datos en caso de que se pierdan containers, ya que las tablas de nuestra base de datos estarán en el dockerhost; dicho directorio es administrado únicamente por Docker Daemon, por lo cual no vamos a poder modificar ningun archivo ni configuración como en el caso anterior (a menos que seamos usuario root)
Ahora que ya tenemos nuestro volumen, vamos a usarlo, primero hacemos un pull de la imagen oficial de postgres:
$ docker pull postgres:latest
Luego levantamos el container con los siguientes parámetros:
$ docker run -itd --name pg-docker -v vol-postgres:/var/lib/postgresql/data -p 5432:5432 postgres
Que hicimos: levantamos el contenedor en segundo plano, lo tagueamos con un nombre que nos guste, y luego con el flag -v referenciamos nuestro named volume apuntando al path del contenedor donde postgres aloja los datos (esta info la saque directamente en las notas de DockerHub donde se indica en un apartado como se debe usar un named volume en dicha imagen), por último seteamos los puertos tanto del container como del host y le decimos en qué imagen nos vamos a basar para levantar el container, la imagen que acabamos de descargar.
Ahora, probemos realmente si la persistencia es real, lo que sigue a continuación no es puramente docker pero nos servirá para comprobar la persistencia y ver cómo trabaja un named volume, abrimos una consola dentro del contenedor, luego nos logueamos al motor de la BD y crearemos una base de datos, luego veremos cómo se comporta nuestro volumen:
Abrimos una consola bash, como root, dentro del container con:
$ docker exec -it pg-docker bash
Entramos a la CLI de postgres y una vez dentro creamos la BD con:
# psql -U postgres
postgres=# CREATE DATABASE pgtest;
ahora en otra terminal, con el usuario root de nuestra máquina, vamos a explorar el lugar en el directorio de postgres (a su vez dentro del directorio de data volumes de docker) donde el motor guarda todas las bases de datos:
# ls /var/lib/docker/volumes/vol-postgres/_data/base
Veremos solamente unos cuantos id’s pero nada legibles, ahora, para asociar estos números a los nombres de las bases de datos vamos a volver a nuestra consola de postgres dentro de nuestro contenedor y vamos a ejecutar:
postgres=# SELECT datname,oid from pg_database;
Veremos al instante la relación entre los archivos de configuración del motor de base de datos corriendo en nuestro contenedor con los archivos dentro del volumen que creamos anteriormente, si queres podes verificarlo tirando un DROP DATABASE pgtest; y luego tirar nuevamente el ls en el volumen para ver cómo efectivamente la BD se borra de este último respondiendo a la configuración del motor dentro del container.
En conclusión, tenemos un container Postgres trabajando con datos dentro de un Named Volume de Docker, esto nos asegura la persistencia de las bases de datos y sus configuraciones a pesar de la condición efímera de un container.
Último Ejemplo: Compartir Data Volumes entre Containers
Ahora, supongamos que no necesitamos, o no queremos, que los datos pasen por el DockerHost, y solamente necesitamos que un par de containers compartan info entre sí sin tener que pasar por el DockerHost.
Un caso de uso puede ser una app web que necesite compartir sus assets (CSS,JS etc) con un web server (Apache o Nginx), para el ejemplo vamos a usar nuestro Dockerfile de Flask con una ligera modificación:
Como vemos en la imagen, debemos agregar la linea VOLUME [“/app/public”] y también un directorio public con dos archivos, un css y un js, no hay problema si no sabes javascript y CSS, los archivos están vacíos y es simplemente a modo de ejemplo, básicamente lo que hacemos con la instrucción VOLUME del dockerfile es indicar que el path que le pasamos como parámetro lo “comparta” en forma de volumen docker, luego de haber hecho las modificaciones del dockerfile, vamos a re-buildear la imagen con dichos cambios y a levantar el container:
$ docker build -t dockerflask:latest .
$ docker run -itd --name dockerflask -p 5000:5000 dockerflask
Ahora vamos a descargar y levantar una imagen de Nginx pero, con un flag adicional:
$ docker pull nginx:latest
$ docker run -itd --name mi-enginex --volumes-from dockerflask nginx
El flag es –volumes-from nombre-de-container lo que hacemos es decirle a nuestro contenedor Nginx que use absolutamente todos los volúmenes que tenga definido nuestro container flask, ahora podemos abrir una consola bash en nginx y verificar que efectivamente podemos ver los assets que están publicados desde el contenedor flask:
si hacemos un cambio del contenido en el primer container debería impactar automáticamente en el segundo, ya que el segundo está leyendo la información del volumen creado por dockerflask.
Docker Compose
Como te habrás dado cuenta, a medida que vamos aprendiendo nuevos conceptos en el mundo docker, y aplicándolos, nuestro comando docker run se vuelve bastante largo y tedioso de escribir, y esto teniendo en cuenta que hasta ahora solo realizamos pequeñas pruebas con máximo dos containers, imagínense un escenario más real en un entorno de microservicios con 10 o 15 contenedores docker comunicándose entre sí, con varias docker networks y muchos data volumes dando vueltas.
Acá es donde aparece Docker Compose, una tool del ecosistema de docker para correr aplicaciones multicontenedores y realizar la administración de los mismos sin tener que escribir instrucciones larguísimas con muchos flags por línea, con docker-compose podremos administrar muchos containers al mismo tiempo y también configurar la manera en la que van a estar interactuando entre ellos.
Ejemplo 1: Wordpress, deploy básico (2 contenedores)
Como primer caso de uso vamos a levantar un wordpress, para ello usaremos dos imágenes, una de wordpress y otra de mysql, pero en lugar de armar un dockerfile para wordpress, otro para mysql, levantar docker volumes, pasarle parámetros de configuración etc, vamos a definir absolutamente todo en nuestro docker compose.
En el directorio que más nos guste, vamos a crear un archivo llamado docker-compose.yml con el siguiente contenido:
version: '3.3'
services:
db:
image: mysql:5.7
volumes:
- db_data:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: miwordpress
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
wordpress:
depends_on:
- db
image: wordpress:latest
ports:
- "8000:80"
restart: always
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
volumes:
db_data: {}
En el compose file, lo primero que definimos es la versión de la API de docker compose que vamos a usar (veremos más adelante de que se trata esto), luego definimos dos containers, en la jerga de docker compose, definimos “2 servicios” uno llamado db, en el que vamos a usar una imagen mysql:5.7, un named volume llamado db_data apuntando al directorio de mysql en el container, y algunas variables de configuración, por supuesto pasar las credenciales de una BD en un file en texto plano no es para nada una buena práctica, la forma correcta es pasar esa info usando secrets de docker, pero a modo de ejemplo lo vamos a dejar así (de hecho el código es de un ejemplo de la página oficial de docker). El segundo service que definimos lo llamamos wordpress y está basado en la última imagen oficial de wordpress, definimos los puertos del host y del container, y agregamos un parámetro muy interesante, el parámetro depends_on, que pasandole el atributo db, estamos diciéndole a docker-compose, que para iniciar el servicio wordpress, primero tiene que iniciar la base de datos, un parámetro muy útil cuando tenemos contenedores que dependen de otros para arrancar, por último se pasan variables de configuración, y por fuera de los services, se definen los volumes que utilizamos más arriba en el service db, lo definimos seteando un objeto vacío
Ahora vamos a levantar todo el stack, con sus configuraciones, networks y volumes con un solo comando:
$ docker-compose up -d
Si ejecutamos el comando up pero sin el flag -d veremos como docker comienza a construir todo el entorno, hace pull de las imágenes y comienza a configurar ambos servicios, levanta los volumes que indicamos en nuestro yml y además también va a levantar una Docker Network; al finalizar vamos a poder ver nuestro server wordpress en funcionamiento en el browser en 0.0.0.0:8000
Ingresando a http://0.0.0.0:8000 vamos a poder terminar de setear nuestro WordPress
Si volvemos a nuestra terminal podemos ver que docker-compose creó no solo el volumen que indicamos en la configuración, también creó una docker network, una maravilla, si queremos parar los containers y limpiar todo, lo hacemos con el comando:
$ docker-compose down
Por otro lado también vamos a dar de baja los data volumes creados con:
$ docker-compose down --volumes
Ejemplo 2 : Arquitectura de Microservicios
Vamos a explorar un ejemplo un poco más real y mejor aún, más representativo acerca de la arquitectura de microservicios, en futuros posts vamos a explicar un poco mejor de qué se trata esto de microservicios, le dedicaremos un post completo a todo lo que es Microservices, Kubernetes y CI/CD Pipelines, pero básicamente la idea es descomponer una aplicación típicamente monolítica (poner todo en una sola caja), en varias aplicaciones interactuando entre sí, generalmente agrupados por funcionalidad o a veces también por dominio del problema, fomentando la escalabilidad, rollings updates, menos o nulos downtimes etc.
El ejemplo es una aplicación web que se compone de 6 containers, o servicios en docker-compose, el stack es Flask, NodeJS, C#, dotNET, postgreSQL y Redis; dichos servicios van a interactuar utilizando diferentes Docker Networks; este no es un stack definido por mí, es simplemente un ejemplo que esta en linea y es un ejemplo oficial escrito por el equipo de Docker así que lo único que vamos a hacer es clonar el repo, analizar un par de files y levantarlo para ver cómo funciona
Básicamente son dos aplicaciones, una en la que vas a poder votar entre perros y gatos, y una segunda aplicación en donde vas a poder ver los resultados de la votación en tiempo real.
La Arquitectura
A los bifes
Clonamos el repo:
$ git clone https://github.com/dockersamples/example-voting-app.git
Veamos el contenido de docker-compose.yml (el proyecto tiene muchos docker-compose para diferentes plataformas pero vamos a enfocarnos en este) :
version: "3"
services:
vote:
build: ./vote
command: python app.py
volumes:
- ./vote:/app
ports:
- "5000:80"
networks:
- front-tier
- back-tier
result:
build: ./result
command: nodemon server.js
volumes:
- ./result:/app
ports:
- "5001:80"
- "5858:5858"
networks:
- front-tier
- back-tier
worker:
build:
context: ./worker
depends_on:
- "redis"
networks:
- back-tier
redis:
image: redis:alpine
container_name: redis
ports: ["6379"]
networks:
- back-tier
db:
image: postgres:9.4
container_name: db
volumes:
- "db-data:/var/lib/postgresql/data"
networks:
- back-tier
volumes:
db-data:
networks:
front-tier:
back-tier:
Según lo que aprendimos hasta ahora, podemos hacer una lectura rápida del archivo, tenemos 5 services, 1 data volume y 2 docker networks, perfecto, sentite libre de analizar el código de la app y de hacer pruebas con otras plataformas, ahora vamos a levantarlo en nuestro debian con:
$ docker-compose up --build -d
Listo!!, ahora, vamos a ver la app de votación en el puerto 5000 y la app de resultados en el puerto 5001 de nuestro DockerHost
Quizás la app no es muuuuyyy útil pero su arquitectura es un ejemplo muy bueno acerca de cómo interactúa una aplicación multicontainer basada en microservicios, te invito a que explores el repo, obviamente sin ponerte a codear con dotNET, NodeJS o Python, sino, que analices cada Dockerfile que vas a encontrar en las subcarpetas de cada microservicio, en este punto del curso ya no deberías tener mayores problemas al leer un dockerfile, ya que aprendimos lo básico y el resto esta en linea, muy buen trabajo y no te olvides de limpiar todo tirando un down al compose y eliminando los volumes y networks.
Docker Swarm
Docker Swarm es básicamente el cluster manager que trae Docker de manera nativa, lo podemos administrar con Docker CLI sin instalar nada adicional, y tampoco necesitamos instalar nada extra para orquestar containers, con swarm vamos a tener todo lo necesario, load balancers, scaling, services etc, algo muy similar pero un poco más completo es lo que hace Kubernetes (k8s) pero no discutiremos eso por ahora.
Docker Swarm expone una API la cual usamos para para administrar la asignación de recursos y configuraciones de los containers que corren dentro de cada nodo, y lo mejor de todo es que docker swarm nos permite realizar la administración de todo esto como si se tratase de un solo container Docker.
Algunos Conceptos:
Docker Swarm se utiliza para administrar un cluster de containers, es decir, vamos a tener una máquina que va a correr todo lo necesario para la administración del cluster y delegación de recursos (el swarm manager o master) y una o más máquinas diferentes que van a tener el rol de workers o nodos del cluster que corren Services, estos servicios a su vez se exponen mediante un ingress load balancer básicamente es esa la arquitectura junto con algunos conceptos que vamos a ir entendiendo mejor a continuación.
Armando Nuestro Docker Swarm usando Docker in Docker:
Típicamente Docker Swarm se monta con varios DockerHosts (sean máquinas virtuales o físicas), una de ellas para el rol de Master y el resto para los Workers, a modo de ejemplo vamos a usar algo llamado Docker in Docker (o dind) un container docker, que a su vez corre docker en su interior, algo muy útil para pruebas, pero NO es recomendable para producción ya que trae varios problemas técnicos a bajo nivel, para hacer esto Docker provee una imagen especial que deben correr en modo privilegiado, ahora lo vemos en un rato.
Vamos a levantar un cluster con un Master y 3 workers:
Levantamos los pseudos-dockerhosts con dind pero antes creamos una docker network para trabajar con ella:
$ docker network create --driver bridge mi-swarm
$ docker run -itd --privileged --name swarmaster --net mi-swarm -d docker:dind
$ docker run -itd --privileged --name worker01 --net mi-swarm -d docker:dind
$ docker run -itd --privileged --name worker02 --net mi-swarm -d docker:dind
$ docker run -itd --privileged --name worker03 --net mi-swarm -d docker:dind
Ya tenemos nuestros hosts corriendo Docker, de hecho si abrimos una consola en uno de ellos (docker exec -it swarmaster sh) podremos comprobar que efectivamente tienen instalado Docker:
Configurando el Master, Los Nodos y un visualizador del cluster
Abrimos una consola en el master:
$ docker exec -it swarmaster sh
Iniciamos el Swarm Mode:
# docker swarm init
Esto nos mostrará la IP de nuestro master y el comando exacto que hay que usar para unir a los demás nodos a nuestro cluster, esto se hace usando un token que también nos proporciona el master, entonces copiamos el comando y lo pegamos en todos nuestros workers, como se muestra en la siguiente imagen del nodo 1:
Ahora, volviendo a la consola del master vamos a ejecutar el siguiente comando para ver el estado de los nodos:
# docker node ls
*Ya tenemos nuestro swarm corriendo, con un master y 3 workers, ahora que tenemos levantado nuestro cluster, el paso siguiente, es configurar los services que nuestro docker swarm debe proveer, antes que esto, vamos a levantar un servicio que no tiene nada que ver con la operación del cluster en sí, sino más bien con el monitoreo del comportamiento del clúster, el servicio es un visualizer, lo levantamos en el master con: *
docker service create \
--detach=true \
--name=clusterMonitor \
--publish=8000:8080/tcp \
--constraint=node.role==manager \
--mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
dockersamples/visualizer
Lo verificamos con:
# docker service ls
NOTA: recuerden verificar hasta que el estado del servicio sea 1/1 en la columna réplica.
Ahora si vamos al browser y colocamos la IP de nuestro master:8000 vamos a ver un monitor en tiempo real de nuestro cluster swarm, en mi caso 172.24.0.2:8000 y como veremos, ya nos muestra el servicio que acabamos de levantar y en qué nodo está corriendo.
DEPLOY Ahora vamos a deployar algo más funcional, por ej, un web server, y vamos a decirle a nuestro cluster que queremos 2 réplicas del server, y veamos el comportamiento en nuestro visualizador:
# docker service create --name mi-enginex --replicas=2 -p 80:80 nginx:latest
Tardará unos instantes hasta ponerse en verde, pero ya tenemos deployado nuestro container nginx en dos workers y con un solo comando, y un load balancer automatico a cargo de don Docker Swarm, si queremos también podemos examinar el deploy con:
# docker service ls
# docker service inspect mi-enginex --pretty
Y con:
# docker service ps mi-enginex
Veremos los layers ID de los containers y los ID s de los workers en donde están corriendo actualmente
ESCALAMOS?
Agregar más réplicas es tan sencillo como:
# docker service scale mi-enginex=3
Veremos los cambios al instante en el visualizador, y si queremos exagerar un poco con las réplicas:
# docker service scale mi-enginex=15
Vemos que las instancias se replican y se reparten entre los nodos sin problemas.
SE ROMPIÓ UN NODO
Como estamos usando Docker in Docker, vamos a simular que un nodo tuvo un problema, borrandolo directamente, ya saben borrar containers así que hagan la prueba y verán en el monitor del cluster que los containers que estaban corriendo en el nodo que falló se van a distribuir entre los nodos vivos, es decir, seguimos con 15 réplicas de nuestro web server.
Muy bien, hasta acá lo básico de Docker Swarm, en un siguiente post vamos a analizar las diferencias principales con Kubernetes, y por su puesto se viene el post K8s para Principiantes para el que esté arrancando desde cero con todo lo referido a la orquestación de containers.
Listo, estamos…
Hasta acá llegó este minicurso de Docker para principiantes, luego de estos posts ya tenes las bases para seguir creciendo en el mundo de los containers, la buena y mala noticia al mismo tiempo, es que todo lo que vimos hasta acá, es NADA, los escenarios en la vida real son mucho más complejos y grandes por supuesto, pero espero que este curso te haya servido como una introducción al mundo de Docker.
Qué aprendiste??:
- Arquitectura de Docker (cómo funciona)
- Docker Images
- Docker Containers
- Docker Registry
- Build Process (Dockerfiles)
- Docker Networks
- Docker Volumes
- Docker Compose
- Docker Swarm
- Y de yapa, un poco de python usando Flask jaja
Les dejo una pequeña lista de comandos recurrentes de Docker CLI, al menos recurrentes en mi día a día:
Comandos:
$ docker pull [imagen] (descarga imagen de registry)
$ docker ps -a (lista los contenedores)
$ docker history [container] (tira eventos de un container en particular)
$ docker images ls
$ docker network ls
$ docker volume ls
$ docker build (los parámetros lo vimos en la parte 2 del curso)
$ docker exec -it [container] [comando] (ejecuta un comando dentro de un container, uso muchooooo)
$ docker inspect [container] (tira una descripción de la implementación del container)
$ docker stop [container]
$ docker rm [container]
$ docker rmi [imagen]
$ docker container logs [container] -f (tira los logs de un container con un livetail )
$ docker system prune -f (borra la caché del prune)
$ docker system prune -a ( ojo con este es jodidisimo, borra imágenes containers todo, limpia todos los inodos)
$ docker-compose build
$ docker-compose pull
$ docker-compose up
$ docker-compose down
$ docker-compose logs -f
$ docker-compose restart
$ docker-compose exec
$ docker-compose rm -f
$ muchos más y ni hablar de las combinaciones de flags y parámetros etc,
$ no te olvides que con "docker --help" podes explorar todas las opciones de Docker CLI