Docker, una ballena con muchos contenedores

TL;DR docker esta siendo utilizado cada vez mas como herramienta principal para hacer despliegues de proyectos de software creando entornos para cada una de nuestras necesidades y conectandolas gracias a compose, en el siguiente serie de post aprenderás a utilizar docker y compose para desplegar una aplicación de django.

Hey, ¡hola todos!, despues de varios dias sin pasar por aca hoy he decidido hablar sobre una herramienta muy poderosa que utilizo casi que a diario en mi trabajo, ¡Si! docker. Hemos logrado pasar nuestra infraestructura de servicios a containers “dockerizados” para dar flexibilidad y armonia entre todos nuestros entornos, tanto de producción como de test.

Pero… ¿Y que es docker?

La idea detras de docker es un software que cree “contenedores” de software y portables que aprovechen las ventajas de una maqunia creando pequeñas “maquinas virtuales” de linux que aprovechan  y así separar todo lo instalado dentro de el del sistema operativo huesped. Normalmente estas maquinas corren sistemas Linux muy muy reducidos en peso solo con lo necesario para desplegar nuestros servidores. Además ayuda a “homogenizar” nuestros entornos, esto ayuda a depurar los problemas que muchas veces son de configuraciones especificas en producción y que no aparecen en otros servidores o nuestros entornos locales.

¿Que ventajas tiene Docker?

  • Al ser contenedores son mas livianos que una maquina virtual, además son mas portables que esta, debido que no son dependientes del hardware de la maquina host, ni su sistema operativo.
  • Otra ventaja importante de docker es su sistema de versionamiento, que va creando “snapshots” de los cambios que hemos realizado en nuestra manquina de manera que reconstruirla sea mucho mas sencillo y rapido y no necesite descargar todo o comenzar desde 0, esto ayuda a que los despliegues sean mucho mas rapidos.
  • Su gran repositorio de imagenes, muchos desarrolladores de software como mongo, redis, rabbit, incluso django ya poseen imagenes oficiales de las cuales podemos basarnos para crear nuestros propios despliegues.

Está es una definicion corta pero sustanciosa de Docker, ahora vamos a la instalación, en el ejemplo estaré utilizando un VPS de DigitalOcean.

usaremos el comando

$ sudo apt install docker.io

Screen Shot 2018-06-16 at 12.06.47 AM.png Una vez hecho esto ya tenemos docker instalado podemos ver la version con el comando “docker version” les recomiendo que se haya instalado una version 1.13 o mayor, paso 1 listo veamos que sigue.

¡Bajando mi primera imagen de docker!

Listo, tenemos docker, y ahora… ¿Que hacemos con el? Primero entendamos que son las imagenes, docker utiliza un sistema de imagenes de sistemas operativos para basarse en ellos y crear los containers, por ahora bajaremos una imagen sencilla “ubuntu”, para ellos primero buscamos la imagen dentro del repositorio de imagenes de docker con el comando:

$ docker search ubuntu

como resultdo obtendremos cuales son las imagnes de ubuntu y basadas en este que podemos utilizar.

Screen Shot 2018-06-16 at 12.17.46 AM.png

Como podemos ver hay muchas imagenes de Ubuntu, pero nosotros utilizaremos la oficial, podemos ver cuales son oficiales en la columna “oficial” que tienen un [OK] y en nuestro caso le diremos a docker que descargue la “ultima” version que tenga de ubuntu con el “tag” latest de la siguiente manera:

$ docker pull ubuntu:latest

Screen Shot 2018-06-16 at 12.20.26 AM.pnglisto, ya descargamos nuestra imagen, nos cercioraremos cuales imagenes se han descargado con el siguiente comando:

$ docker images

¡Ok!, una imagen de ubuntu de 81.2 MB… si… contenedores ligeros a eso me refería. Perfecto ahora intentemos levantar esta imagen y entremos en una consola de esta imagen de ubuntu.

Screen Shot 2018-06-16 at 12.25.45 AM.png

perfecto! dentro de la imagen de ¡ubuntu!, paso 2 realizado. ¡Moving on!

¿Como paso de una imagen a un container en docker?

Listo ya entendímos el concepto de una imagen, que es un SO muy muy minificado, ahora ¿como realizamos un container a partir de eso?.

Para ello vamos a crear un archivo Dockerfile, con instrucciones de todo lo que debe instalarse, llevar, configurar, etc nuestro entorno.

El primer paso es decirle a Docker desde que “imagen” se va a construir nuestro contenedor con el comando From:

FROM ubuntu:latest

Perfecto, esto lo que realizará es una descarga de la imagen de ubuntu (la ultima gracias a nuestro tag latest).

