Docker Compose

En el artículo Creación de imágenes para Docker  vimos como crear imágenes para Docker, pero si necesitáramos configurar una aplicación compleja que ejecuta múltiples imágenes, la mejor manera de hacerlo es usar Docker Compose. Con Docker Compose podríamos crear un archivo de configuración en formato yaml y juntar los diferentes servicios y las opciones específicas para ejecutarlos de una sola vez. Como todos los cambios se almacenan en un archivo de configuración, la ejecución solo es aplicable a contenedores en un único host de Docker. Docker Compose no se instala de forma predeterminada, por lo que hay que instalarlo según indica en la documentación https://docs.docker.com/compose/install/

Vamos a usar la misma aplicación que todos usan para demostrar Docker. Es una aplicación simple pero completa desarrollada por Docker para demostrar las diversas características disponibles en la ejecución de una pila de aplicaciones en Docker. Así que primero nos familiarizamos con la aplicación porque estaremos trabajando con la misma aplicación en diferentes secciones durante el resto de entradas. Puede acceder a su repositorio en https://github.com/dockersamples/example-voting-app 

Ejemplo

Es una aplicación de votación que proporciona una interfaz para que un usuario vote y otra interfaz para mostrar los resultados. La aplicación consta de varios componentes, como la aplicación de votación, que es una aplicación web desarrollada en Python para proporcionar al usuario una interfaz para elegir entre dos opciones: gato o perro.

Cuando realiza una selección, el voto se almacena en redis, una base de datos en la memoria. El voto es procesado por el “worker”, que es una aplicación escrita en .net. y que toma el nuevo voto y actualiza la base de datos persistente que es un SQL de Postgres que tiene una tabla con el número de votos para cada categoría. El resultado de la votación se muestra en una interfaz web que es otra aplicación web desarrollada en nodeJS que lee el recuento de votos de la base de datos PostgreSQL y lo muestra al usuario.

La aplicación está construida con una combinación de diferentes servicios, diferentes herramientas de desarrollo y múltiples plataformas, como Python, NodeJS, .Net, etc. Vamos a ver como esta aplicación se usará para mostrar cómo configurar una pila de aplicaciones completa que consiste en diversas competencias en Docker, nos permite mantener a un lado los servicios y las pilas. Veremos cómo podemos crear esta pila de aplicaciones en un solo motor Docker primero con los comandos de ejecución de Docker y luego con Docker-compose. Supongamos que todas las imágenes de las aplicaciones ya están compiladas y están disponibles en el repositorio de Docker. 

Construcción

Comencemos con la capa de datos primero, ejecutando el comando “docker run -d –name=redis redis” para iniciar una instancia de redis, agregando el parámetro -d para ejecutar este contenedor en segundo plano y también lo nombraremos, ya que es importante. En segundo lugar implementaremos la base de datos PostgreSQL ejecutando el comando “docker run -d –name=db postgres:9.4“ también con la opción -d y nombrándolo. A continuación comenzaremos con los servicios de la aplicación. Así que ejecutamos una aplicación front-end para la interfaz de votación ejecutando una instancia de imagen de la aplicación de votación con “docker run -d –name=vote -p 5000:80 voting-app” y asignando un nombre.

Ya que se trata de un servidor web, tiene una instancia que se ejecuta en el puerto 80 y que publicaremos en el puerto 5000 en el sistema host para que podamos acceder a él desde un navegador. A continuación, implementaremos la aplicación web de resultados que muestra los resultados al usuario. Para esto, implementamos un contenedor utilizando la imagen de la aplicación de resultados y publicamos el puerto 80 al puerto 5001 en el host para que de esta manera, podemos acceder a la interfaz de usuario web de la aplicación resultante en un navegador: “docker run -d –name=result -p 5001:80 result-app”. Finalmente implementamos el “worker” ejecutando “docker run -d –name=worker worker“

Ahora todo esta ejecutándose, pero no puede funcionar, ya que no hemos vinculado entre sí los diferentes contenedores. No hemos dicho a la aplicación web que use la instancia de redis, ya que podría haber múltiples redis ejecutándose. Y tampoco  hemos dicho al “worker” que use la base de datos PostgresSQL. Es por esto que usamos enlaces. 

Enlaces

Link es una opción de línea de comando que se puede usar para vincular dos contenedores. Por ejemplo, el servicio web de la aplicación de votación depende del servicio de “redis”, así que agregamos una opción link al ejecutar el contenedor de la aplicación de votación para vincularlo al contenedor de redis: “docker run -d –name=vote -p 5000:80 –link redis:redis voting-app”. Es por esto por lo que renombramos todos los contenedores, que en realidad está creando una entrada en el archivo /etc/hosts en el contenedor de la aplicación de votación que agrega una entrada con el nombre de host “redis” y una IP interna del contenedor de “redis”.

