Kitabı oku: «Aprender Docker, un enfoque práctico», sayfa 2
1.4. ¿Qué es una máquina virtual?
Una «máquina virtual» o virtual machine (VM) es un entorno que emula la misma funcionalidad de una máquina física. Una máquina virtual hace uso de los recursos que se le hayan asignado; cuenta con su propia CPU, memoria, interfaz de red, almacenamiento y su propio sistema operativo.
Las máquinas virtuales se crean y ejecutan sobre un software llamado «hipervisor» o virtual machine monitor (VMM). El hipervisor se ejecuta sobre una máquina física y actúa como una capa intermedia entre el hardware de la máquina anfitriona o host y la máquina virtual. El hipervisor se encarga de gestionar y distribuir los recursos de la máquina física (CPU, memoria, almacenamiento, etc.) entre las máquinas virtuales.
Es posible crear varias máquinas virtuales sobre una misma máquina física. Cada una de las máquinas virtuales estará aislada del resto, tendrá sus propios recursos y contará con su propio sistema operativo, que no tiene por qué ser el mismo que el de la máquina anfitriona.
1.5. ¿Qué es un contenedor?
Un «contenedor» se puede definir como una unidad estándar de software que permite empaquetar el código fuente de una aplicación y todas sus dependencias, para que se pueda distribuir y ejecutar de forma rápida y fiable en diferentes entornos.
También se puede definir como un proceso que ha sido aislado de todos los demás procesos de la máquina anfitriona donde se está ejecutando. Aunque es posible tener más de un proceso en un contenedor, las buenas prácticas nos recomiendan ejecutar solo un proceso por contenedor.
1.6. Analogía con los contenedores de transporte marítimo
El término docker, en inglés, significa «estibador», que es la persona encargada de realizar la carga y descarga de un buque u otros medios de transporte. Para entender mejor qué es un «contenedor», podemos establecer una analogía entre los contenedores que se utilizan en el transporte marítimo y los contenedores software.
Los contenedores de transporte marítimo:
Cumplen una normativa ISO (International Organization for Standardization), donde se establecen las medidas, tamaño y forma de los contenedores.
Se rigen por un estándar para distribuir mercancías.
Pueden ser transportados en cualquier embarcación que cumpla el estándar ISO.
Los contenedores software:
Cumplen con los estándares abiertos de la industria de los contenedores software desarrollados por la OCI (Open Container Initiative).
Se rigen por un estándar para distribuir software.
Pueden ser ejecutados en cualquier runtime que cumpla el estándar OCI.
1.7. Diferencias entre contenedores y máquinas virtuales
A continuación, se enumeran las principales diferencias que existen entre contenedores y máquinas virtuales:
Una máquina virtual necesita un sistema operativo completo para poder funcionar, mientras que un contenedor no, ya que comparte el kernel del sistema operativo de la máquina donde se está ejecutando.
Los contenedores necesitan menos recursos que las máquinas virtuales. Con el mismo hardware, es posible tener un mayor número de contenedores que de máquinas virtuales.
El tiempo necesario para iniciar un contenedor es mucho menor que el de una máquina virtual. Un contenedor se puede iniciar en cuestión de milisegundos, mientras que una máquina virtual puede llegar a necesitar algunos minutos.
Por lo general, los contenedores son más ligeros que las máquinas virtuales. El tamaño de los contenedores se suele medir en megabytes, mientras que el de las máquinas virtuales se suele medir en gigabytes.
Un contenedor se puede ejecutar en una máquina virtual pero no al revés.
Los contenedores no han venido para sustituir a las máquinas virtuales, ya que cada tecnología es adecuada para casos de uso diferentes. Podemos decir que ambas tecnologías se complementan entre sí.
Figura 1.1. Diferencias entre contenedores y máquinas virtuales.
1.8. Arquitectura de Docker
Al inicio, Docker fue diseñado con una arquitectura monolítica, pero, más tarde, fue rediseñado a una arquitectura modular, formada por diferentes componentes que pueden ser reemplazados o incluso utilizarse en otros proyectos.
Cada uno de los componentes de Docker se desarrolla por separado y muchos de ellos forman parte del proyecto Moby. El proyecto Moby es un proyecto open source creado por Docker, Inc. en 2017, donde se desarrollan componentes y herramientas que pueden ser utilizados para crear productos basados en la tecnología de contenedores. Podemos decir que Moby es el proyecto donde se desarrollan los componentes y Docker es un producto que utiliza esos componentes:
Los principales componentes de Docker que debemos conocer son los siguientes:
Cliente de Docker.
Docker CLI.
Docker Compose.
Docker Engine.
Docker Engine API.
Docker daemon.
Container Runtime.
Containerd.
Runc.
Docker Registry.
En la figura 1.2, se trata de dar una visión global de cómo se relacionan los componentes que forman la arquitectura de Docker. En la figura se muestran tres bloques que representan al cliente, el host de Docker y el Registry. El host de Docker será una máquina que tiene instalado el componente Docker Engine y puede estar dentro o fuera de nuestra red local. En una instalación habitual, el cliente también estará instalado en el host de Docker, pero no es un requisito obligatorio. En los siguientes apartados de este capítulo, se irá describiendo con más detalle cada uno de estos componentes.
Figura 1.2. Componentes de la arquitectura de Docker.
1.9. Cliente de Docker
Docker utiliza una arquitectura cliente-servidor, donde una aplicación cliente interactúa con un servicio llamado Docker daemon. Un mismo cliente puede comunicarse con más de un servicio Docker daemon. La comunicación entre el cliente y el servidor se realiza a través de una API HTTP, conocida como Docker Engine API.
Las aplicaciones cliente permiten que un usuario pueda interaccionar con el servicio Docker daemon para realizar acciones como crear y ejecutar contenedores. Las aplicaciones oficiales que se pueden utilizar como cliente son Docker CLI (Command Line Interface) y Docker Compose. Aunque cualquier aplicación cliente que haga uso de la API de Docker Engine puede ser un cliente válido.
El cliente y el servidor se pueden ejecutar en la misma máquina o pueden estar en máquinas separadas. Cuando el cliente y el servidor están en la misma máquina, la comunicación entre ambos se puede realizar a través de un socket IPC o un socket TCP. Y, cuando se encuentran en máquinas separadas, se realiza a través de un socket TCP.
A continuación, se muestran dos ejemplos donde se describe a alto nivel cómo es el flujo de ejecución de un comando y cuáles son los componentes que intervienen.
Ejemplo de ejecución del comando docker pull
En la figura 1.3, se muestra el flujo de ejecución a alto nivel del comando docker pull
, que es el comando utilizado para descargar una imagen de un Docker Registry.
En este ejemplo, se realizan los siguientes pasos:
1. En primer lugar, el usuario ejecuta el comando docker pull
desde la línea de comandos y el cliente Docker CLI se comunica con el servicio Docker daemon.
2. El servicio Docker daemon se encarga de gestionar la petición al Docker Registry para descargar la imagen solicitada por el cliente.
3. La imagen se descarga del Docker Registry y se almacena en el host de Docker.
Figura 1.3. Ejemplo del flujo de ejecución a alto nivel del comando docker pull.
Ejemplo de ejecución del comando docker run
En la figura 1.4, se muestra el flujo de ejecución a alto nivel del comando docker run
, que es el comando utilizado para crear y ejecutar un contenedor a partir de una imagen.
En este ejemplo, se realizan los siguientes pasos:
1. En primer lugar, el usuario ejecuta el comando docker run
desde la línea de comandos y el cliente Docker CLI se comunica con el servicio Docker daemon.
2. El servicio Docker daemon comprueba si la imagen que ha seleccionado el usuario para crear el contenedor se encuentra en el host de Docker. Si la imagen no estuviese en el host, entonces la descargaría automáticamente del Docker Registry.
3. Se crea un contenedor a partir de la imagen seleccionada y se inicia la ejecución.
Figura 1.4. Ejemplo del flujo de ejecución a alto nivel del comando docker run.
1.9.1. Docker CLI
Docker CLI es el cliente oficial de Docker. Es una interfaz de línea de comandos que permite a los usuarios interaccionar con el servicio Docker daemon. El comando base de esta aplicación es docker
.
El caso de uso más habitual de Docker CLI es cuando se quiere interactuar con un único contenedor, mientras que Docker Compose se suele utilizar para trabajar con aplicaciones que ejecutan varios contenedores a la vez.
En las distribuciones Linux, podemos instalar la versión Community Edition de Docker CLI con el paquete docker-ce-cli,
que es un paquete independiente de Docker Engine, mientras que, en los sistemas operativos Windows y Mac, viene incluido en la aplicación Docker Desktop, que incluye también Docker Engine, entre otros componentes.
1.9.2. Docker Compose
Docker Compose es una aplicación utilizada desde la línea de comandos y permite a los usuarios interaccionar con el servicio Docker daemon.
Esta aplicación permite definir y ejecutar aplicaciones con múltiples contenedores. Utiliza un archivo de configuración con formato YAML, para definir los servicios, las redes y los volúmenes de los que consta la aplicación que queremos ejecutar. Una de las ventajas que nos ofrece Docker Compose es que solo hay que ejecutar un comando para crear y ejecutar todos los servicios que se han definido en el archivo YAML de configuración.
Actualmente, existen dos versiones de Docker Compose. La versión v1 está desarrollada en Python y tiene que ser instalada como una herramienta adicional con el nombre de docker-compose
, mientras que la nueva versión v2 está desarrollada en Go e integra el comando compose
dentro del cliente oficial de Docker CLI. Por lo tanto, para utilizar la nueva versión, se utiliza el comando docker compose
.
En el momento de escribir este libro, la versión v2 de Docker Compose solo está incluida en la aplicación Docker Desktop para Windows y macOS. Se espera que, muy pronto, también estará disponible en el cliente oficial Docker CLI para Linux. De momento, se puede instalar de forma manual como un plugin de Docker CLI.
1.10. Docker Engine
Docker Engine es el componente principal de Docker, encargado de crear, ejecutar y gestionar contenedores. Tiene un diseño modular y está formado por varios componentes que cumplen los estándares abiertos de la Open Container Iniciative (OCI). Entre sus componentes, destacamos los siguientes:
Docker Engine API.
Docker daemon.
Container Runtime.
Gestión de redes y almacenamiento.
Docker Build (BuildKit), para la creación de imágenes.
Distribution, para la interacción con los registros de contenedores.
Soporte nativo para la orquestación de contenedores con Docker Swarm (SwarmKit).
Gestión de plugins.
El componente principal de Docker Engine es el Docker daemon, que es un proceso encargado de administrar las imágenes, contenedores, redes y volúmenes. En muchas ocasiones, cuando nos referimos a Docker Engine, nos estamos refiriendo realmente al proceso Docker daemon.
En las primeras versiones, al instalar Docker Engine, se incluía el cliente Docker CLI, el Docker daemon y el Container Runtime, pero, actualmente, son tres componentes independientes que se tienen que instalar en paquetes separados.
Docker Engine se ejecuta de forma nativa en los sistemas Linux y Windows Server. En el resto de los sistemas operativos Windows, que no sean Windows Server, y en los sistemas operativos Mac, no se ejecuta de forma nativa, sino que lo hace sobre una máquina virtual Linux.
En las distribuciones Linux, podemos instalar la versión Community Edition de Docker Engine con el paquete docker-ce,
mientras que, en los sistemas operativos Windows y Mac, viene incluido en la aplicación Docker Desktop.
Figura 1.5. Componentes de Docker Engine.
1.10.1. Docker Engine API
La comunicación entre un cliente Docker y el servicio Docker daemon se realiza a través de una API HTTP conocida como Docker Engine API.
Esta API implementa todas las operaciones que un usuario puede realizar desde el cliente oficial de Docker CLI; por ejemplo, cuando un usuario ejecuta el comando docker ps
desde la línea de comandos, el cliente Docker CLI está haciendo una petición GET al endpoint /containers/json
de la API. El servicio Docker daemon ejecuta la petición del cliente y le devuelve una respuesta en formato JSON.
La API suele cambiar cada vez que se libera una nueva versión de Docker Engine. Para que los clientes que tienen una versión antigua de la API puedan seguir manteniendo compatibilidad con las nuevas versiones de Docker Engine, se incluye un prefijo en la URL de los endpoints de la API indicando la versión que quieren utilizar para comunicarse; por ejemplo, una llamada al endpoint /v1.41/containers/json
utilizaría la versión 1.41 de la API, mientras que una llamada a /v1.40/containers/json
usaría la versión 1.40.
Puede consultar la especificación completa de la API en la web oficial de Docker. En el momento de escribir este libro, la última versión disponible es la v1.41:
En la actualidad, existe un SDK oficial para Go y otro para Python, que permite a los desarrolladores crear aplicaciones que interactúan con la API de Docker Engine. También existe una gran variedad de librerías no oficiales que han sido desarrolladas por la comunidad para otros lenguajes de programación. A continuación, se muestra un ejemplo de cómo se puede hacer uso de la API de Docker Engine con la utilidad curl
.
Ejemplo
En este ejemplo, vamos a utilizar la utilidad curl
como cliente para realizar una llamada al endpoint http://localhost/v1.41/containers/json
de la API de Docker Engine, para obtener un listado de todos los contenedores que están en ejecución:
1. En este ejemplo, utilizamos un socket UNIX porque el cliente y el servicio Docker daemon se encuentran en la misma máquina. Como este ejemplo se ha realizado en una máquina Linux, el socket que utiliza el servicio Docker daemon está ubicado en /var/run/docker.sock
.
2. Indicamos el endpoint de la API de Docker Engine al que queremos hacer la llamada.
Si no existe ningún contenedor en ejecución, obtendremos una respuesta vacía:
Pero, si existen contenedores en ejecución, obtendremos una respuesta similar a la que se muestra a continuación. Tenga en cuenta que se han omitido algunos valores de la respuesta para simplificarla y mejorar su legibilidad:
Si desea profundizar más sobre el empleo de los SDKs de Docker, puede consultar la web oficial, donde encontrará diferentes ejemplos de uso:
1.10.2. Docker daemon
El servicio Docker daemon es el encargado de crear y gestionar todos los objetos con los que trabaja Docker, como las imágenes, los contenedores, las redes y los volúmenes. Este servicio se ejecuta en un proceso llamado dockerd
.
El cliente Docker se comunica con el servicio Docker daemon a través de una API HTTP y el servicio Docker daemon se comunica con el container runtime. En las primeras versiones, el Docker daemon también incluía el container runtime, pero, actualmente, son dos componentes independientes. El container runtime de Docker está formado por containerd y runc, de los que hablaremos más adelante.
El servicio Docker daemon expone una API HTTP para comunicarse con los clientes Docker y puede utilizar tres tipos de sockets para interaccionar con ellos: unix
, tcp
y fd
.
En una instalación habitual, el cliente Docker y el Docker daemon suelen estar en la misma máquina. Cuando trabajamos con contenedores Linux, se utiliza un socket de tipo UNIX que estará en la ruta /var/run/docker.sock
. Para poder hacer uso de este socket, es necesario tener permisos de root
o pertenecer al grupo de usuarios del sistema docker
. Cuando trabajamos con contenedores Windows, se utiliza un named pipe, que estará en la ruta \\.\pipe\docker_engine
.
Si vamos a trabajar en un entorno donde el cliente y el Docker daemon se ejecutan en diferentes máquinas, entonces necesitaremos utilizar un socket TCP. En este caso, la comunicación se realiza por defecto sobre un canal no seguro en el puerto 2375. Esta configuración puede ser adecuada para un entorno de desarrollo, pero nunca se debe utilizar en un entorno de producción. En un entorno de producción, se debe usar una conexión cifrada con TLS y se suele emplear el puerto 2376.
1.11. Container runtime
El container runtime es el software encargado de ejecutar los contenedores. Docker Engine utiliza dos tipos de container runtime:
containerd: container runtime de alto nivel.
runc: container runtime de bajo nivel.
1.11.1. containerd
Es un container runtime de alto nivel, que se ejecuta como un proceso daemon. Este componente se encarga de administrar el ciclo de vida de un contenedor dentro de un host. Realiza tareas como descargar las imágenes de los registros de los contenedores, almacenarlas en el host, supervisar la ejecución de los contenedores o gestionar el almacenamiento y las redes.
Este componente está diseñado para ser embebido dentro de otros sistemas más complejos. Aunque puede ser utilizado desde la línea de comandos con el cliente ctr
, que se incluye en la instalación por defecto.
Se trata de un proyecto open source, que fue creado por Docker, Inc. en 2016, junto a Google e IBM. En 2017, fue donado a la Cloud Native Computing Foundation (CNCF) y, en 2019, se convirtió en un proyecto graduado de la CNCF. Esto indica que el proyecto se encuentra en un nivel de madurez adecuado para ser utilizado por las empresas más conservadoras a la hora de adoptar nuevas tecnologías.
El componente containerd también implementa la interfaz CRI (Container Runtime Inferface) de Kubernetes. Esto quiere decir que este container runtime puede ser utilizado en un cluster de Kubernetes para crear y ejecutar contenedores a partir de imágenes Docker, que son imágenes compatibles con la especificación OCI.
1.11.2. runc
Es un container runtime de bajo nivel, encargado de interaccionar con el kernel del host de la máquina anfitriona donde se ejecutan los contenedores. Utiliza el componente libcontainer para interaccionar con el sistema operativo del host.
Se trata de una implementación de código abierto de la especificación OCI Runtime Specification, que describe cómo tiene que ser la configuración, el entorno de ejecución y el ciclo de vida de un contenedor.
Figura 1.6. Ejemplo de cómo interaccionan containerd y runc con el resto de los componentes.