Introducción a contenedores Docker

>> Es miércoles y David no ha publicado la 4ª parte del tutorial de OpenShift y por aquí se acerca Docker. ¿Se acerca el fin del mundo?
>> No tengo respuesta para eso, pero sí tengo una explicación de por qué no hay tutorial esta semana. Debido a una situación laboral muy particular, he decidido hacer una pausa en el Mega Tutorial de OpenShift v3.

>> ¿Y cuándo estará?

>>No tengo respuesta tampoco, pero es posible que en dos o tres semanas lo retome

>>¿Y mientras?

>> Pues mientras, ya que lo comenté y he tenido bastantes peticiones de ello, hablaré de Docker y de Ansible que, en parte, nos ayudará a profundizar en OpenShift v3

 

¿Empezamos?

Docker

Docker

Un contenedor Docker envuelve una pieza de software en un sistema de ficheros completo con todo lo necesario para ser ejecutado: código, runtimes, herramientas de sistema, librerías del sistema – cualquier cosa que pueda instalarse en un servidor. Esto garantiza que  el software siempre se ejecutará de la misma manera, independientemente de su entorno.

Extraído de la página de Docker

 LXC

LXC (LinuX Containers) es un método de virtualización a nivel del sistema operativo que permite ejecutar múltiples sistemas Linux de manera aislada. No es una máquina virtual, pero ofrece todo lo necesario para que el sistema “contenido” pueda trabajar, tales como CPU, network, I/O. Todo ello gracias a dos componentes a nivel del kernel:

  • Namespaces: aisla los recursos de los procesos: ID de procesos (PID), hostnames, ID de usuario (UID), acceso a red, comunicación entre procesos y sistema de ficheros.
  • CGroups: es una funcionalidad del kernel para limitar, regular y dar cuenta del uso de recursos para un conjunto de procesos.

En definitiva, sirve para crear una jaula para un grupo de recursos, algo conocido como Sandbox, la caja de arena donde dejamos jugar “seguros” a nuestros niños.

 

Docker + LXC

Configurar LXC puede ser una tarea bastante compleja, por ello Docker nos ofrece un mecanismo simple para gestionar nuestros “contenedores”. Docker se encargará de crear los namespaces y cgroups necesarios para ejecutar nuestro código. Aunque lo realmente impresionante de Docker es su modularidad basada en dos componentes principales:

  • Imagen: Es un conjunto de sistemas de ficheros y librerías, de solo lectura, que servirán como base para la ejecución de nuestro código.
  • Contenedor: Como nuestro código necesita desplegar procesos y ficheros que puedan escribirse, los contenedores crean una capa adicional sobre la imagen ofreciendo un entorno de ejecución.

Así que, como si de piezas de Lego (aunque yo era fan de TENTE, qué recuerdos,sniff, sniff) se tratara, podemos ir acoplando una imagen sobre otra hasta tener una base necesaria para montar nuestro contenedor.

Imaginemos por un momento que quiero ejecutar código PHP sobre un framework Laravel. Primero necesito un sistema base, pongamos un CentOS, sobre ese sistema necesito un servidor Apache, al cual le acoplo un framework Laravel. Una vez tengo todo eso, tres imágenes, conecto una sobre otra, lo que me da un sistema de solo lectura con todas las librerias y binarios necesarios para acoplar mi contenedor de procesos ejecutables en el que estará mi código PHP.

Dicho así parece muy fácil pero, ¿es fácil de verdad? De verdad es fácil, sólo tenemos que ir conectando las imágenes entre sí con los comandos que Docker nos ofrece hasta conseguir el resultado esperado.

Aunque la verdad es todavía más fácil, ya que Docker es un sistema abierto, podemos encontrar imágenes base, con todo lo necesario para nuestro código, creadas por otros usuarios y compartidas a la comunidad. Así por ejemplo tenemos una imagen llamada docker.io/eboraas/laravel ya lista para nuestro código PHP.

 

Manos a la obra

En el momento de redactar esta entrada la versión más reciente de Docker es la 1.12.1.

La versión que emplearé será la 1.10.3, que es la que está disponible en CentOS 7.2.

 

Instalaremos Docker via yum, aunque si lo hacéis en otra distro podéis seguir las instrucciones en su web. Por cierto, está disponible en servicios cloud como AWS, y en sistemas MAC y Windows.

sudo yum install docker

 