A continuación podemos correr los comandos para instalar lo necesario, por ejemplo instalar django con PIP, para ello le diremos a ubuntu que actualice primero sus paquetes con apt y luego realizaremos la instalación de django con pip.

RUN apt update
RUN apt-get install wget python -y
RUN wget https://bootstrap.pypa.io/get-pip.py
RUN python get-pip.py
RUN pip install django

Listo, por ultimo vamos a realizar una construcción de nuestro container

$ docker build ./

Screen Shot 2018-06-21 at 10.52.45 AM.png

una vez realizada la construcción veremos como paso a paso docker va a ir ejecutando dentro de esta nueva imagen cada uno de los comandos que dejamos arriba, y al terminar, tendremos una imagen para crear nuestro ¡contenedor!

Creemos en nuestro servidor un proyecto de django en una nueva carpeta.

Screen Shot 2018-06-21 at 11.13.56 AM.png

¡Perfecto!, ahora haremos unos cambios en el docker file para que tome como directorio nuestra app recien creada, para ello utilizaremos los comandos WORDIR y ADD para decirle a docker a que directorio vamos a copiar nuestro codigo. Ademas utilizaremos el comando EXPOSE para decirle a docker que puerto va a exponer desde el container hacia el exterior y por ultimo el comando CMD que ejecutará un comando cuando se utilize el comando docker run para levantar nuestro contenedor.

El archivo Dockerfile quedará así:

Screen Shot 2018-06-21 at 12.05.34 PM.png

listo una vez hecho esto podemos re construir nuestra imagen, con docker build, pero esta vez utilizaremos el parametro -t para darle un tag/nombre a nuestra nueva imagen.

Screen Shot 2018-06-21 at 11.39.26 AM.png

Perfecto, ahora corramos nuestra imagen con docker run y veamos como nos resulta.

Screen Shot 2018-06-21 at 12.06.37 PM.png

Utilizando el comando docker run -p 80:8000 helloworld. Donde -p es un argumento para enmascarar un puerto de la maquina host por uno expuesto en la imagen (ese que utilizamos con expose), en nuestro caso lo redireccionamos al puerto 80, y helloworld es el tag que utilizamos para construir nuestra imagen, listo docker corriendo, ¿ahora que es compose?

Screen Shot 2018-06-21 at 12.06.31 PM.png

Y ahora, que entre ¡docker compose!

podriamos tardarnos mas tiempo en hablar de otros comandos de docker y sobre como crear imagenes personalizadas, pero se extendería mucho este post!. Así que, al grano.

Docker compose es una herramienta para definir multiples containers y correrlos. para esto compose utiliza el formato YAML para configurar cada uno de los servicios que se necesitarán en nuestro despliegue.

Utilizar docker compose es tan sencillo como seguir estos 3 pasos:

  1. Define la imagen de tu contenedor para tu app con un Dockrfile
  2. Definimos nuestros servicios (como bases de datos, brokers, postrfix, etc) en un archivo docker-compose.yml
  3. Levantar todos tus contenedores con docker-compose up

¿Sencillo no?, primero vamos a definir que vamos a utilizar, para trabajar con Django en nuestro entorno necesitamos 1.) Una base de datos, que en nuestro caso utilizaremos postgres. 2.) Una imagen con django, crearemos una propia desde 0 para utilizar django y configurarlo con wsgi 3.) Algunas veces utilizamos Celery, por ello vamos a crear una imagen para celery. 4.) Para utilizar Celery normalmente necesitamos un broker de mensajes, para ello utilizaremos la imagen de rabbitmq.

Perfecto! entonces ya conocemos nuestra infraestructura, ahora manos a la obra:

con esto vamos a comenzar a escribir nuestro docke-compose.yml

version: '3'
services:
  web:
    '''aqui va todo lo de django'''
  rabbitmq:
    '''aqui va todo lo de rabbitmq'''
  postgres:
    '''aqui va todo lo de postgres'''
  celery:
    '''aqui va todo lo de celery'''

¡Pefecto!, esta es la primera estructura de nuestro archivo, ahora entremos a cada uno de los servicios de manera detallada:

Primero comencemos con los servicios que no necesitamos personalizar mucho, por ello comenzamos con rabbitmq y postgres.

postgres:
    image: postgres:latest
    hostname: db
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=qandasystem
    ports:
      - "5432:5432"

