Docker para ciberseguridad – Parte 5: Docker Compose (1)

por Alberto Jódar
0 comentario 20 minutos lectura

Ya hemos visto lo fundamental de Docker en las entradas anteriores. Podemos ir levantando aplicaciones alojadas en contenedores y gestionar su flujo de ejecución. Podemos crear un ecosistema de contenedores que nos permitan construir una aplicación más compleja. Un contendor con Apache, con un contenedor con CentOS que haga de backend, con otro contenedor con una mysql

Pero Docker nos obliga a tener que estar gestionando estos contenedores individualmente. Levantarlos, unirlos a una red… Docker Compose es una aplicación/herramienta que nos va a permitir crear aplicaciones multi-contenedor que se gestionen de forma centralizada. Con un solo fichero definimos una aplicación formada por múltiples contenedores y lo podemos gestionar todo de forma centralizada.

Vamos a usar todos los conceptos que ya hemos aprendido de Docker pero a través de Docker Compose. Todo se va a definir en un archivo de configuración de la aplicación con formato YAML. Vamos a ello

Instalación

Según la versión de Docker que tengamos, puede venir instalado. Si habéis instalado la Docker CE que instalamos en la entrada 1 lo tendréis. Podéis comprobarlo.

$> docker compose version

Si no, podéis instalarlo con las instrucciones que nos da la documentación. En Ubuntu:

# Actualizamos repo e instalamos
$> sudo apt-get update
$> sudo apt-get install docker-compose-plugin

# Comprobamos instalación
$> docker compose version

Estructura en Docker Compose

Una aplicación en Docker Compose se compone de varios elementos. Estos se definen en lo que se denomina el Compose application model. Lo podéis encontrar en la documentación. Los elementos que pueden formar parte de la configuración os sonarán:

  • Services: Es la parte computacional de la aplicación. Donde está la chica. Estará formado por los contenedores que se ejecutarán a partir de imágenesy que van a dar soporte a las funcionalidades de la aplicación.
  • Networks: Esta es una abstracción que permite definir la conectividad a nivel 3 (nivel IP) que tendrán los contendores. Permite definir las rutas IP de los contenedores, cuál estarán expuestos, cuál estará conectado a cuál…
  • Volumes: Como ya hemos visto en Docker, los volúmenes serán parte del sistema de fichero de los contenedores, pero que tendrán persistencia entre diferentes ejecuciones de la aplicación. Estos volúmenes podrán ser por cada contenedor o compartidos entre ellos.
  • Config: Aunque son una especie de volumen, merecen una sección aparte. Los config serán una serie de datos persistentes y que pueden ser compartidos entre aplicaciones que servirán para definir la configuración de las aplicaciones que alojan los contenedores.
  • Secret: Es un tipo especial de config, pero que tiene un carácter de confidencialidad especial. Los secrets también se montan en los contenedores, como los volúmenes, pero almacenan información sensible. Para tratarlo de manera especial, requiera esta nomenclatura separada de los volumes y config.

Un ejemplo que se entiende muy bien sería el que nos ilustra la documentación.

Como veis tenemos:

  • 2 services, que son contenedores que se llaman webapp y database
  • 1 secret (HTTPS certificate) que servirá para la aplicación web
  • 1 configuration (HTTP) que se usará por webapp para definir su funcionamiento
  • 1 volume que servirá para que persistan datos en el backend
  • 2 networks. La que conecta webapp y database. Y la que expone el puerto de webapp.

Definición de la aplicación: compose.yaml

Esta arquitectura se define en un fichero de formato YAML. Lo recomendado es que tenga el nombre de «compose.yaml«, pero lo vais a ver con otros:

  • «compose.yaml» (PREFERENTE)
  • «compose.yml«
  • O como en las versiones anteriores:
    • «docker-compose.yaml«
    • «docker-compose.yml«

