¿Automatizar la construcción de tus microservicios? ¡Docker Compose al rescate!

Cuando se tiene una aplicación compuesta de varios microservicios la complejidad de los despliegues se comienza a notar. Docker Compose es una herramienta dentro del ecosistema Docker que nos facilita esta tarea.

Hemos visto anteriormente como dockerizar una aplicación de manera que podemos simplemente dar acceso al repositorio de nuestra aplicación a los compañeros de trabajo y docker se encargará de crear la misma imagen que nosotros tenemos. Pero, ¿Qué pasa si nuestra aplicación va creciendo? Ya no es solo una app que sirve contenido estático, ahora se conecta a la base de datos y expone una API a través de la cual se conectan otros clientes, ¿Y si además la app requiere despachar información en tiempo real por lo que se conecta a una base de datos en memoria como redis?

Vemos que el workflow comienza a enmarañarse, debemos tener por lo menos para este ejemplo 3 contenedores, cada uno corriendo un servicio diferente y además requerimos alguna manera para que se comuniquen entre ellos y manualmente ir corriendo cada contenedor. Si se te olvida estar constantemente limpiando contenedores vas a encontrarte con un montón de ellos consumiendo espacio y recursos en tu máquina local, ahora imaginate la complejidad de montar el pipeline que construye tu aplicación,la vida no es siempre color de rosa.

Microservices Hell

Docker Compose es una herramienta dentro del ecosistema Docker que nos permite automatizar la construcción y enlazado de nuestros contenedores, dándonos un control a mas alto nivel en la administración. Nos permite entre otras cosas realizar despliegues en caliente de mas contenedores para servicios críticos de manera que podamos reaccionar ante picos de alta demanda, esto claro teniendo un balanceador de carga como NGNix que veremos en artículos posteriores.

Tomaremos la app en Node que ya habíamos dockerizado e introduciremos una capa adicional de base de datos en MongoDB, utilizaremos los volúmenes para persistir en base de datos la información que recibamos a partir de la API.

La aplicación se torna un poco mas compleja así que analizaremos sólo las partes mas importantes, veamos la arquitectura que utilizaremos:

Architecture

Como vemos tenemos una arquitectura de 2 módulos y la base de datos, la API interactua con el módulo de app-db de manera que desacoplamos la base de datos y logramos así cambiar la tecnología que estemos usando, en este caso MySql, sin afectar a nuestra API ni a nuestros clientes. Con esta arquitectura deberíamos entonces tener 3 contenedores cada uno con sus configuraciones específicas para luego levantarlos de forma coordinada para no tener ningún problema a la hora de levantar nuestra App, sólo con Docker tendríamos que ir a cada uno de los directorios raíz de los módulos e ir haciendo docker build . y docker run ... teniendo en cuenta las dependencias entre ellos, y sin olvidar crear manualmente los volúmenes necesarios para persistir la información de los contenedores.

Con Compose podemos automatizar y controla de forma organizada todo este proceso, para ello si estamos en linux debemos instalar $ sudo apt-get install docker-compose, esto para las distribuciones basadas en Debian, para mas información acerca de su instalación diríjase a la Página oficial. Una vez con compose instalado debemos crear en la raíz del proyecto un archivo de configuración con nombre docker-compose.yml, en este archivo diseñamos como se deben construir y desplegar nuestros contenedores (orquestación).

Todo el código de este artículo está disponible en una nueva rama del ejemplo en Github

version: "3.2" 

services: # En esta parte definimos nuestros servicios
  db: # Definimos el nombre del servicio de Base de datos
    image: mysql # Usaremos la imagen de MySql disponible en dockerhub
    volumes: # Declaramos un volúmen para almacenar los datos y persistirlos fuera del contenedor
      - 'mysql:/var/lib/mysql' # el volúmen mysql se va a montar dentro de /var/lib/mysql del contenedor
    restart: always # Reiniciar el contenedor cada vez que corremos docker-compose up
    environment: # Variables de entorno
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test
      MYSQL_USER: jsteven
      MYSQL_PASSWORD: jsteven

  app_db:
    build: ./app-db # en este caso usaremos una imagen construida por nosotros mismos
    depends_on: # El contenedor no iniciará hasta que db nos responda que ya se encuentra disponible
      - db
    links: # Le decimos a nuestro contenedor que cuando se haga una petición a db responda nuestra base de datos
      - db
    environment:
      DB_HOST: db
      DB_DATABASE: test      
      DB_USER: jsteven
      DB_PASS: jsteven

  app_api:
    build: ./app-api
    ports: # Exponemos los puertos
      - 3000:3000 # El puerto 3000 de nuestro equipo hará bind 3000 en nuestro contenedor 
    volumes:
      - '.:/usr/src/app' # El directorio raíz del proyecto se montará en el contenedor

