enchufado
   RSS
#
Empezando a andar con Docker (GNU/Linux) 2023-04-11 17:07:05

Ya hace unos dias, aparecieron los contenedores. Si fue para quedarse o no, sólo el tiempo lo dirá, pero mientras tanto, llevan un tiempo dando vueltas, han ido madurando y sin duda pueden encontrarse diversos casos de uso para ellos. Así que en éste post vamos a tratar de introducirlos y a ver algún ejemplo básico.

Teoria de Docker

Arquitectura

Docker usa una arquitectura cliente-servidor en la que el cliente de Docker habla con el Docker daemon, el servicio, quien hace realmente las tareas de build, ejecución y distribución de los contenedores Docker.

Los contenedores

Un contenedor es un proceso ejecutado en una sandbox para aislarlo del resto de procesos del host. Ese aislamiento se produce basándose en los namespaces y cgroups del kernel de Linux. Algunas características de un contenedor:

  • Es una instancia ejecutable de una imagen. Puede crearse, iniciarse, pararse, moverse o eliminarse.
  • Pueden ser ejecutados (docker run) directamente sobre un host físico, en una VM o desplegados en el cloud.
  • Son portables (funcionan en diversos SO's).
  • Estan aislados de otros contenedores y procesos no sólo en su ejecución, sino también en cuanto al software, binarios y configuraciones.

Un contenedor supone una capa de abstracción más por encima del SO (Sistema Operativo). Añadí "más" porque hasta entonces, se puede decir que se operaba bajo el paradigma de la virtualización (VM o Máquina Virtual), ya de por sí una capa de hardware por software situada entre el hardware real y el SO.

Algunas ventajas de los contenedores son:

  • Portabilidad/distribución: Poder mover servicios/aplicaciones con cierta facilidad a otro host (bien a través de una definición de los servicios/aplicaciones, bien exportando/importando conenedores y/o imágenes de los mismos).
  • Segmentación/modularidad (orientación a microservicios): Idealmente cada servicio va en su propio contenedor y se conectan entre ellos en base a necesidad.
  • Ligereza: Cada servicio tiene su propio contenedor con lo mínimo imprescindible para que éste funcione como se desea.
  • Escalabilidad: Facilita poder dividir los servicios/aplicaciones en distintos hosts.
  • Gestión de versiones de servicios/aplicaciones y del SO, al estar éstas desacopladas del Sistema Operativo y existir un histórico de las mismas en los registries.
  • Seguridad: Por el aislamiento de cada contenedor tanto de otros contenedores, como del SO.

Existen diversos motores de contenedores: lxc, docker, RKT, CRI-O, podman, containerd... Y por encima de éstos, aunque no entraremos a comentarlos, como soluciones de orquestación y automatización de contenedores tenemos Docker Swarm, OpenShift y Kubernetes. El artículo que nos ocupa es sobre Docker.

Las imágenes

Una imagen es en realidad un sistema de archivos con todas las dependencias necesarias para ejecutar el servicio/aplicación. Ésta se tiene que construir (docker build).

Las imágenes son la base de los contenedores, ya que éstos se crean a partir de las mismas. Son inmutables en el sentido que funcionan como un template/plantilla, lo cual no significa que no las podamos modificar para adaptarlas a nuestras necesidades y posteriormente guardarlas (modificando ese template o creando uno nuevo). Pero es importante entender el contepto de que una imagen es una base; no se ejecuta. Lo que se ejecuta es un contenedor creado a partir de una imagen.

Para empezar con Docker, lo más conveniente es realizar los primeros pasos usando imágenes existentes sin modificar y aplicando las configuraciones que requiramos y que estén previstas por las mismas. La biblioteca más grande de imágenes Docker por antonomasia es Docker Hub, y es allí donde hemos de empezar a buscar los servicios que necesitemos.

Dockerfile

En relación a las imágenes, los archivos Dockerfile permiten modificarlas creando de nuevas. Dockerfile es un archivo con las instrucciones necesarias para realizar las modificaciones deseadas a la imagen. Normalmente se parte de una imagen base, a la que se añaden paquetes, se crean usuarios/archivos/directorios, se asignan permisos... Docker tomará éste archivo para hacer el build de la imagen, para lo cual se descargará (si no la tiene ya) la imagen base, ejecutará las instrucciones y acabará generando (en el repositorio de imágenes local) la imagen resultante.

Docker Registry

Como acabamos de avanzar, cuando se trabaja con una imagen Docker para lanzar un contenedor, el flujo que sigue Docker es: si no tengo la imagen localmente, la voy a pedir al registry público (Docker registry) para descargarla y posteriormente poder crear un contenedor en base a la misma. Desde ese mismo instante, esa imagen se servirá en local a ese mismo host siempre que la necesite. El entorno local es en cierta medida una biblioteca de imágenes con opciones sencillas que guarda aquellas con las que hemos trabajado. Si queremos opciones avanzadas como publicar esa biblioteca para que otros puedan hacer uso de nuestras imágenes, necesitariamos de un Docker Registry.

Redes y almacenamiento

Docker permite definir ambos tipos de recursos.

Por lo que respecta a redes, ofrece distintos tipos de redes (drivers) en función de las necesidades:

  • Bridge, el usado por defecto. Útil para cuando los contenedores necesitan hablar entre ellos. En éste caso, puede definir direcciones o rangos ip con direccionamiento dentro del scope privado de ipv4.
  • Host, elimina el isolation entre contenedores y el host, haciando uso directamente de la red del host.
  • Overlay, conecta múltiples Docker daemons entre ellos y habilita los servicios Swarm para hablar entre ellos.
  • Macvlan, permite asignar una MAC address a un contenedor, haciendo que parezca un host físico. Útil para aplicaciones legagy que no vayan bien pasándolas por el stack de red de Docker.
  • None, si queremos deshabilitar todo el networking.
  • Plugins de terceras partes.

Por lo que respecta a almacenamiento, los archivos que se crean dentro un contenedor, se crean en una capa escribible que se pierde cuando se elimina el contenedor. Esa capa de storage es gestionada por un storage driver (unionfs), pensada más bien para trabajar con las imágenes, y no es conveniente (por performance y espacio ocupado) hacer uso indiscriminado de la misma. Así pues, para la persistencia de datos, mejor performance y menor espacio ocupado, es mejor hacer uso de alguna solución de persistencia, bien sea en la propia imagen (versus contenedor) modificándola para que incluya de serie aquello que queremos, bien sea usando volúmenes. Tipos:

  • Bind mounts, que son puntos de montaje entre el host y el contenedor y por tanto los archivos serán visibles/estarán disponibles desde ambos lados.
  • Named volumes, que son volúmenes internos a Docker.

Práctica de Docker

Instalación en una Debian

$ curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian  $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
$ apt-get update
$ apt-get install docker-ce docker-ce-cli containerd.io

Ejemplos de linea de comandos

# Listado de contenedores mostrando también los parados
$ docker ps -a

# Ejecución de contenedores manual (docker run)

# Ejecuta un contenedor en base a la imagen "hello world". Éste 
# escribe un mensaje por pantalla y finaliza su ejecución.
$ docker run hello-world

# Ejecuta un contenedor en base a la imagen "docker/getting-started". Éste despliega un tutorial
# de inicio a Docker que puede ser navegado apuntando nuestro navegador a http://localhost/
$ docker run -d -p 80:80 docker/getting-started
#
# Aquí nos fijamos en los siguientes parámetros:
# -d ejecuta el contenedor en modo detached (lo desvincula/deja libre la consola desde dónde se lanza)
# -p publica un puerto del host (el de la izquierda) a otro del contenedor (el de la derecha)

# Ejecuta un contenedor en base a la imagen "bitnami/apache". Éste despliega
# un servidor apache sirviendo la web alojada en el path /home/web
$ docker run -d -v /home/web:/app -p 8080:8080 bitnami/apache
#
# Aquí se añade el parámetro -v, que mapea un volumen con un directorio del host dentro del contenedor (bind mount).

# Inicio/Parada/Reinicio de un contenedor
$ docker start|stop|restart nombre_contenedor

# Eliminado de un contenedor, sólo posible cuando está parado
$ docker rm nombre_contenedor

# Listado de imagenes
$ docker image ls

# Listado de redes
$ docker network ls

# Listado de volumenes
$ docker volume ls

Para la ejecución de contenedores organizadamente tenemos el cliente de Docker docker-compose. Ésta herramienta permite organizar la parametrización de cada contenedor, definir las relaciones entre los mismos, el orden de dependencia/arranque, usar uno o varios archivos de entorno (.env) y Dockerfiles, así como crear redes y volúmenes de forma más sencilla. Todo ésto mediante un archivo de definición .yaml, que podría considerarse un archivo de definición de proyecto y despliegue con un formato específico.

Para ver un ejemplo sencillo, tomaremos el docker-compose.yml público de la imagen Docker de Wordpress:

version: '3.1'

services:

  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - wordpress:/var/www/html

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - db:/var/lib/mysql

volumes:
  wordpress:
  db:

Como vemos, lo primero a indicar es la versión de la especificación de Docker Compose que vamos a desplegar. Y a continuación va la palabra services seguida por cada uno de los servicios que deseamos; en éste caso 2, wordpress y db. Éste nombre del servicio lo ponemos nosotros. Cada servicio de corresponde a un contenedor.

Dentro de cada servicio debemos especificar un mínimo obligatorio: la imagen a usar para cada uno de ellos y según cada imagen base, probablemente sea necesario especificar algunas variables de entorno para que éste inicie por primera vez correctamente. El resto de parámetros (redes, volúmenes, puertos...) ya depende de lo que quedamos hacer/montar. En éste aspecto, en el docker-compose.yml vas usando imágenes de diferentes distribuidores de software y montando un "Tetris" estableciendo puntos de unión entre servicios.

Para levantar los servicios con éste sistema en modo detached seria:

$ docker-compose up -d

El cliente nos irá informando de todo lo que va haciendo (descarga y extracción de imagen, levantamiento de cada servicio...) y finalmente del estado de cada servicio.

Creación de imágenes

No vamos a profundizar en éste punto, pero sí que podemos ver un ejemplo básico de modificación de una imagen mediante la instalación de un paquete para una distribución base (Debian):

FROM debian:latest-slim
RUN apt-get update && \
    apt-get install --no-install-recommends -y htop iotop procps psutils && \
    rm -rf /var/lib/apt/lists/*

En éste caso, estamos diciendo que vamos a usar la imagen slim de la última versión de la imagen de Docker de Debian y a instalar una serie de utilidades en la nueva imagen resultante. Si quisiéramos usarla, tendríamos que crear un contenedor en base a la misma, bien con docker run, bien con docker-compose o con cualquier otro cliente de Docker.


Comentarios (0)


Volver al indice

login, admin, form, register