Para la definición de la arquitectura que hemos visto arriba tendríamos un fichero yaml como el siguiente (recordad que este ejemplo lo estamos sacando de la documentación oficial):

services:
  frontend:
    image: example/webapp
    ports:
      - "443:8043"
    networks:
      - front-tier
      - back-tier
    configs:
      - httpd-config
    secrets:
      - server-certificate

  backend:
    image: example/database
    volumes:
      - db-data:/etc/data
    networks:
      - back-tier

volumes:
  db-data:
    driver: flocker
    driver_opts:
      size: "10GiB"

configs:
  httpd-config:
    external: true

secrets:
  server-certificate:
    external: true

networks:
  # The presence of these objects is sufficient to define them
  front-tier: {}
  back-tier: {}

Vamos a ver en detalle cada una de las secciones:

Sección: services

Según la documentación: «A service is an abstract definition of a computing resource within an application which can be scaled or replaced independently from other components. Services are backed by a set of containers, run by the platform according to replication requirements and placement constraints. As services are backed by containers, they are defined by a Docker image and set of runtime arguments

Pues en la sección services vamos a definir las imágenes y las opciones de ejecución del contenedor (las opciones que en el caso de Docker seleccionaríamos cuando ejecutamos con docker run). Se define con una serie de parámetros. Primero se añade el nombre que le vamos a dar al servicio, y luego una serie de configuraciones. Todas las configuraciones posibles que se pueden definir para el servicio se encuentran aquí.

Vamos a ver un ejemplo:

services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
  db:
    image: postgres:latest
    environment:
      POSTGRES_PASSWORD: example

En esta sección hemos definido dos servicios. Un servicio de nombre «web» basado en la imagen de nginx. Y otro servicio llamado «db» basado en la imagen de postgres. Como veis, dentro de la definición de cada servicio hay una serie de parámetros, muy parecidos a los que definíamos cuando ejecutábamos los contenedores con docker run. Todos los parámetros que se pueden definir para un servicio están listados en la documentación oficial. Los más relevantes:

  • build: Podemos indicar un Dockerfile a partir del cual construir la imagen.
  • ports: Mapea puertos del contenedor a puertos en el host.
  • volumes: Elige qué volúmenes montar en el contenedor.
  • environment: Permite establecer variables de entorno.
  • networks: Selecciona las redes a las que estará conectado el contenedor
  • depends_on: Establece dependencias, permitiendo elegir que el contenedor no se lance hasta que sus dependencias se hayan ejecutado correctamente.
  • command: Como la orden CMD del Dockerfile.
  • healthcheck: Permite personalizar el check de salud del contenedor. Por ejemplo, haciendo un curl a localhost en el caso del Apache. Podemos definir el intervalo y los reintentos.
  • logging: Definir como guardar los logs del contenedor.
  • deploy: Permite definir réplicas, límites de recursos y estrategias de actualización.
  • restart: permite definir la política de reinicio
  • entrypoint: Nos permite sobrescribir el punto de entrada al contendor. Sobrescribe lo que venga en la imagen y nos permite seleccionar el que queramos.
  • extra_hosts: Nos permite añadir entradas extras al fichero «/etc/hosts» del contenedor.
  • secrets: Definimos que secrets usará el contenedor.
  • configs: Definirá que configs usará el contenedor.

Para que veáis el formato:

services:
  web:
    image: nginx:latest
    build: ./app
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html
    environment:
      - POSTGRES_PASSWORD=example
    networks:
      - frontend
    depends_on:
      - db
    command: ["python", "app.py"]
    entrypoint: ["./entrypoint.sh"]
    restart: always
    deploy:
      replicas: 3
      update_config:
        parallelism: 2
        delay: 10s
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 1m30s
      timeout: 10s
      retries: 3
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"
    extra_hosts:
      - "somehost:162.242.195.82"
    secrets:
      - my_secret
    configs:
      - source: my_config
        target: /etc/config

Sección: networks

