- Docker para ciberseguridad – Parte 1: Instalación e imágenes
- Docker para ciberseguridad – Parte 2: Crear imágenes. Dockerfile
- Docker para ciberseguridad – Parte 3: Gestión de contenedores
- Docker para ciberseguridad – Parte 4: docker volume y docker network
- Docker para ciberseguridad – Parte 5: Docker Compose (1)
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.