Como no me gusta trabajar mucho siendo el usuario root, voy a permitir que mi usuario david tenga capacidad de gestionar contenedores. Por ello crearé un grupo especial llamado docker. Este grupo tiene ciertas capacidades administrativas para el servicio docker, por lo que cualquier usuario dentro de este grupo podrá ver imágenes y gestionar contenedores, así que mucho cuidado con quien forma parte del grupo 😉

sudo groupadd docker
sudo usermod -aG docker david
<logout and login again>
sudo systemctl start docker
sudo systemctl enable docker

 

Una vez configurado podemos arrancar nuestro primer contenedor.

>> Pero, ¿no hay que descargar antes alguna imagen?

Podríamos hacerlo, pero una de las ventajas que ofrece docker en su gestión es la capacidad de realizar automáticamente los pasos necesarios para desplegar nuestro contenedor. Tomemos el siguiente ejemplo:

docker run hello-world
Unable to find image 'hello-world:latest' locally
Trying to pull repository docker.io/library/hello-world ... 
latest: Pulling from docker.io/library/hello-world
c04b14da8d14: Pull complete 
Digest: sha256:0256e8a36e2070f7bf2d0b0763dbabdd67798512411de4cdcf9431a1feb60fd9
Status: Downloaded newer image for docker.io/hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
 executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
 to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

Como se puede ver el cliente docker ha contactado con el servicio para preguntarle por una imagen llamada hello-world. El servicio no la tiene almacenada en local, así que contacta con un registro (almacén de imagenes), que por defecto es el público de docker, para descargar la imagen. Acto seguido crea un contenedor y ejecuta un comando definido en el blueprint de la imagen.

 

Comandos

Ya hemos empezado a usar docker, pero vamos a hacer un alto en algunos comandos básicos:

  • docker run: crea un contenedor ejecutando el comando definido en el blueprint de una imagen. También podemos crear un contenedor ejecutando un comando diferente, algo que veremos más adelante.
  • docker images: mostrará un listado de las imágenes principales. Las imágenes se pueden separar en imágenes principales e imágenes intermedias, que son las que permiten desde una base crear una principal. Esto lo veremos en detalle cuando definamos un blueprint. Para ver todas, base, intermedias y principales podemos añadir la opción “-a
  • docker ps: mostrará un listado de los contenedores en ejecución. Un contenedor puede tener tres estados posibles: en ejecución, en pausa o finalizado. Para poder ver todos los contenedores podemos usar la opción “-a“.

En base a eso podemos obtener información de lo que ha ocurrido con nuestra anterior ejecución:

docker images
REPOSITORY            TAG    IMAGE ID     CREATED      SIZE
docker.io/hello-world latest c54a2cc56cbb 12 weeks ago 1.848 kB


docker images -a
REPOSITORY            TAG    IMAGE ID     CREATED      SIZE
docker.io/hello-world latest c54a2cc56cbb 12 weeks ago 1.848 kB


docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES


docker ps -a
CONTAINER ID IMAGE        COMMAND CREATED        STATUS                     PORTS   NAMES
185a3df70ef2 hello-world "/hello" 16 minutes ago Exited (0) 16 minutes ago          mad_swirles

 

Cuando ejecutamos docker images la información que nos ofrece es:

  • IMAGE ID: Un identificador único de la imagen. Nos sirver como checksum para saber si ha sido alterada o coincide con el identificador que hay en el registro
  • REPOSITORY: Nos informa del registry desde el que se ha descargado. docker.io es el registry público de docker.
  • TAG: Al igual que ocurre con un repositorio de control de versiones, podemos tener un control de versiones de una imagen. Si no indicamos nada siempre buscará la imagen con TAG latest
  • CREATED: Nos informa la fecha de creación de la imagen. No tiene nada que ver con la fecha en la que ha sido descargada
  • SIZE: El tamaño de la imagen. Es virtual, ya que contine el tamaño de las imágenes intermedias.

Para docker ps esta es la información:

  • CONTAINER ID: Un identificador único para cada contenedor. Podemos crear diferentes contenedores partiendo de  la misma imagen.
  • IMAGE: La imagen base sobre la que se ejecutará el contenedor.
  • COMMAND: El comando con el que se ha iniciado el contenedor.
  • CREATED: El tiempo desde que se creó el contenedor
  • STATUS: El estado del mismo.
  • PORTS: Los puertos expuestos hacia el exterior. Más adelante veremos cómo conectar.
  • NAMES: Un nombre único por el que identificaremos al contenedor, para evitar memorizar el ID

