Docker


La vida de un DevOps está llena de palabros raros. Uno que suena mucho últimamente es el de Docker, pero... ¿qué es exactamente? ¿Por qué está tan de moda?

En este post contaré cómo lo veo yo y por qué no sólo que los Docker han venido para quedarse, sino que están revolucionando la industria.

Qué es un Docker

Un Docker es una manera de empaquetar una infraestructura.

Imaginad un .deb que lo trae todo configurado como lo necesitáis, listo para trabajar; pues es aún más, ya que permite tener distintas versiones de todas sus dependencias.

Otro palabro que está de moda es la Orquestación. Hay muchas herramientas como Puppet, Chef, Salt o Ansible, pero Docker va aún más lejos en busca de una Infraestructura inmutable.

La idea original detrás de Docker no es nueva. Hace ya muchos años que un colega me hablaba de las jaulas, contenedores que permiten correr procesos. Docker ha perfeccionado esas jaulas, permitiendo su empaquetado, distribución y definición.

Todo comienza en un Dockerfile, como veremos más adelante.

Conceptos

Hay algunos conceptos es importante tener en cuenta a la hora de trabajar con Docker:

Dockerfile
Es un archivo que permite definir las imágenes.
imagen
Consiste en un conjunto de aplicaciones empaquetadas. En este paquete va todo: dependencias, configuración, puertos expuestos, etc. Sin embargo, éstos no son usables, ya que no están en ejecución.
container o contenedor
Es una instancia de una imagen en ejecución. Es lo que usaremos.

Tanto las imágenes como los containers tienen un nombre consistente en una hash, algo como b02610296ec7, pero pueden tener un alias más fácil de recordar.

Ejemplo: RabbitMQ

Generando las imágenes

Vamos con un ejemplo pequeño: vamos a crear un docker que ejecute un RabbitMQ. Vamos a hacer lo mismo que hicimos en el artículo Colas de mensajes: RabbitMQ. Para ello vamos con un Dockerfile:

FROM debian:8.1

MAINTAINER Miguel Angel Garcia <miguelangel@magmax.org>

RUN \
    apt-get update && \
    apt-get install -y rabbitmq-server
RUN \
    rabbitmq-plugins enable rabbitmq_management && \
    rabbitmqctl add_vhost /my_vhost && \
    rabbitmqctl add_user my_user my_pass && \
    rabbitmqctl set_permissions -p /my_vhost my_user ".*" ".*" ".*" && \
    rabbitmqctl set_user_tags my_user management monitoring && \
    /etc/init.d/rabbitmq-server stop

CMD ["rabbitmq-server"]

# Expose ports.
EXPOSE 5672
EXPOSE 15672

Ahora utilizaremos el Dockerfile para generar una imagen:

docker build .

Y nos vamos a tomar un café. Aquí Docker hará de las suyas, descargándose la imagen base (en nuestro caso, debian:8.1) y ejecutando todas las órdenes de RUN. Cada orden RUN generará una imagen intermedia que ocupará espacio, pero son puntos de "checkpoint", ya que generará el resto de imágenes a partir de ahí.

Si optamos por varios RUN es importante el orden, ya que cualquier modificación regenerará todas las imágenes tras ella. Por ejemplo, si creemos que vamos a cambiar la contraseña del usuario con frecuencia, sería más eficiente usar algo así:

FROM debian:8.1

MAINTAINER Miguel Angel Garcia <miguelangel@magmax.org>

RUN \
    apt-get update && \
    apt-get install -y rabbitmq-server
RUN rabbitmq-plugins enable rabbitmq_management
RUN rabbitmqctl add_vhost /my_vhost
RUN \
    rabbitmqctl add_user my_user my_pass && \
    rabbitmqctl set_permissions -p /my_vhost my_user ".*" ".*" ".*"
RUN \
    rabbitmqctl set_user_tags my_user management monitoring && \
    /etc/init.d/rabbitmq-server stop

Esto generará 5 imágenes reutilizables. Si modificamos la contraseña, hay tres imágenes que se reusarán, haciendo el proceso de build mucho más rápido.

Un poco más abajo vemos la orden CMD, que es la orden por defecto a ejecutar cuando lancemos el container.

Finalmente, un par de EXPOSE, que son puertos que queremos exportar. Lo veremos ahora en la parte de ejecución.

También podríamos haber visto un ADD, que copia archivos de la máquina actual a la jaula o VOLUME, que sirve para crear puntos de montaje, pero para este artículo es suficiente con FROM, MAINTAINER, RUN, CMD y EXPOSE. No os creáis que hay muchas órdenes más.