Según la documentación: «Networks let services communicate with each other. By default Compose sets up a single network for your app. Each container for a service joins the default network and is both reachable by other containers on that network, and discoverable by the service’s name«

Tenemos por defecto una red a la que se unirán todos los contenedores que definamos para nuestra aplicación. O podemos crear nuestras propias redes y luego seleccionarlas en cada contenedor en la sección «networks«.

Las networks se crean indicando su nombre y su tipo:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

Por ver algunos parámetros más avanzados que podemos usar:

  • driver: Nos permite elegir el tipo de red como vimos en la entrada 4. Nos permite elegir entre «bridge», «overlay» y «host».
  • driver_opts: según el driver que hayamos elegido, se podrán definir opciones adicionales. Estas se indican aquí en formato «clave:valor».
  • ipam: Permite definir subred, gateway…
  • internal: Indica si la red se pueda usar fuera de los servicios anexados a ella
  • external: Indica si se puede usar fuera de la definición del Compose
  • name: permite poner un nombre personalizado a la red
  • enable_ipv6: habilita IPv6

Para que veáis el formato:

networks:
  my_network:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: my_bridge
    ipam:
      config:
        - subnet: 172.16.238.0/24
          gateway: 172.16.238.1
    internal: true
    external: true
    name: custom_network_name
    enable_ipv6: true

Sección: volumes

Según documentación: «Volumes are persistent data stores implemented by the container engine. Compose offers a neutral way for services to mount volumes, and configuration parameters to allocate them to infrastructure«

Desde Docker compose se nos permite gestionar estos volúmenes de forma centralizada. Principalmente vamos a usar los volúmenes para mantener datos persistentes y para compartir datos entre contenedores. Para su definición tenemos la sección «volumes«.

services:
  db:
    image: postgres
    volumes:
      - db-data:/var/lib/postgresql/data
  web:
    image: nginx
    volumes:
      - web-data:/usr/share/nginx/html

volumes:
  db-data:
  web-data:

En este ejemplo se definen dos volúmenes, «db-data» y «web-data». En la definición de cada aplicación, dentro de la sección «volumes» se define qué carpeta dentro del contenedor se mapea con el volumen.

También podemos listar los parámetros más importantes:

  • driver: Específica el controlador que gestiona el volumen. Son una especie de plugin que defienen como se va a gestionar el almacenamiento. Puede ser, entre otros:
    • local: Utiliza el almacenamiento local del host para persistir datos
    • nfs: Utiliza el sistema de archivos de red pra compartir archivos entre varios hosts
    • cifs: Sistema de archivos CIFS
    • azure_file: Almacenamiento en Azure
    • awslogs: Almacenamiento en AWS
    • Otros: glusterfs,flocker,rexray,portworx,convoy,vSphere,gce,ebs,ceph,zfs,isilon…
  • driver_opts: lista de opciones adicionales que se definen en formato «clave:valor». Dependera del driver que elijamos.
  • external: Indica si el volumen ya existe y no debe ser creado. Por si se quiere compartir volumenes entre varias definiciones de compose.
  • name: Establece un nombre personalizado al volumen.
volumes:
  my_volume:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /path/to/dir
    external: true
    name: custom_volume_name

A la hora de elegir el montaje en la sección «volumes» dentro de «services» tenemos otra serie de opciones. Lo primero es definir el tipo. Un mismo volumen puede montarse en cada servicio de forma diferente. Recordad los tipos de volumen que vimos en la entrada 4.

Si no especificamos un tipo de volumen a montar:

# Tipo Named volumen: Volumen que se monta con un nombre en una ruta. Tiene que estar creado en la sección de volumes
services:
  web:
    image: nginx
    volumes:
      - volume_name:/container/path

# Tipo Mount: Carpeta del host que se monta en una carpeta del contenedor. Al ser un montaje, no tiene que estar creado en la sección de volumes
services:
  web:
    image: nginx
    volumes:
      - /host/path:/container/path