OFF-TOPIC. Docker tiene un easter-egg para los nombres por defecto de los contenedores. Escoge aleatoriamente un adjetivo y le agrega el nombre de un científico, también escogido de manera aleatoria. Si queréis conocer las posibilidades, están disponibles en su código fuente. En mi caso es :

  • mad: loco
  • swirles: Bertha Swirles fue una física teórica que contribuyó a las primeras teorías cuánticas. (Vaya, no me acostaré sin saber algo más)

Por cierto, docker está escrito en Go, un lenguaje que también trataré en algún post futuro.

 

Trabajando con  imágenes

La búsqueda

Es el momento de iniciar un proyecto, y queremos probar a hacerlo con docker. Lo primero que sé es que mi proyecto hará uso de un código Java para una aplicación web simple. NOTA: no vamos a poner el código en un contenedor todavía. Este punto trata sólo de la gestión de las imágenes.

Si queremos ejecutar código Java para una aplicación web necesitamos un Java Web Server y, en este caso, el universal por excelencia es Tomcat. Luego ya tengo una palabra clave. El siguiente paso será hacer uso de docker search, una herramienta de docker para buscar en los registros configurados la palabra que le indicamos. Docker buscará en el nombre de la imagen y en la descripción.

docker search tomcat
INDEX     NAME                           DESCRIPTION                                     STARS OFFICIAL AUTOMATED
docker.io docker.io/tomcat               Apache Tomcat is an open source implementa...   943   [OK] 
docker.io docker.io/dordoka/tomcat       Ubuntu 14.04, Oracle JDK 8 and Tomcat 8 ba...   25             [OK]
docker.io docker.io/consol/tomcat-7.0    Tomcat 7.0.57, 8080, "admin/admin"              16             [OK]
docker.io docker.io/consol/tomcat-8.0    Tomcat 8.0.15, 8080, "admin/admin"              15             [OK]
....
OMMITED
....

 

Las búsquedas presentan un máximo de 25 resultados y nos ofrecen:

  • INDEX: El nombre del registro en el que se encuentra la imagen. –no-index=false para no mostrarlo
  • NAME: El nombre de la imagen.
  • DESCRIPTION_ Una descripción, truncada, de la imagen. –no-trunc=true para mostrar la descripción completa.
  • STARS: El número de estrellas dadas por los usuarios.  -s x | –stars=x  para limitar la búsqueda a imágenes con estrellas igual o superior a x
  • OFFICIAL: Indica si es una imagen base.
  • AUTOMATED: Indica si esta imagen se regenera al modificarse su imagen base. Esto nos muestra que es una imagen basada en otra. –automated=true para mostrar sólo este tipo de imágenes.

La obtención

Una vez localizada la imagen que vamos a usar, llega el momento de descargar la imagen. docker pull nos permite obtener la imagen de uno de los registry. El comando requiere el <nombre de la imagen> o bien registry/<nombre de la imagen>

docker pull docker.io/tomcat
Using default tag: latest
Trying to pull repository docker.io/library/tomcat ... 
latest: Pulling from docker.io/library/tomcat

6a5a5368e0c2: Pull complete 
7b9457ec39de: Pull complete 
d5cc639e6fca: Pull complete 
dae3b0638638: Pull complete 
ab678d1c6f00: Pull complete 
d5bf826c3153: Pull complete 
0081bad1df81: Pull complete 
8fafa3f26de4: Pull complete 
ae984359ed7e: Pull complete 
9175a2e1674f: Pull complete 
2e8f15e74426: Pull complete 
Digest: sha256:87025ccebdd5534826b4d315ceda1a97ba75e35431075eeaacaf616998a46ed0
Status: Downloaded newer image for docker.io/tomcat:latest

En este caso ha descargado la imagen tomcat del registry docker.io y, como no he indicado nada. la etiqueta latest, que en este caso corresponde a Tomcat 8. Si hubiera querido usar Tomcat 6 entonces debería haber usado el comando docker pull docker.io/tomcat:6

 

La inspección

Bien, y ¿una vez que tenemos la imagen? Pues lo adecuado sería inspeccionar cuál es la composición de la imagen. Para ello tenemos el comando docker inspect que nos permite inspeccionar componentes de docker, imagenes o contenedores. Veamos de que se compone la imagen que hemos descargado. Lo primero obtenemos el ID de la imagen con docker images y lo utilizamos para inspeccionar el componente

