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:
|
|
Ahora utilizaremos el Dockerfile para generar una imagen:
|
|
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í:
|
|
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:
|
|
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:
|
|
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 156725e98d103b422
Hash o nombre de la imagen a ejecutar.
Ahora podemos ver el container corriendo:
|
|
Por defecto Docker da nombres graciosos a todos los containers, como “suspicious_turing” :D
Y podemos conectarnos a https://localhost:35001, donde está la interfaz de nuestro precioso RabbitMQ.
Otras órdenes interesantes
Para terminar, veamos otras órdenes interesantes, como parar un container:
|
|
O volver a lanzarlo (notad que ya no es necesario indicar el mapeo de puertos):
|
|
Mostrar todos los containers disponibles:
|
|
Eliminar un container
|
|
Mostrar todas las imágenes disponibles:
|
|
Eliminar una imagen que no tiene containers:
|
|
O forzar el borrado de una imagen a pesar de tener containers:
|
|
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.
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:
|
|
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 .
Como inconvenientes (gracias, Ángel), se puede mencionar la propia inmutabilidad, ya que hasta que se le coge el truco puede provocar pérdidas de datos.
Otro inconveniente es el pequeño overhead que provocan los contenedores, aunque si se usan varias imágenes que compartern layers el resultado final es mínimo.