Si especificamos un tipo de entre:

  • volume: Será un volumen administrado por Docker
  • bind: Será un directorio del host
  • tmpfs: Es un directorio que se monta temporalmente en la memoria
# volume
volumes:
  - type: volume
    source: volume_name
    target: /container/path
    volume:
      nocopy: true

# bind
volumes:
  - type: bind
    source: /host/path
    target: /container/path
    bind:
      propagation: rprivate

# tmpfs
volumes:
  - type: tmpfs
    target: /container/path
    tmpfs:
      size: 100m

#volumen seleccionando tipo de sistema de ficheros
volumes:
  - type: volume
    source: nfs-volume
    target: /container/path
    volume:
      driver: local
      driver_opts:
        type: nfs
        o: addr=host.example.com,rw
        device: ":/path/to/dir"

No entramos en detalle de cada opción dentro de cada tipo de voluemn ya que no lo vamos a usar demasiado. Pero podeís navegar la documentación, que entra en detalle de todas las opciones posibles.

Sección: configs

Según documentación: «Configs let services to adapt their behaviour without the need to rebuild a Docker image. As with volumes, configs are mounted as files into a container’s filesystem. The location of the mount point within the container defaults to /<config-name> in Linux containers and C:\<config-name> in Windows containers.«

La sección configs por tanto define ficheros que se van a montar en los contenedores. En una ruta por defecto, o donde nosotros definamos en el YAML. La sección «configs» tiene esta pinta. Supongamos que queremos definir el fichero de configuración de Apache sin necesidad de tener que entrar al contenedor ni modificar la imagen:

configs:
  http_config:
    file: ./httpd.conf

services:
  web:
    image: httpd
    configs:
      - source: http_config
        target: /usr/local/apache2/conf/httpd.conf

Primero definimos en config que aquella con nombre «httpd_config» hace referencia a un fichero que tenemos en la misma ruta donde está el YAML que se llama «httpd.conf«. Luego cuando definimos el servicio «web» que parte de la imagen de «httpd«, decimos que el config se copie a la ruta «/usr/local/apache2/conf/httpd.conf«. Asi cuando arranque nuestra aplicación con Docker compose, tendremos el Apache configurado a nuestro gusto sin necesidad de tocar la imagen ni de entrar al contenedor.

Sección: secrets

Según documentación: «Secrets are a flavor of Configs focusing on sensitive data, with specific constraint for this usage.«

Es como config, pero que se supone que va a almacenar datos sensibles como contraseñas, certificados… Si continuamos el ejemplo anterior del Apche, donde queremos subirle un certificado:

secrets:
  web_cert:
    file: ./server.key

configs:
  http_config:
    file: ./httpd.conf

services:
  web:
    image: httpd
    secrets: 
      - source: web_cert
        target: /usr/local/apache2/conf/ssl/server.key
    configs:
      - source: http_config
        target: /usr/local/apache2/conf/httpd.conf

Mismo proceso. Dejamos el fichero con el certificado en este caso en la ruta raiz, donde se encuentra el YAML, y definimos el secret. En la definición del servicio indicamos que el secret se mueva a la ruta de los certificados de Apache.

Conclusiones

Se que ha sido un poco duro. Hemos visto las opciones posibles en el fichero YAML que define la aplicación multicontenedor con Docker Compose. Hemos ido sección por sección, ya que con lo que ya conoceis de Docker de las entradas anteriores, es más fácil que visualicéis las posibilidades.

En la siguientes entradas veremos como se gestiona con docker compose esta aplicación multicontenedor y conformaremos una aplicación propia que ponga en práctica estos conocimientos.

Artículos relacionados

Deja un comentario

* Al utilizar este formulario usted acepta el almacenamiento y tratamiento de sus datos por parte de este sitio web.

Este sitio web utiliza cookies para mejorar su experiencia Aceptar Leer más