Entender cómo las diferentes tecnologías de virtualización se integran en la creación de contenedores.
Crear infraestructuras virtuales completas.
Comprender los pasos necesarios para la configuración automática de las mismas.
Una aplicación consiste generalmente en un conjunto de servicios interconectados (y acoplados de forma ligera) que se van a desplegar de forma conjunta; el código que describe la infraestructura debe incluir la descripción de esta estructura para que los despliegues puedan ser fácilmente reproducibles.
En clusters o grupos de contenedores, a veces llamados pods, se consigue que los diferentes servicios ejecutándose en los diferentes contenedores usen una red común, y estén conectados entre sí como si se tratara de servicios ejecutándose simplemente en el sistema operativo. Esto añade una capa de seguridad, pero también de funcionalidad, que no se puede conseguir de otra forma.
En la mayor parte de los despliegues de aplicaciones habrá que crear este tipo de clusters; todos las aplicaciones van a necesitar servicios externos, como mínimo un almacenamiento de logs, adicionalmente a cualquier servicio de datos que vaya a necesitar. Usando esta composición de servicios se pueden hacer pruebas de integración o end to end, o crear entornos de desarrollo que se asemejen lo más posible al entorno de producción final. En general, para pasar a producción habrá que usar herramientas más potentes como Nomad o Kubernetes.
A continuación veremos diferentes formas de crear grupos de contenedores y manejarlos, comenzando con la forma más simple, usar pods.
Como estos clusters son contenedores + red + almacenamiento, haremos una pequeña introducción al uso de las redes virtuales, tal como se usan en Docker y su ecosistema.
Sobre almacenamiento, consultar el tema sobre contenedores, donde se habla de forma extensa del mismo.
Una parte importante de la infraestructura virtual son las redes definidas por software, o SDN (Software Defined Networks). En general, si creas algo que va a actuar como un nodo de la red, también puedes crear redes virtuales que se superpongan a las redes reales, lo que permite otra capa de seguridad y también funcionalidad específica.
Docker es un protocolo de aislamiento, y como tal tiene también una serie de capacidades para crear, manejar y aislar redes virtuales. Docker, por omisión, asigna una MAC y una IP a cada uno de sus contenedores que se estén ejecutando, que forman por tanto un SDN.
Sin embargo, también permite definir el tipo de red que van a usar. Por omisión, el tipo de red que usan se llama bridge, un tipo de red puente que permite que los contenedores se comuniquen entre sí, usando las IPs por omisión que cada uno reciben.
La otra alternativa es host, que hace aparecer todos los servicios en el mismo nodo, y cualquier servicio se puede referir a cualquier otro como localhost.
podman
Podman recibe su nombre, precisamente, del hecho que se puede trabajar con estos pods.
Si no has instalado podman, hazlo ya y recuerda instalarlo para que se ejecute sin root, lo que es imprescindible si quieres manejar usuarios dentro de los contenedores.
Todas las órdenes relacionadas con pod se ejecutan con podman
pod
. Crearemos para empezar un pod y expondremos un puerto del mismo.
podman pod create -n hugitos -p 1415
Esto crea un pod llamado hugitos
y expone el puerto 1415 del
mismo. Es un pod vacío, ahora mismo no hay nada. Para ver todo lo que
se puede hacer ahora mismo, como listar los pods existentes,
ver
este tutorial.
Vamos a añadirle un contenedor con Logstash. Vamos a utilizar este contenedor de Logstash proporcionado por Bitnami. La configuración es la necesaria para que funcione y reciba entradas de un programa externo.
export LOGSTASH_CONF_STRING='input { tcp { port => 8080 codec => json } } output { stdout {} }'
podman run --pod hugitos --rm -dt --env LOGSTASH_CONF_STRING=$LOGSTASH_CONF_STRING --name logstash bitnami/logstash:latest
Con --pod hugitos
añadimos este contenedor al pod que hemos creado
anteriormente; con --dt
lo ejecutamos como daemon, de forma que esté
disponible. En principio, este contenedor estaría ya listo para
ejercer de log de cualquier otro. Así que le añadimos otro:
podman run --pod hugitos --rm -dt jjmerelo/hugitos:test
Con podman logs [número]
se puede acceder a los logs de cada uno de
los contenedores que se han creado.
Nuestro podman pod
maneja de esta forma el grupo de contenedores, y
podemos pararlo con podman pod stop hugitos
. Alternativamente,
podemos arrancar el pod directamente con el primer contenedor que
arranquemos:
podman run -p 31415:31415 --pod new:hugitos --name hugitos_web --rm -dt jjmerelo/hugitos:test
Con esto, además, exponemos el puerto 31415 y lo conectamos al
interior, y le damos un nombre al contenedor para que sea sencillo
acceder a sus logs. La clave para crear el pod es el usar --pod
new:
, que avisa a podman que se trata de un pod; además le asignamos
un nombre al contenedor para que sea más fácil acceder a los
logs. El
Dockerfile incluye
la definición del puerto correspondiente, así como la ejecución de un
servicio web lanzado con Green Unicorn como se indicó en el tema de
microservicios.
Con estas dos órdenes creamos el pod, y además, a partir de él se puede generar la configuración de Kubernetes (que, recordemos, sería necesaria para hacer un despliegue).
podman generate kube -s hugitos
Crear un pod con dos o más contenedores, de forma que se pueda usar uno desde el otro. Uno de los contenedores contendrá la aplicación que queramos desplegar.
docker compose
Docker compose tiene que instalarse de forma individual porque no forma parte del conjunto de herramientas que se instalan por omisión (el daemon y el cliente de línea de órdenes, principalmente). Su principal misión es crear aplicaciones que usen diferentes contenedores, entre los que se citan entornos de desarrollo, entornos de prueba o en general despliegues que usen un solo nodo. Para entornos que escalen automáticamente, o entornos que se vayan a desplegar en la nube las herramientas necesarias son muy diferentes.
docker-compose
es una herramienta que parte de una descripción de
las relaciones entre diferentes contenedores y que construye y arranca
los mismos, relacionando los puertos y los volúmenes; por ejemplo,
puede usarse para conectar un contenedor con otro contenedor de datos,
de la forma siguiente:
version: '2'
services:
config:
build: config
web:
build: .
ports:
- "80:5000"
volumes_from:
- config:ro
La especificación de la versión indica de qué versión del interfaz se
trata. Hay hasta una versión 3, con
cambios sustanciales. En
este caso, esta versión permite crear dos servicios, uno que
denominamos config
, que será el contenedor que tenga la
configuración en un fichero, y otro que se llama web
. YAML se
organiza como un hash o diccionario, de forma que services
tiene
dos claves config
y web
. Dentro de cada una de las claves se
especifica como se levantan esos servicios. En el primer caso se trata
de build
o construir el servicio a partir del Dockerfile, y se
especifica el directorio donde se encuentra; solo puede haber un
Dockerfile por directorio, así que para construir varios servicios
tendrán que tendrán que ponerse en directorios diferentes, como
en este caso. El segundo servicio
está en el mismo directorio que el fichero, que tiene que llamarse
docker-compose.yml
, pero en este estamos indicando un mapeo de
puertos, con el 5000 interno cambiando al 80 externo (que, recordemos,
es un puerto privilegiado) y usando volumes_from
para usar los
volúmenes, es decir, los datos, contenidos en el fichero
correspondiente.
Para ejecutarlo,
docker-compose up
Esto construirá las imágenes de los servicios, si no existen, y les
asignará un nombre que tiene que ver con el nombre del servicio;
también ejecutará el programa, en este caso de web
. Evidentemente,
docker-compose down
parará la máquina virtual.
Podemos reproducir en Docker Compose el “pod” creado anteriormente usando podman y combinarlo con este. Este fichero lo haría:
version: "3.9"
services:
data:
build: data
web:
build:
context: .
dockerfile: nodata.Dockerfile
environment:
LOG_HOST: log
LOG_PORT: 8080
ports:
- "31415:31415"
volumes_from:
- data:ro
log:
image: bitnami/logstash:latest
env_file: ./log.env
ports:
- "8080:8080"
Ha habido que hacer unos pequeños cambios, en el fichero y en los
contenedores. Para empezar, el contenedor anterior incluía también el
fichero de datos que ahora hemos decidido externalizar, por lo que el
nuevo nodata.Dockerfile
sería este:
FROM bitnami/python:3.9
LABEL version="1.0.1" maintainer="JJMerelo@GMail.com"
ARG PORT
ENV PORT=${PORT:-31415}
RUN useradd -ms /bin/bash hugitos
USER hugitos
WORKDIR /home/hugitos
ENV PATH $PATH:/home/hugitos/.poetry/bin
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
ADD pyproject.toml .
ADD HitosIV/* HitosIV/
RUN poetry install
EXPOSE 31415
CMD [ "sh", "-c", "poetry run gunicorn --bind 0.0.0.0:${PORT} HitosIV.hugitos:__hug_wsgi__ --log-file -" ]
La única diferencia es, en realidad, que se elimina la copia al
contenedor, en el directorio /data
, de los datos que se van a
servir.
Que constituyen en este caso la única fuente de verdad, SSOT.
El servicio web
usa ahora dos variables de entorno para establecer
dónde se encuentra el log (y esas variables de entorno se usan desde
el interior del mismo, como es natural).
finalmente, para simplificar un poco el fichero, en el caso de de Logstash usamos un fichero de entorno en vez de definir las variables de entorno. Ese fichero de entorno contiene la definición, en el formato VARIABLE=valor, de las variables de entorno que se usan, en este caso la definición de la configuración de Logstash.
Como en el caso anterior, se puede usar build
para construirlo y a
continuación up
para lanzarlo.
Usar un miniframework REST para crear un servicio web y introducirlo en un contenedor, y componerlo con un cliente REST que sea el que finalmente se ejecuta y sirve como “frontend”.
En este artículo se explica cómo se puede montar un entorno de desarrollo con Python y Postgres usando Docker Compose. Montar entornos de desarrollo independientemente del sistema operativo en el que se encuentre el usuario es, precisamente, uno de los casos de uso de esta herramienta.
La ventaja de describir la infraestructura como código es que, entre otras cosas, se puede introducir en un entorno de test tal como Travis. Travis permite instalar cualquier tipo de servicio y lanzar tests; estos tests se interpretan de forma que se da un aprobado global a los tests o se indica cuales no han pasado.
Y en estos tests podemos usar docker-compose
y lanzarlo:
services:
- docker
env:
- DOCKER_COMPOSE_VERSION=1.17.0
before_install:
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- docker-compose up
script:
- docker ps -a | grep -q web
Como se ve más o menos, este fichero de configuración en YAML
reproduce diferentes fases del test. Después de seleccionar la versión
con la que vamos a trabajar, en la fase before_install
borramos (por
si hubiera una versión anterior) e instalamos desde cero
docker-compose
, y finalmente hacemos un build
usando el fichero
docker-compose.yml
que debe estar en el directorio principal; con
up
además levantamos el servicio. Si hay
algún problema a la hora de construir el test parará; si no lo hay,
además, en la fase script
que es la que efectivamente lleva a cabo
los tests se comprueba que haya un contenedor que incluya el nombre
web
(el nombre real será algo así como web-algo-1
, pero siempre
incluirá el nombre del servicio en compose). Si es así, el test
pasará, si no, el test fallará, con lo que podremos comprobar offline
si el código es correcto o no.
Estos tests se pueden hacer también con simples Dockerfile, y de hecho sería conveniente combinar los tests de los servicios conjuntos con los tests de Dockerfile. Cualquier infraestructura es código, y como tal si no está testeado está roto.
Alternativamente, podemos usar este test exclusivo, usando GitHub actions, para comprobar que se puede construir y funciona
name: Comprobar que docker compose funciona
on: [push,pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Construye el cluster
run: docker-compose up -d
- name: Testea el cluster
run: wget http://localhost:31415/status || exit 1
Esencialmente, lanza el cluster y a continuación hace una petición
sobre el mismo para comprobar que está funcionando, saliendo si wget
devuelve un error. No es que sea muy extensivo, pero fallará si no se
puede construir, por ejemplo.
En producción habría que usar algo más avanzado como Kubernetes.