Una vez construida la imagen podemos listarla:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
<none>              <none>              5e98d103b422        22 seconds ago      229.1 MB

Ejecutando un docker

Lo siguiente es ejecutar un docker. Lo primero que tenemos que ver son los puntos de montaje, variables y puertos que exponga. En este caso sólo explicaré los puertos expuestos. Es importante porque alterarán los argumentos con los que llamar al Docker.

En nuestro caso, vamos a mapear los puertos locales 35000 y 35001 a los de la máquina 5672 y 15672, respectivamente:

docker run -i -p 35000:5672 -p 35001:15672 5e98d103b422

Explico los argumentos:

-i
Modo interactivo. Útil para depurar y ver qué está pasando dentro del Docker.
-p 35000:5672
Mapeo el puerto 35000 local al 5672
-p 35001:15672
Mapeo el puerto 35001 local al 15672
5e98d103b422
Hash o nombre de la imagen a ejecutar.

Ahora podemos ver el container corriendo:

$ docker ps
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS              PORTS                                               NAMES
859bd82d0f8a        5e98d103b422:latest   "rabbitmq-server"   2 minutes ago       Up 2 minutes        0.0.0.0:35000->5672/tcp, 0.0.0.0:35001->15672/tcp   suspicious_turing

Por defecto Docker da nombres graciosos a todos los containers, como "suspicious_turing" :D

Y podemos conectarnos a http://localhost:35001, donde está la interfaz de nuestro precioso RabbitMQ.

Ventajas e inconvenientes

¿Qué ventajas ofrece este sistema de empaquetado?

La primera, que una vez creada la imagen podemos lanzar varias instancias de la misma:

$ docker run -d -p 35000:5672 -p 35001:15672 5e98d103b422
f99518d24970fe5d39787340e25947851daba31c8dea7e8c4380f23f4bb82d19
$ docker run -d -p 36000:5672 -p 36001:15672 5e98d103b422
2b415bcb600995b80874e1109a1ad1320f20162ce1efac58637a1c8a04b7d439
$ docker ps
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS              PORTS                                               NAMES
2b415bcb6009        5e98d103b422:latest   "rabbitmq-server"   4 seconds ago       Up 4 seconds        0.0.0.0:36000->5672/tcp, 0.0.0.0:36001->15672/tcp   prickly_babbage
f99518d24970        5e98d103b422:latest   "rabbitmq-server"   11 seconds ago      Up 11 seconds       0.0.0.0:35000->5672/tcp, 0.0.0.0:35001->15672/tcp   grave_stallman

Y tener una escuchando en el pueto 35001 y otra en el 36002 (la opción -d es para lanzar en segundo plano). Con un par de scripts resultaría sencillo montarse un cluster, ¿no?

Otra ventaja es la inmutabilidad. Si tenemos una infraestructura montada con Puppet, Chef, Salt o Ansible, nada impide que entre dos ejecuciones se haya cambiado una librería (la típica libc6) que provoque fallos de instalación o, lo que es mucho peor, de ejecución. Sin embargo, con las imágenes de Docker estamos seguros de que todas las máquinas son iguales.

Recomiendo leer el artículo Immutable Infrastructure: No SSH que habla sobre la inmutabilidad y los contenedores, y que ha sido en gran parte la inspiración para este artículo.

Sólo queda recordar que todo lo que ocurra dentro de un container no se replica en otros containers y se eliminará con éste. Existe la opción de jugar con los VOLUME (que quizá explique en otro artículo), para compartir un directorio entre el host y el container .

Otras órdenes interesantes

Para terminar, veamos otras órdenes interesantes, como parar un container:

$ docker stop 2b415bcb6009

O volver a lanzarlo (notad que ya no es necesario indicar el mapeo de puertos):

$ docker start 2b415bcb6009

Mostrar todos los containers disponibles:

$ docker ps -a

Eliminar un container

$ docker rm 2b415bcb6009

Mostrar todas las imágenes disponibles:

$ docker images

Eliminar una imagen que no tiene containers:

$ docker rmi 5e98d103b422

O forzar el borrado de una imagen a pesar de tener containers:

$ docker rmi -i 5e98d103b422

Finalmente, un aviso: El nombre del container o su hash siempre es el último argumento de cualquier órden. Eso es algo que me ha vuelto loco en alguna ocasión.


Comentarios

Comments powered by Disqus