De manera similar, tendremos que agrega un enlace para que la aplicación resultante se comunique con la base de datos, ya que internamente intenta conectarse a la base de datos de un host llamado “db”. Así que agregamos una opción de enlace para referir la base de datos con el nombre db: “docker run -d –name=result -p 5001:80 –link db:db result-app”. Finalmente, la aplicación “worker”necesita acceso tanto a la redis como a la base de datos de Postgres, por lo que agregamos dos enlaces a la aplicación de trabajo, un enlace para vincular redis y el otro enlace para vincular la base de datos postgres: “docker run -d –name=worker –link db:db –link redis:redis worker”.

El uso de enlaces de esta manera está en desuso y el soporte se puede eliminar en el futuro. Esto se debe a que los conceptos avanzados y más nuevos de Docker-swarm y la creación de redes admiten mejores formas de lograr lo que acabamos de hacer aquí con enlaces, pero es bueno ver la base, ya que es fácil generar un docker para componer archivos a partir de estos conceptos.

Creación de fichero Yalm para Docker Compose

Para ello creamos un diccionario de nombres de contenedores. Vamos a usar los mismos nombres que usamos en los comandos de ejecución de Docker. Cogemos todos los nombres y creamos una clave con cada uno de ellos:

redis:
db:
vote:
result:
worker:

Luego, debajo de cada elemento, especificamos qué imagen usar. La clave es “image” y el valor es el nombre de la imagen que se usará a continuación. 

redis:
    image: redis
db:
    image: postgres:9.4
vote:
    image: voting-app
result:
    image: result-app
worker:
    image: worker

Inspeccione los comandos y vea cuáles son las otras opciones utilizadas. Publicamos puertos, así que vamos a mover esos puertos debajo de los contenedores respectivos. Entonces creamos una propiedad llamada “ports” y enumeramos todos los puertos que le gustaría publicar debajo de ella. 

redis:
    image: redis
db:
    image: postgres:9.4
vote:
    image: voting-app
    ports:
        – 5000:80
result:
    image: result-app
    ports:
        – 5001:80
worker:
    image: worker
Finalmente también ponemos los enlaces, por lo que a cualquier contenedor que requiera de enlaces debe tener una propiedad “links” y el listado de enlaces. Si el enlace y el nombre del host es el mismo, podemos crear el enlace sin los dos puntos: poner “redis” es similar a poner “redis:redis”.

redis:
    image: redis
db:
    image: postgres:9.4
vote:
    image: voting-app
    ports:
        – 5000:80
    links:
        – redis
result:
    image: result-app
    ports:
        – 5001:80
    links:
        – db
worker:
    image: worker
    links:
        – redis
        – db

Ya hemos terminado con nuestro archivo de compilación, solo hay que ejecutarlo con la orden “docker-compose up”. 

Mejoras

De los 5 componentes, 2 de ellas son imágenes que sabemos que ya están disponibles en Docker Hub (redis y postgres), pero las tres restantes son de nuestra propia aplicación. No tienen porque estar compilados y disponibles en el registro de Docker. Podemos enseñar a Docker como ejecutar la construcción en lugar de intentar extraer una imagen. Para esto podemos reemplazar la linea “image” con una línea “build” y especificar la ubicación de un directorio que contiene un archivo de Docker con instrucciones para compilar la imagen:

image: voting-app → build: ./vote
image: result → build: ./result
image: worker → build: ./worker

En este ejemplo, para la aplicación de votación, todo el código de la aplicación está en una carpeta llamada “vote” que contiene todo el código de la aplicación y el archivo Docker. Cuando se ejecute el comando “docker-compose up”, primero compilará las imágenes con un nombre temporal y luego usará esas imágenes para ejecutar contenedores utilizando las opciones que especificó antes.  

Ahora veremos diferentes versiones de Docker compose file. Esto es importante porque puede ver a Docker componer archivos en diferentes formatos en diferentes lugares y preguntarse por qué algunos se ven diferentes. Docker compose evolucionó con el tiempo y ahora admite muchas más opciones que al principio. Por ejemplo, esta es la versión recortada del archivo compuesto Docker que utilizamos anteriormente:

redis:
    image:redis
db:
    image: postgres:9.4
vote:
    image: voting-app
    ports:
        - 5000:8080
    links:
        - redis