primero vemos el apartado de image, en el vamos a seleccionar que imagen de docker vamos a utilizar, en nuestro caso podemos utilizar la imagen de postgres version 10 que es el tag latest al momento que escribo este post. El segundo apartado es el hostname que utilizaremos para identificar este contenedor dentro de la red de docker que se creará entre los diferentes servicios. Siguiente entrada son las variables environments con environment, en el podemos crear las variables de entorno que necesita el docker par ainiciar, en el caso de postgres utilizamos el postgres_user para el usuario, el postgres_password y el postgres_db para saber como le llamaremos a la base de datos. Por ultimo utilizamos ports para saber cual puerto se expondrá dentro de la red y hacia afuera. puedes ver mas configuraciones de entorno en la página oficial de la imagen de docker haciendo click aqui.

rabbitmq:
    hostname: rabbit
    image: rabbitmq:3.6.0
    environment:
      - RABBITMQ_DEFAULT_USER=admin
      - RABBITMQ_DEFAULT_PASS=mypass
    ports:
      - "5672:5672"  # we forward this port because it's useful for debugging
      - "15672:15672"  # here, we can access rabbitmq management plugin

Segundo al igual que postgres, vanmos con rabbitmq. Utilizaremos igualmente la imagen de rabbit con tag 3.6.0, su hostname sera colocado como rabbit, se colocará los password y usuarios a través de las variables de entorno y expondremos los puertos 5672 para conectarse a rabbit y 15672 para conectarse a la interfaz de administración de rabbit.

Perfecto vamos con los contenedores de Celery y Django, estos nos darán un poco mas de trabajo. Recuerdan aquel archivo dockerfile, vamos a necesitarlo. En el mismo directorio donde estamos creando el archivo docker-compose.yml vamos a pegar el Dockerfile (si podemos llamarlo Dockerfile.django sería mejor para controlar si necesitamos mas de un dockerfile). Y así quedará nuestro código dentro del servicio django.

django:
    build:
      context: .
      dockerfile: Dockerfile.django
    volumes:
      - ./app/:/app/
    depends_on:
      - rabbitmq
      - postgres
    ports:
      - 8000:8000

Perfecto, vemos algunas entradas nuevas. Primero veamos build, para docker-compose, el apartado de build le dice que necesita para construir la imagen, context refiere al directorio donde se encontrarán los archivos de construcción y dockerfile refiere a el archivo Dockerfile que se hará para crear la imagen. El apartado de volumes copiara el contenido de la carpeta de la izquierda dentro de la maquina huesped a la carpeta del contenedor del lado derecho. Por ultimo depends_on nos dice que contenedores se necesitan creados para el contenedor actual, y esto realiza una conexión de red entre los contenedores que se encuentran aquí y el container a construir.

celery:
    build:
      context: .
      dockerfile: Dockerfile.django
    volumes:
      - ./app/:/app/
    depends_on:
      - rabbitmq
      - postgres
    entrypoint: ['celery','worker','--app=appname','--loglevel=DEBUG','--logfile=/tmp/celery.log','--pidfile=/tmp/celery.pid','-Ofair']

Por ultimo describiremos el contenedor de celery. Si, es muy parecido a django, de hecho utiliza la misma imagen, solo cambia el “entrypoint”, que es el comando que se ejecutará y quedara como “demonio” una vez se levante el contenedor, así que perfecto, ahora veamos como queda todo el archivo.

version: '3'
services:
  django:
    build:
      context: .
      dockerfile: Dockerfile.django
    volumes:
      - ./app/:/app/
    depends_on:
      - rabbitmq
      - postgres
    ports:
      - 8000:8000

  rabbitmq:
    hostname: rabbit
    image: rabbitmq:3.6.0
    environment:
      - RABBITMQ_DEFAULT_USER=admin
      - RABBITMQ_DEFAULT_PASS=mypass
    ports:
      - "5672:5672"  # we forward this port because it's useful for debugging
      - "15672:15672"  # here, we can access rabbitmq management plugin

  postgres:
    image: postgres:latest
    hostname: db
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=qandasystem
    ports:
      - "5432:5432"

  celery:
    build:
      context: .
      dockerfile: Dockerfile.django
    volumes:
      - ./app/:/app/
    depends_on:
      - rabbitmq
      - postgres
    entrypoint: ['celery','worker','--app=appname','--loglevel=DEBUG','--logfile=/tmp/celery.log','--pidfile=/tmp/celery.pid','-Ofair']

Listo, nuestros servicios estan descritos, ahora solo basta con hacer el comando docker-compose up para probar nuestro servicio con docker.

¡Perfecto!, por ahora dejaremos por aca pero continuaré en un nuevo post, donde realizaremos algunas mejoras como correr django con uwsgi en vez de correr el server de development de django y haremos un ejemplo mas practico. ¿Me pregunto, les gustaría un video? ¡dejenmelo saber en los comentario! Muchas gracias y hasta la proxima 😁