volumes: # Declaramos el volúmen para la base de datos
  mysql:

Al estar en formato Yaml es bastante sencillo de leer y entender, estamos describiendo cómo se debe construir cada uno de nuestros servicios y como se conectan entre ellos. Hemos montado todo el proyecto dentro del contenedor de la app_api con el fin de poder que cuando actualicemos o escribamos código automáticamente se refresque en nuestro contenedor.

En el Dockerfile de app_api tenemos:

FROM node:8.9.1-alpine

WORKDIR /usr/src/app/app-api

RUN npm install -g nodemon

RUN npm install

EXPOSE 3000

CMD [ "npm", "run", "start-dev" ]

Una construcción muy parecida a la que ya habíamos visto en el artículo anterior, el código tanto de app_api como app_db es simplemente una API que permite almacenar y obtener videojuegos, el modelo está formado por un nombre y una descripción:

'use strict'

const Sequelize = require('sequelize')
const setupDataBase = require('../lib/db')

module.exports = function setupGameModel (config) {
  const sequelize = setupDataBase(config)

  return sequelize.define('game', {
    uuid: {
      type: Sequelize.STRING,
      allowNull: false
    },
    name: {
      type: Sequelize.STRING,
      allowNull: false
    },
    description: {
      type: Sequelize.TEXT,
      allowNull: false
    }
  })
}

Las ruta disponible para acceder a la API es /api/game y mediante GET o POST podemos obtener todos los juegos y almacenar un nuevo juego respectivamente:

api.get('/games', async (req, res, next) => {
  debug('A request has come to /games')

  let games = []
  try {
    games = await Game.findAll()
  } catch (err) {
    return next(err)
  }

  res.send(games)
})

api.post('/games', async (req, res, next) => {
  debug('A request to post has come to /games')

  let game = req.body
  if (!game.uuid) {
    game.uuid = uuidv4()
  }
  let result
  try {
    result = await Game.createOrUpdate(game)
  } catch (err) {
    return next(err)
  }

  res.send(result)
})

Recuerden que todo el código está disponible en GitHub, solo deben clonar el repositorio y moverse a la rama docker-compose:

git clone https://github.com/stevenyepes/docker-node-example.git
cd docker-node-example
git checkout docker-compose

Una vez hecho esto podemos iniciar todo el aplicativo haciendo:

docker-compose build
docker-compose up -d

Con el primero construimos todos los contenedores y con el segundo los levantamos y dejamos corriendo en background. Para iniciar las tablas y configuración necesaria para la base de datos debemos ejecutar sólo la primera vez:

docker-compose exec app_api node node_modules/app-db/setup.js

Nos dirá que la base de datos será recreada. Una vez hecho esto podemos acceder a localhost:3000/api/games y veremos que nos devolverá un arreglo vacío ([]) esto porque aún no tenemos juegos creados.

Para agregar un nuevo juego usaremos Postman, una herramienta diría que indispensable para testear nuestras APIs. Haremos la siguiente petición:

Postman post

El formato del body para hacer la petición es:

{   
  "name": "Injustice 2",
  "description": "Injustice 2 is the super-powered sequel to the hit game Injustice"
}

Cuando damos en send obtendremos la siguiente respuesta:

Postman response

Podemos seguir guardando juegos respetando los atributos del json de antes y si ahora hacemos GET en vez de POST veremos algo parecido a lo siguiente en la respuesta:

[
    {
        "uuid": "2f45564d-8297-44cf-b7d6-e75819c1c9f7",
        "name": "Final Fantasy IV",
        "description": "A Rol game"
    },
    {
        "uuid": "70e2133f-7397-4e1c-9b39-725e65bc077e",
        "name": "Megaman",
        "description": "A very adictive game"
    },
    {
        "uuid": "8c1042aa-0472-4be3-80cb-1bd86912299e",
        "name": "Injustice 2",
        "description": "Injustice 2 is the super-powered sequel to the hit game Injustice"
    }

Con esto ya tenemos nuestra aplicación usando Docker Compose de manera que sea mas sencillo la administración de esta. Para detener nuestra App simplemente debemos ejecutar: docker-compose down, esto detendrá los contenedores y volúmenes que conforman nuestra aplicación.

Hemos visto como Docker Compose es una herramienta poderosa que nos ayuda a la hora de orquestar nuestros contenedores, aquí pretendo mostrar un ejemplo que dentro de la complejidad que podemos llegar a tener en aplicaciones reales es bastante simple pero ilustra las posibilidades que disponemos para tratar de atacar dicha complejidad. En futuros artículos veremos como integrar NGNix a nuestra aplicación como balanceador para luego poder levantar mas contenedores en caliente de nuestra API dejando que NGNix administre las peticiones realizadas por los clientes, no olvides dejar tus comentarios y decirme si te pareció de ayuda este artículo.

comments powered by Disqus