docker inspect 30d95ba23356
[
 {
   "Id": "sha256:30d95ba23356c135ca73b2e6a83fb7c5f8a35a7a6b5a2d57ced9adedc47badb4",
   "RepoTags": [
     "docker.io/tomcat:latest"
   ],
   "RepoDigests": [],
   "Parent": "",
   "Comment": "",
   "Created": "2016-09-23T23:53:21.418015533Z",
   "Container": "1b1d84493d7bd13a2afbba20f6b0421f003ad06a52cead3562d1966ee7f07a45",
   "ContainerConfig": {
     "Hostname": "383850eeb47b",
     "Domainname": "",
     "User": "",
     "AttachStdin": false,
     "AttachStdout": false,
     "AttachStderr": false,
     "ExposedPorts": {
        "8080/tcp": {}
     },
     "Tty": false,
     "OpenStdin": false,
     "StdinOnce": false,
     "Env": [
        "PATH=/usr/local/tomcat/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
        "LANG=C.UTF-8",
        "JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64/jre",
        "JAVA_VERSION=7u111",
        "JAVA_DEBIAN_VERSION=7u111-2.6.7-1~deb8u1",
        "CATALINA_HOME=/usr/local/tomcat",
        "TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib",
        "LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib",
        "OPENSSL_VERSION=1.0.2i-1",
        "TOMCAT_MAJOR=8",
        "TOMCAT_VERSION=8.0.37",
        "TOMCAT_TGZ_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=tomcat/tomcat-8/v8.0.37/bin/apache-tomcat-8.0.37.tar.gz",
        "TOMCAT_ASC_URL=https://www.apache.org/dist/tomcat/tomcat-8/v8.0.37/bin/apache-tomcat-8.0.37.tar.gz.asc"
    ],
    "Cmd": [
       "/bin/sh",
       "-c",
       "#(nop) ",
       "CMD [\"catalina.sh\" \"run\"]"
    ],
    "ArgsEscaped": true,
    "Image": "sha256:bb923de3fcd8edef19b14b1c4a9415d3ab97be519a1b727a631ac0ba85bc2bd5",
    "Volumes": null,
    "WorkingDir": "/usr/local/tomcat",
    "Entrypoint": null,
    "OnBuild": [],
    "Labels": {}
   },
   "DockerVersion": "1.12.1",
   "Author": "",
   "Config": {
      "Hostname": "383850eeb47b",
      "Domainname": "",
      "User": "",
      "AttachStdin": false,
      "AttachStdout": false,
      "AttachStderr": false,
      "ExposedPorts": {
      "8080/tcp": {}
   },
   "Tty": false,
   "OpenStdin": false,
   "StdinOnce": false,
   "Env": [
      "PATH=/usr/local/tomcat/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "LANG=C.UTF-8",
      "JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64/jre",
      "JAVA_VERSION=7u111",
      "JAVA_DEBIAN_VERSION=7u111-2.6.7-1~deb8u1",
      "CATALINA_HOME=/usr/local/tomcat",
      "TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib",
      "LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib",
      "OPENSSL_VERSION=1.0.2i-1",
      "TOMCAT_MAJOR=8",
      "TOMCAT_VERSION=8.0.37",
      "TOMCAT_TGZ_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=tomcat/tomcat-8/v8.0.37/bin/apache-tomcat-8.0.37.tar.gz",
      "TOMCAT_ASC_URL=https://www.apache.org/dist/tomcat/tomcat-8/v8.0.37/bin/apache-tomcat-8.0.37.tar.gz.asc"
   ],
   "Cmd": [
      "catalina.sh",
      "run"
   ],
   "ArgsEscaped": true,
   "Image": "sha256:bb923de3fcd8edef19b14b1c4a9415d3ab97be519a1b727a631ac0ba85bc2bd5",
   "Volumes": null,
   "WorkingDir": "/usr/local/tomcat",
   "Entrypoint": null,
   "OnBuild": [],
   "Labels": {}
 },
 "Architecture": "amd64",
 "Os": "linux",
 "Size": 355294925,
 "VirtualSize": 355294925,
 "GraphDriver": {
    "Name": "devicemapper",
    "Data": {
       "DeviceId": "30",
       "DeviceName": "docker-253:0-16777380-6346c1bd8ddda0fa9be8fc867860e1e011ad47514d821598cf2ee6d7a9598666",
       "DeviceSize": "10737418240"
    }
  }
 }
]

Lo que nos ha devuelto es un objeto json con cierta información destacable como:

  • “Config”: {… “ExposedPorts”: { “8080/tcp”: {} } que nos informa que abre el puerto 8080 para un servicio. Este puerto está expuesto sólo a la red de docker, configurada localmente por el servicio, por lo que se podrá acceder sólo desde la máquina host. Más adelante veremos como exponer los puertos al exterior.
  • “Config”: { “Env”: [ … ] define una serie de variables de entorno que utilizará el contenedor. En este caso podemos ver qué JRE usará así como vemos que descarga Tomcat en el contenedor para aliviar el peso de la imagen.
  • “Cmd”: [ “catalina.sh”, “run” ] Y la pieza importante, qué comando se ejecutará al arrancar el contenedor. Vemos una lista donde el arg[0] es el comando y arg[>=1] serán los argumentos que recibe el comando

 

La ejecución

El comando docker run <imagen> nos permite crear un contenedor basado en la imagen, que ejecutará el comando definido en el CMD del blueprint.

docker run tomcat
.....
<After a while>
.....
28-Sep-2016 14:17:20.012 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-apr-8080"]
28-Sep-2016 14:17:20.019 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-apr-8009"]
28-Sep-2016 14:17:20.020 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 139292 ms

Vemos que docker run ha capturado nuestra terminal. Si finalizamos el proceso con CTRL+C o cerramos la terminal, el contenedor finaliza. Ahora veremos como ejecutarlo en segundo plano

docker run -d tomcat
6a0a3e5410192519bccf8a4b2ec59b3f480f5e9dc1be393d36aab43ccbf16c3a

El ID que nos da es el del contenedor en ejecución que podemos ver con docker ps

docker ps
CONTAINER ID IMAGE  COMMAND           CREATED       STATUS       PORTS    NAMES
6a0a3e541019 tomcat "catalina.sh run" 2 minutes ago Up 2 minutes 8080/tcp backstabbing_meninsky

Si está funcional veremos que se ha creado hace dos minutos y que su estado es Up desde hace dos minutos. Además vemos que expone el puerto 8080. Pero no conocemos su IP, por lo que vamos a inspeccionar el contenedor.

Primero voy a descargar una herramienta que es el sed de JSON, jq. Una vez instalada inspeccionaré el contenedor para extraer su dirección IP

mkdir ~/bin
wget https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 -O ~/bin/jq && chmod +x ~/bin/jq
docker inspect backstabbing_meninsky|jq '.[0].NetworkSettings.IPAddress'
"172.17.0.2"

Esa es la dirección IP de mi contenedor. Ahora a ver cómo se ha desplegado nuestro Tomcat:

curl -s http://172.17.0.2:8080/|head -n 20



<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="UTF-8" />
 <title>Apache Tomcat/8.0.37</title>
 <link href="favicon.ico" rel="icon" type="image/x-icon" />
 <link href="favicon.ico" rel="shortcut icon" type="image/x-icon" />
 <link href="tomcat.css" rel="stylesheet" type="text/css" />
 </head>

 <body>

<div id="wrapper">

<div id="navigation" class="curved container">
 <span id="nav-home"><a href="http://tomcat.apache.org/">Home</a></span>
 <span id="nav-hosts"><a href="/docs/">Documentation</a></span>
 <span id="nav-config"><a href="/docs/config/">Configuration</a></span>
 <span id="nav-examples"><a href="/examples/">Examples</a></span>

Ahora voy a parar el contenedor, y seguido veremos un despliege del contenedor

  1. Con un nombre que elijamos
  2. Redireccionando los puertos a puertos de nuestro host. Con la opción -p requiere uno de los puertos del contenedor para exponerlo a un puerto aleatorio del host, o bien puede adquirir este formato host_ip:host_port:container_port
  3. Lógicamente, en background
docker stop backstabbing_meninsky 
docker run -d --name=myTomcat_8 -p 8080:8080 tomcat
659838deee0f03dd991a2bae7d127698a678cfe8cb5541b33470d705d9d5223
docker ps
8659838deee0 tomcat "catalina.sh run" 2 minutes ago Up 2 minutes 0.0.0.0:8080->8080/tcp myTomcat_8

curl -s http://localhost:8080 |head -n 20
<OMMITED>

Y aquí lo dejamos…. Que era sólo una breve introducción. Y sí, sé que dejo pendiente cómo cargar nuestro código en un contenedor, pero será en otro post, y os puedo asegurar que será muy interesante. Nos vemos

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *