Introducción a Docker: simplificación de DevOps

Publicado: 2022-03-11

Si le gustan las ballenas, o simplemente está interesado en la entrega continua rápida e indolora de su software a producción, lo invito a leer este tutorial introductorio de Docker. Todo parece indicar que los contenedores de software son el futuro de la TI, así que vamos a darnos un chapuzón con las ballenas de contenedores Moby Dock y Molly.

Docker, representado por un logotipo con una ballena de aspecto amistoso

Docker, representado por un logotipo con una ballena de apariencia amigable, es un proyecto de código abierto que facilita la implementación de aplicaciones dentro de contenedores de software. Su funcionalidad básica está habilitada por las características de aislamiento de recursos del kernel de Linux, pero además proporciona una API fácil de usar. La primera versión se lanzó en 2013, y desde entonces se ha vuelto extremadamente popular y está siendo ampliamente utilizada por muchos jugadores importantes como eBay, Spotify, Baidu y más. En la última ronda de financiación, Docker obtuvo una enorme cantidad de $ 95 millones y está en camino de convertirse en un elemento básico de los servicios DevOps.

Analogía del transporte de mercancías

La filosofía detrás de Docker podría ilustrarse con la siguiente analogía simple. En la industria del transporte internacional, las mercancías deben transportarse por diferentes medios, como carretillas elevadoras, camiones, trenes, grúas y barcos. Estos productos vienen en diferentes formas y tamaños y tienen diferentes requisitos de almacenamiento: sacos de azúcar, latas de leche, plantas, etc. Históricamente, era un proceso doloroso que dependía de la intervención manual en cada punto de tránsito para la carga y descarga.

Un carro tirado por caballos, una camioneta y un camión de transporte, todos transportando mercancías.

Todo cambió con la adopción de contenedores intermodales. Como vienen en tamaños estándar y se fabrican teniendo en cuenta el transporte, todas las maquinarias relevantes se pueden diseñar para manejarlos con una mínima intervención humana. El beneficio adicional de los contenedores sellados es que pueden preservar el ambiente interno como la temperatura y la humedad de los productos sensibles. Como resultado, la industria del transporte puede dejar de preocuparse por los bienes y concentrarse en llevarlos de A a B.

Transporte mediante contenedores marítimos por tierra y por mar

Y aquí es donde entra Docker y aporta beneficios similares a la industria del software.

¿En qué se diferencia de las máquinas virtuales?

A simple vista, las máquinas virtuales y los contenedores Docker pueden parecer similares. Sin embargo, sus principales diferencias se harán evidentes al observar el siguiente diagrama:

Cuadro comparativo de máquinas virtuales (VM) y contenedores

Las aplicaciones que se ejecutan en máquinas virtuales, además del hipervisor, requieren una instancia completa del sistema operativo y las bibliotecas de soporte. Los contenedores, por otro lado, comparten el sistema operativo con el host. El hipervisor es comparable al motor de contenedores (representado como Docker en la imagen) en el sentido de que administra el ciclo de vida de los contenedores. La diferencia importante es que los procesos que se ejecutan dentro de los contenedores son como los procesos nativos en el host y no presentan ninguna sobrecarga asociada con la ejecución del hipervisor. Además, las aplicaciones pueden reutilizar las bibliotecas y compartir los datos entre contenedores.

Como ambas tecnologías tienen diferentes fortalezas, es común encontrar sistemas que combinen máquinas virtuales y contenedores. Un ejemplo perfecto es una herramienta llamada Boot2Docker descrita en la sección de instalación de Docker.

Arquitectura acoplable

Arquitectura acoplable

En la parte superior del diagrama de arquitectura hay registros. De forma predeterminada, el registro principal es Docker Hub, que aloja imágenes públicas y oficiales. Las organizaciones también pueden alojar sus registros privados si así lo desean.

En el lado derecho tenemos imágenes y contenedores. Las imágenes se pueden descargar de los registros de forma explícita ( docker pull imageName ) o de forma implícita al iniciar un contenedor. Una vez que se descarga la imagen, se almacena en caché localmente.

Los contenedores son las instancias de las imágenes, son lo vivo. Podría haber varios contenedores ejecutándose en función de la misma imagen.

En el centro, está el demonio Docker responsable de crear, ejecutar y monitorear contenedores. También se encarga de construir y almacenar imágenes. Finalmente, en el lado izquierdo hay un cliente Docker. Habla con el demonio a través de HTTP. Los sockets de Unix se usan cuando están en la misma máquina, pero la administración remota es posible a través de una API basada en HTTP.

Instalación de Docker

Para obtener las instrucciones más recientes, siempre debe consultar la documentación oficial.

Docker se ejecuta de forma nativa en Linux, por lo que, dependiendo de la distribución de destino, podría ser tan fácil como sudo apt-get install docker.io . Consulte la documentación para obtener más detalles. Normalmente, en Linux, los comandos de Docker se anteponen con sudo , pero lo omitiremos en este artículo para mayor claridad.

Dado que el demonio de Docker utiliza funciones del kernel específicas de Linux, no es posible ejecutar Docker de forma nativa en Mac OS o Windows. En su lugar, debe instalar una aplicación llamada Boot2Docker. La aplicación consta de una máquina virtual VirtualBox, el propio Docker y las utilidades de administración de Boot2Docker. Puede seguir las instrucciones de instalación oficiales para MacOS y Windows para instalar Docker en estas plataformas.

Uso de la ventana acoplable

Comencemos esta sección con un ejemplo rápido:

 docker run phusion/baseimage echo "Hello Moby Dock. Hello Molly."

Deberíamos ver esta salida:

 Hello Moby Dock. Hello Molly.

Sin embargo, ha sucedido mucho más detrás de escena de lo que piensas:

  • La imagen 'phusion/baseimage' se descargó de Docker Hub (si aún no estaba en la memoria caché local)
  • Se inició un contenedor basado en esta imagen.
  • El comando echo se ejecutó dentro del contenedor.
  • El contenedor se detuvo cuando salió el comando.

En la primera ejecución, puede notar un retraso antes de que el texto se imprima en la pantalla. Si la imagen se hubiera almacenado en caché localmente, todo habría tomado una fracción de segundo. Los detalles sobre el último contenedor se pueden recuperar ejecutando docker ps -l :

 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES af14bec37930 phusion/baseimage:latest "echo 'Hello Moby Do 2 minutes ago Exited (0) 3 seconds ago stoic_bardeen

Tomando la próxima inmersión

Como puede ver, ejecutar un comando simple dentro de Docker es tan fácil como ejecutarlo directamente en una terminal estándar. Para ilustrar un caso de uso más práctico, a lo largo del resto de este artículo, veremos cómo podemos utilizar Docker para implementar una aplicación de servidor web simple. Para simplificar las cosas, escribiremos un programa Java que maneje las solicitudes HTTP GET a '/ping' y responda con la cadena 'pong\n'.

 import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; public class PingPong { public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); server.createContext("/ping", new MyHandler()); server.setExecutor(null); server.start(); } static class MyHandler implements HttpHandler { @Override public void handle(HttpExchange t) throws IOException { String response = "pong\n"; t.sendResponseHeaders(200, response.length()); OutputStream os = t.getResponseBody(); os.write(response.getBytes()); os.close(); } } }

Dockerfile

Antes de saltar y crear su propia imagen de Docker, es una buena práctica verificar primero si hay una existente en Docker Hub o en cualquier registro privado al que tenga acceso. Por ejemplo, en lugar de instalar Java nosotros mismos, utilizaremos una imagen oficial: java:8 .

Para construir una imagen, primero debemos decidir qué imagen base vamos a utilizar. Se denota por la instrucción FROM . Aquí, es una imagen oficial de Java 8 de Docker Hub. Vamos a copiarlo en nuestro archivo Java emitiendo una instrucción COPY . A continuación, vamos a compilarlo con RUN . La instrucción EXPOSE indica que la imagen proporcionará un servicio en un puerto en particular. ENTRYPOINT es una instrucción que queremos ejecutar cuando se inicia un contenedor basado en esta imagen y CMD indica los parámetros por defecto que le vamos a pasar.

 FROM java:8 COPY PingPong.java / RUN javac PingPong.java EXPOSE 8080 ENTRYPOINT ["java"] CMD ["PingPong"]

Después de guardar estas instrucciones en un archivo llamado "Dockerfile", podemos construir la imagen de Docker correspondiente ejecutando:

 docker build -t toptal/pingpong .

La documentación oficial de Docker tiene una sección dedicada a las mejores prácticas relacionadas con la escritura de Dockerfile.

Contenedores en funcionamiento

Cuando se ha construido la imagen, podemos darle vida como un contenedor. Hay varias formas en que podemos ejecutar contenedores, pero comencemos con una simple:

 docker run -d -p 8080:8080 toptal/pingpong

donde -p [puerto-en-el-host]:[puerto-en-el-contenedor] denota la asignación de puertos en el host y el contenedor respectivamente. Además, le estamos diciendo a Docker que ejecute el contenedor como un proceso demonio en segundo plano especificando -d . Puede probar si la aplicación del servidor web se está ejecutando intentando acceder a 'http://localhost:8080/ping'. Tenga en cuenta que en las plataformas donde se usa Boot2docker, deberá reemplazar 'localhost' con la dirección IP de la máquina virtual donde se ejecuta Docker.

En Linux:

 curl http://localhost:8080/ping

En plataformas que requieren Boot2Docker:

 curl $(boot2docker ip):8080/ping

Si todo va bien, debería ver la respuesta:

 pong

¡Hurra, nuestro primer contenedor Docker personalizado está vivo y nadando! También podríamos iniciar el contenedor en modo interactivo -i -t . En nuestro caso, anularemos el comando de punto de entrada para que se nos presente una terminal bash. Ahora podemos ejecutar los comandos que queramos, pero al salir del contenedor se detendrá:

 docker run -i -t --entrypoint="bash" toptal/pingpong

Hay muchas más opciones disponibles para poner en marcha los contenedores. Cubrimos algunos más. Por ejemplo, si queremos conservar los datos fuera del contenedor, podríamos compartir el sistema de archivos del host con el contenedor usando -v . De forma predeterminada, el modo de acceso es de lectura y escritura, pero se puede cambiar al modo de solo lectura agregando :ro a la ruta del volumen dentro del contenedor. Los volúmenes son particularmente importantes cuando necesitamos usar información de seguridad como credenciales y claves privadas dentro de los contenedores, que no deben almacenarse en la imagen. Además, también podría evitar la duplicación de datos, por ejemplo, asignando su repositorio Maven local al contenedor para evitar que descargue Internet dos veces.

Docker también tiene la capacidad de vincular contenedores entre sí. Los contenedores vinculados pueden comunicarse entre sí incluso si ninguno de los puertos está expuesto. Se puede lograr con –link otro-contenedor-nombre . A continuación se muestra un ejemplo que combina los parámetros mencionados anteriormente:

 docker run -p 9999:8080 --link otherContainerA --link otherContainerB -v /Users/$USER/.m2/repository:/home/user/.m2/repository toptal/pingpong

Otras operaciones de contenedores e imágenes

Como era de esperar, la lista de operaciones que uno podría aplicar a los contenedores e imágenes es bastante larga. Para abreviar, veamos solo algunos de ellos:

  • stop: detiene un contenedor en ejecución.
  • start: inicia un contenedor detenido.
  • commit: crea una nueva imagen a partir de los cambios de un contenedor.
  • rm - Elimina uno o más contenedores.
  • rmi: elimina una o más imágenes.
  • ps: enumera los contenedores.
  • imágenes: enumera las imágenes.
  • exec: ejecuta un comando en un contenedor en ejecución.

El último comando podría ser particularmente útil para fines de depuración, ya que le permite conectarse a una terminal de un contenedor en ejecución:

 docker exec -i -t <container-id> bash

Docker Compose para el mundo de los microservicios

Si tiene más de un par de contenedores interconectados, tiene sentido usar una herramienta como docker-compose. En un archivo de configuración, describe cómo iniciar los contenedores y cómo deben vincularse entre sí. Independientemente de la cantidad de contenedores involucrados y sus dependencias, puede tenerlos todos en funcionamiento con un solo comando: docker-compose up .

Docker en estado salvaje

Veamos las tres etapas del ciclo de vida del proyecto y veamos cómo nuestra amigable ballena podría ser de ayuda.

Desarrollo

Docker lo ayuda a mantener limpio su entorno de desarrollo local. En lugar de tener varias versiones de diferentes servicios instalados, como Java, Kafka, Spark, Cassandra, etc., puede iniciar y detener un contenedor requerido cuando sea necesario. Puede llevar las cosas un paso más allá y ejecutar varias pilas de software una al lado de la otra, evitando la confusión de las versiones de dependencia.

Con Docker, puede ahorrar tiempo, esfuerzo y dinero. Si tu proyecto es muy complejo de configurar, “dockerízalo”. Pase por el dolor de crear una imagen de Docker una vez y, a partir de este punto, todos pueden iniciar un contenedor en un abrir y cerrar de ojos.

También puede tener un "entorno de integración" ejecutándose localmente (o en CI) y reemplazar los stubs con servicios reales que se ejecutan en contenedores Docker.

Pruebas / Integración Continua

Con Dockerfile, es fácil lograr compilaciones reproducibles. Jenkins u otras soluciones de CI se pueden configurar para crear una imagen de Docker para cada compilación. Puede almacenar algunas o todas las imágenes en un registro privado de Docker para referencia futura.

Con Docker, solo prueba lo que debe probarse y elimina el entorno de la ecuación. Realizar pruebas en un contenedor en funcionamiento puede ayudar a mantener las cosas mucho más predecibles.

Otra característica interesante de tener contenedores de software es que es fácil crear máquinas esclavas con la misma configuración de desarrollo. Puede ser especialmente útil para las pruebas de carga de implementaciones en clúster.

Producción

Docker puede ser una interfaz común entre los desarrolladores y el personal de operaciones que elimina una fuente de fricción. También fomenta el uso de la misma imagen/binarios en cada paso de la canalización. Además, poder implementar un contenedor completamente probado sin diferencias de entorno ayuda a garantizar que no se introduzcan errores en el proceso de construcción.

Puede migrar sin problemas aplicaciones a producción. Algo que antes era un proceso tedioso y escamoso ahora puede ser tan simple como:

 docker stop container-id; docker run new-image

Y si algo sale mal al implementar una nueva versión, siempre puede retroceder rápidamente o cambiar a otro contenedor:

 docker stop container-id; docker start other-container-id

… garantizado para no dejar ningún desorden o dejar las cosas en un estado inconsistente.

Resumen

Un buen resumen de lo que hace Docker se incluye en su propio lema: Build, Ship, Run.

  • Build - Docker le permite componer su aplicación a partir de microservicios, sin preocuparse por las inconsistencias entre los entornos de desarrollo y producción, y sin bloquearse en ninguna plataforma o lenguaje.
  • Ship: Docker le permite diseñar todo el ciclo de desarrollo, prueba y distribución de aplicaciones, y administrarlo con una interfaz de usuario uniforme.
  • Ejecutar: Docker le ofrece la capacidad de implementar servicios escalables de forma segura y confiable en una amplia variedad de plataformas.

¡Diviértete nadando con las ballenas!

Parte de este trabajo está inspirado en un excelente libro Using Docker de Adrian Mouat.