De hecho, esta es la versión original del archivo compuesto Docker conocido como versión 1. Esto tenía una serie de limitaciones, por ejemplo, si desea implementar contenedores en una red diferente a la red puente predeterminada. No había forma de especificar que en esta parte del archivo también se dice que tiene una dependencia u orden de inicio de algún tipo. Por ejemplo, el contenedor de la base de datos debe aparecer primero y solo entonces se debe iniciar la aplicación de votación.

Versión 2

El soporte para estos vino en la versión 2. En la versión 2 hasta el formato del archivo también cambió un poco. Ya no especifica su información de pila directamente como lo hizo antes. Todo está encapsulado en la sección “services”, así que se crea una propiedad llamada “services” en la raíz del archivo y se mueve todos los servicios debajo

version: 2
services:
    redis:
        image:redis
    db:
        image: postgres:9.4
    vote:
        image: voting-app
        ports:
            – 5000:8080
        links:
            – redis

Para la versión 2 se debe especificar la versión en la parte superior del archivo para que docker-compose sepa que formato está utilizando, ya que la versión dos se diferencia en la creación de redes: en la versión uno docker-compose conecta todos los contenedores que ejecuta a una red puente predeterminada y luego usa enlaces como lo hicimos antes. Con la versión 2 docker-compose crea automáticamente un red puente dedicada para esta aplicación y luego conecta todos los contenedores a esa nueva red. Así todos los contenedores pueden comunicarse entre sí utilizando el nombre del servicio del otro. Así que básicamente no necesita usar enlaces en la versión 2 de docker-compose. Gracias a esto puede deshacerse de todos los enlaces de la version 1:

version: 2
services:
    redis:
        image:redis
    db:
        image: postgres:9.4
    vote:
        image: voting-app
        ports:
            – 5000:8080

Dependencia

Finalmente la versión 2 también presenta una función dependiente si desea especificar un orden de inicio. Por ejemplo, digamos que la aplicación web de votación depende del servicio de redis.Por lo tanto, debe asegurarse de que el contenedor de redis se inició primero y solo entonces se debe iniciar la aplicación web de votación. Así podríamos agregar que depende de la aplicación de redis. 

version: 2
services:
    redis:
        image:redis
    db:
        image: postgres:9.4
    vote:
        image: voting-app
        ports:
            – 5000:8080
        depends_on:
            – redis

Luego viene la versión 3, que es la última y que es similar a la versión 2. Esto significa que especifica la versión en la parte superior y una sección de servicios en la que coloca todos sus servicios al igual que en la versión 2. Hay que asegurarse de especificar el número de versión como 3. Viene con soporte para Docker Swarm.

Redes en Docker Compose

Hablemos sobre las redes en Docker Compose para volver a nuestra aplicación. Como hemos visto, hasta ahora hemos estado implementando todos los contenedores en la red puente  predeterminada. Supongamos que modificamos un poco la arquitectura para contener el tráfico de las diferentes fuentes. Por ejemplo, nos gustaría separar el tráfico generado por el usuario del tráfico interno de las aplicaciones, por lo que creamos una red front-end dedicada al tráfico de los usuarios y una red back-end dedicada al tráfico dentro de la aplicación. Luego conectamos las aplicaciones de cara al usuario, que son la aplicación de votación y la aplicación de resultados, la red frontend y toda la competencia a una red interna de back-end, así que de vuelta en nuestro archivo de redacción de Docker. Tenga en cuenta que en realidad he eliminado la sección de puertos por simplicidad, todavía están allí, pero simplemente no se muestran aquí.

Lo primero que debemos hacer si vamos a usar redes es definir las redes que vamos a usar. En nuestro caso dos redes, una front-end y otra back-end, así que creamos una nueva propiedad llamada “ntworks” en el nivel raíz:

version: 2
services:
    redis:
        image:redis
    db:
        image: postgres:9.4
    vote:
        image: voting-app
    result:
        image: result
networks:
    front-end:
    back-end:

Para cada servicio hay que crear una propiedad de redes y proporcionar una lista de redes a las que se debe conectar. También debe agregar una sección para el “worker”. 

version: 2
services:
    redis:
        image:redis
        networks:
            – back-end:
    db:
        image: postgres:9.4
        networks:
            – back-end:
    vote:
        image: voting-app
        networks:
            – front-end:
            – back-end:
    result:
        image: result
        networks:
            – front-end:
            – back-end:
    worker:
        image: worker
        networks:
            – front-end:
            – back-end:
networks:
    front-end:
    back-end: