Gestionar aplicaciones con Docker Compose

Tutorial #5 - Orquestación de contenedores

Introducción

Amazon Web Services define Docker, como una plataforma de software que le permite crear, probar e implementar aplicaciones rápidamente. Docker empaqueta software en unidades estandarizadas llamadas contenedores que incluyen todo lo necesario para que el software se ejecute, incluidas bibliotecas, herramientas de sistema, código y tiempo de ejecución.

En este tutorial, vamos a abordar el despliegue de una aplicación web desarrollada en Python, utilizando las tecnologías Flask y PostgreSQL. Nuestro enfoque será emplear contenedores para llevar a cabo este proceso. Comenzaremos en el siguiente punto, una aplicación sin uso de Docker:

Aplicación sin uso de contenedores

En la primera sección de este laboratorio, procederemos a aprovisionar la base de datos utilizando contenedores Docker.

En la segunda etapa, nos embarcaremos en la creación de una imagen completamente nueva para nuestra aplicación.

Concluiremos la actividad al automatizar el proceso de despliegue mediante el uso de Docker Compose.

Nota: Este tutorial se desarrolla en un entorno Windows, utilizando Docker Desktop y PowerShell como las herramientas principales para llevar a cabo los ejercicios. Sin embargo, es importante destacar que las instrucciones y conceptos presentados aquí son igualmente aplicables a entornos Linux y Bash. La naturaleza agnóstica de Docker permite una coherencia en la experiencia de uso, independientemente del sistema operativo. Si bien los detalles de la configuración pueden variar, los conceptos y pasos clave se mantienen consistentes en ambos entornos. Así que, si estás utilizando Linux con Bash, podrás seguir este tutorial con confianza.

Nota: Si está trabajando en una distribución GNU/Linux (por ejemplo, Ubuntu), es posible que deba utilizar el comando 'sudo' antes del comando 'docker' para ejecutarlo con privilegios de superusuario.

Paso 1 - Base de datos en un contenedor Docker

Abra una terminal, como por ejemplo la terminal de PowerShell en mi caso:

Para confirmar que Docker está funcionando correctamente, ejecute el comando docker. Verá cómo Docker se ejecuta y muestra los distintos parámetros de configuración disponibles.

Luego, utilice los siguientes comandos para validar la situación: docker image ls, para verificar si hay imágenes descargadas, y docker ps, para comprobar si hay contenedores en ejecución.

Para descargar una nueva imagen, utilice el comando docker pull. En este caso, descargaremos una imagen de la base de datos Postgres.

docker pull postgres

A continuación, cree un contenedor utilizando esta imagen. Para hacerlo, emplee el comando docker run, el cual ejecutará una instancia de un contenedor basado en la imagen previamente especificada:

docker run --name db -p 5432:5432 -e POSTGRES_PASSWORD=libros -d postgres

Siendo db el nombre de nuestro contenedor. Recordemos, ¿Qué significan los flags?

-env , -e    -> define y establece variables de entorno
-detach , -d    -> Ejecuta el contenedor en background e imprime el container ID
-publish , -p    -> Publica o expone un puerto del contenedor en un puerto del host

Para entender mejor el concepto de mapeo de puertos de red, le invitamos a analizar el diagrama que se presenta a continuación:

Ahora, para conectarse al contenedor y crear una nueva base de datos, proceda con la ejecución de los siguientes comandos:

docker exec -it db psql -U postgres

Para crear la base de datos dentro del contenedor:

CREATE USER postgres;
CREATE DATABASE libros OWNER postgres;
ALTER USER postgres WITH PASSWORD 'libros';
exit

Posteriormente, descargaremos una nueva imagen y crearemos un nuevo contenedor para establecer la conexión con nuestra base de datos, este contenedor ejecutar el administrador de base de datos pgAdmin. Esto lo lograremos siguiendo los pasos que detallamos a continuación:

docker pull dpage/pgadmin4
docker run -p 5050:80 -e "PGADMIN_DEFAULT_EMAIL=user@domain.com" -e "PGADMIN_DEFAULT_PASSWORD=SuperSecret" -d dpage/pgadmin4

Utilice su navegador de preferencia e ingrese a http://localhost:5050. Luego, utilice las credenciales proporcionadas en el paso anterior:

Nota: PgAdmin requiere varios minutos para estar en línea.

usuario: user@domain.com
contraseña: SuperSecret

El siguiente paso consiste en conectarnos a la base de datos alojada en el otro contenedor. Para ello, diríjase a la sección "Servers" y seleccioné "Register → Server":

Ingrese los datos de conexión, teniendo en cuenta que la IP corresponde a la dirección de su máquina real (tenga en cuenta que el valor de ejemplo corresponde a una dirección IP local):

usuario: postgres
contraseña: libros

Una vez completado este proceso, habrá establecido la conexión con el servidor. Podrá observar que la base de datos "libros" se encuentra disponible y accesible.

Paso 2: Ejecutar la aplicación Python + Flask

En su terminal, para instalar el entorno virtual, ejecute el siguiente comando:

pip install virtualenv

Nota: Recuerde que en mi caso estoy utilizando PowerShell.

Cree un directorio de trabajo para su proyecto y, a continuación, genere un nuevo entorno virtual utilizando el siguiente comando:

mkdir proyecto
cd proyecto
virtualenv env

Para activar el entorno virtual, ejecute el siguiente comando en PowerShell:

.\env\Scripts\activate.ps1

En Bash:

source env/bin/activate

Nota: Si está trabajando en Windows con PowerShell y Docker Desktop como es mi caso y recibe el mensaje de error + Set-ExecutionPolicy Unrestricted al tratar de ejecutar el ambiente virtual, abra PowerShell como administrador y ejecute el comando Set-ExecutionPolicy Unrestricted

Para volver a intentar con el entorno virtual, ejecute nuevamente el siguiente comando:

.\env\Scripts\activate.ps1

Clone el repositorio que se muestra a continuación:

git clone https://gitlab.com/jpadillaa/my-readings.git

Ejecute el comando pip install -r requirements.txt para instalar las dependencias del proyecto:

cd readings
pip install -r requirements.txt

Para ejecutar la aplicación, utilice los siguientes comandos:

$env:FLASK_APP="apirest"
$env:FLASK_DEBUG=1
flask run --host=0.0.0.0

Si está trabajando en una terminal Bash:

$ export FLASK_APP=apirest
$ export FLASK_DEBUG=1
$ flask run --host=0.0.0.0

Nota: Asegúrese de ajustar la dirección IP de la base de datos en el archivo config.py. Puede utilizar la dirección IP de localhost dado que está haciendo un mapeo de puertos con Docker al contenedor de base de datos o incluso usar su dirección IP que fue asignada a su máquina real en su red local.

Luego, proceda a realizar pruebas con Postman:

A continuación, se presentan los endpoints disponibles en la aplicación:

Paso 3: Agregar persistencia a la base de datos

Para eliminar el contenedor de la base de datos, siga estos pasos:

docker ps
docker stop <nombre_contenedor o id>
docker rm <nombre_contenedor o id>
docker ps
docker container ls -a

Si decide recrear el contenedor de la base de datos, notará que la información almacenada en la base de datos se habrá perdido, ya que los datos generados dentro del contenedor no son persistentes. Para implementar almacenamiento persistente, le recomendamos que cree un directorio en su máquina local destinado a la base de datos. Luego, al recrear el contenedor, especifique que este directorio será utilizado como un volumen de almacenamiento persistente. De esta manera, cuando el contenedor sea eliminado, este punto de montaje mantendrá la información de la base de datos de forma duradera. Además, podrá adjuntar este punto de montaje a un nuevo contenedor de base de datos en el futuro, garantizando la retención de los datos.

mkdir postgres-data
docker run --name db -p 5432:5432 -e POSTGRES_PASSWORD=libros -d -v ${PWD}/postgres-data:/var/lib/postgresql/data postgres

Nota: No es necesario descargar nuevamente la imagen de postgres dado que esta se descargó en un paso previo.

Nota: En la ilustración en pantalla, la ruta del punto de montaje podría no coincidir exactamente con la que se muestra en el código de la aplicación. Asegúrese de validar este detalle en su propio proyecto.

Ahora, ejecute nuevamente la aplicación y notará que no existen datos almacenados en la base de datos. Sin embargo, tenga en cuenta que puede recrear estos datos. Si detiene el contenedor, lo elimina y vuelve a crearlo utilizando el comando mencionado anteriormente, podrá observar cómo los datos generados previamente persisten en la base de datos.

Paso 4: Crear un contenedor para el API REST (Python + Flask)

Para comenzar, realizaremos una ligera modificación en el fragmento de código encargado de gestionar la cadena de conexión a la base de datos. Inicialmente, presentamos el siguiente bloque de código (instance -> config.py):

load_dotenv()
OUR_HOST=os.getenv("DB_HOST", "127.0.0.1")
OUR_DB=os.getenv("DB_DB", "libros")
OUR_USER=os.getenv("DB_USER", "postgres")
OUR_PORT=os.getenv("DB_PORT", "5432")
OUR_PW=os.getenv("DB_PW", "libros")
OUR_SECRET=os.getenv("SECRET", "libros")
OUR_JWTSECRET=os.getenv("JWTSECRET", "libros")

La vamos a cambiar por:

load_dotenv()
OUR_HOST=os.getenv("DB_HOST", "db")
OUR_DB=os.getenv("DB_DB", "libros")
OUR_USER=os.getenv("DB_USER", "postgres")
OUR_PORT=os.getenv("DB_PORT", "5432")
OUR_PW=os.getenv("DB_PW", "libros")
OUR_SECRET=os.getenv("SECRET", "libros")
OUR_JWTSECRET=os.getenv("JWTSECRET", "libros")

A partir de este momento, haremos referencia al contenedor de la base de datos como db.

En este paso, crearemos un contenedor para la aplicación desarrollada en Python y Flask. Para lograrlo, generaremos un archivo llamado Dockerfile en la carpeta del proyecto.

Escriba las siguientes instrucciones en el archivo Dockerfile y guarde los cambios:

# Use an official Python runtime as the base image
FROM python:3.11

# Set the working directory in the container to /app
WORKDIR /readings

# Copy the rest of the application code to the container
COPY . .

# Install the dependencies
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

# Set environment variables
ENV FLASK_APP="apirest"
ENV FLASK_DEBUG=1

# Expose port 5000 for the Flask development server to listen on
EXPOSE 5000

# Define the command to run the Flask development server
CMD ["flask", "run", "--host=0.0.0.0"]

Cada línea del Dockerfile tiene un propósito específico:

  1. La primera línea establece la imagen base que se utilizará en el contenedor: una imagen oficial de Python 3.11.

  2. La segunda línea define el directorio de trabajo dentro del contenedor como "/readings".

  3. La tercera línea copia todo el código de la aplicación restante al interior del contenedor.

  4. En las líneas cuarta y quinta, se instalan las dependencias especificadas en el proyecto mediante la ejecución de "pip install" en la imagen.

  5. Las líneas sexta y séptima establecen variables de entorno para la aplicación: FLASK_APP y FLASK_DEBUG. Esto porque es un contenedor basado en Linux por lo que se crean las variables de entorno para un ambiente bash.

  6. En la octava línea, se expone el puerto 5000 para que el servidor Flask pueda escuchar en él.

  7. Por último, la última línea del Dockerfile define el comando que se ejecutará en el contenedor para iniciar el servidor Flask.

Para generar la imagen de nuestra aplicación, proceda a ejecutar el siguiente comando:

docker build -t <nombre:tag> .
docker image ls

Ahora, para crear un contenedor utilizando esta imagen y desplegar nuestra aplicación, ejecute el siguiente comando:

docker run --name <nombre_del_contenedor> --add-host=db:IP_DEL_HOST -p 5000:5000 -e -d libros-api

Reemplaza IP_DEL_HOST con la dirección IP real de la máquina que está ejecutando el contenedor de la base de datos. Esta configuración es temporal y solo es para la demostración, en la siguiente sección ajustaremos esto con Docker Compose.

Proceda a ejecutar nuevamente la aplicación (realice pruebas con Postman). En este caso, notará que los datos se mantienen almacenados en la base de datos, ya que no hemos detenido la ejecución de nuestro contenedor de base de datos.

Nota: ¿Cuál es la dirección IP del contenedor? Puede utilizar la dirección IP de localhost dado que está haciendo un mapeo de puertos con Docker al contenedor de la aplicación o incluso usar su dirección IP que fue asignada a su máquina real en su red local.

Paso 5: Automatizando el despliegue con Docker Compose

Docker Compose es una herramienta que permite definir y gestionar aplicaciones multi-contenedor de forma declarativa. Con Docker Compose, puede describir toda la configuración de sus servicios, como imágenes, volúmenes y variables de entorno, en un archivo único. Esto simplifica la creación, ejecución y administración de aplicaciones complejas que requieren múltiples contenedores interconectados.

Docker Compose facilita la coordinación y orquestación de estos contenedores, permitiéndote definir redes y volúmenes compartidos, así como configurar interacciones entre servicios, todo en un entorno simple y coherente.

La estructura básica de un archivo docker-compose.yml consta de varias secciones que definen cómo se deben ejecutar los servicios en Docker Compose. Aquí hay una breve descripción de cada sección:

  1. Version: Especifica la versión de Docker Compose que se utilizará en el archivo.

  2. Services: Define los servicios que compondrán tu aplicación. Cada servicio representa un contenedor individual y se configura con propiedades como imagen, puertos expuestos, variables de entorno, etc.

  3. Networks: Define las redes a las que se conectarán los servicios. Esto permite la comunicación entre contenedores en diferentes servicios.

  4. Volumes: Opcionalmente, puedes definir volúmenes para persistir datos entre contenedores o mantenerlos fuera de los contenedores.

  5. Environments/Environment: Define las variables de entorno que se pasarán a los contenedores.

  6. Build: Si se requiere, puedes especificar cómo construir imágenes personalizadas a partir de Dockerfiles.

  7. Ports: Define el mapeo de puertos entre el host y los contenedores.

  8. Depends_on: Indica el orden en que se deben iniciar los servicios.

A continuación, vamos a automatizar todo el proceso de despliegue mediante el uso de Docker Compose. Asegúrese de validar qué contenedores están en ejecución y deténgalos utilizando los siguientes comandos:

docker ps
docker stop <nombre del contenedor o id>

Ahora elimine los contenedores, para esto utilice los comandos:

docker ps -a
docker rm <nombre del contenedor o id>

En esta etapa, automatizaremos el despliegue de los contenedores de nuestra aplicación (la API y la base de datos). Para lograrlo, crearemos un archivo llamado docker-compose.yml dentro de la carpeta del proyecto:

Dentro de este archivo, defina la siguiente configuración:

version: "3.8"
services: 
  libros-api:
    build: .
    container_name: libros_api_app
    networks:
      - web_net
    ports:
      - "5000:5000"
    depends_on: 
      db:
        condition: service_healthy

  db: 
    container_name: libro_db_pg
    environment: 
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: libros
      POSTGRES_DB: libros
    image: "postgres:latest"
    networks:
      - web_net
    ports: 
      - "5432:5432"
    volumes: 
      - ${PWD}/postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready"]
      interval: 5s
      timeout: 5s
      retries: 5

networks:
    web_net:
        driver: bridge

volumes: 
  pg-data: 
    driver: local

Cada aspecto de la configuración en el archivo docker-compose.yml tiene un propósito específico:

  1. La primera línea establece la versión de Docker Compose utilizada: 3.8.

  2. La sección "services" define los servicios que serán creados y ejecutados en el entorno de Docker Compose.

  3. El servicio "libros-api" construye la imagen de la aplicación a partir del Dockerfile en el directorio actual, y se nombra "libros_api_app".

  4. La sección "networks" asigna este servicio a la red "web_net".

  5. La sección "ports" mapea el puerto 5000 del contenedor al puerto 5000 del host.

  6. La sección "depends_on" establece que el servicio "db" debe estar disponible antes de ejecutar "libros-api".

  7. El servicio "db" representa un contenedor de base de datos PostgreSQL llamado "libro_db_pg".

  8. La sección "environment" configura las variables de entorno para el usuario, contraseña y base de datos.

  9. La sección "image" especifica la imagen de la base de datos a utilizar: la última versión de PostgreSQL.

  10. La sección "ports" mapea el puerto 5432 del contenedor al puerto 5432 del host.

  11. La sección "volumes" monta el volumen local "${PWD}/postgres-data" en "/var/lib/postgresql/data" del contenedor, permitiendo la preservación de datos.

  12. La sección "networks" también asigna el servicio "db" a la red "web_net".

  13. Una red llamada "web_net" se define en la sección "networks" con el controlador "bridge".

  14. Además, se define un volumen llamado "pg-data" utilizando el controlador "local".

  15. Al utilizar la opción "healthcheck" junto con "condition: service_healthy" en elarchivo Docker Compose para esperar a que el servicio de base de datos esté saludable antes de iniciar la aplicación. Con este enfoque, Docker Compose esperará a que el servicio de base de datos esté saludable antes de iniciar la aplicación. La salud del servicio de base de datos se verifica mediante la utilidad "pg_isready", que comprueba si la base de datos está lista para aceptar conexiones. La opción "condition: service_healthy" asegura que la aplicación no se inicie hasta que la base de datos esté disponible y saludable.

Para llevar a cabo el despliegue, ejecute el siguiente comando: docker compose up.

Si está utilizando Docker Desktop, desde su interfaz podrá observar el despliegue completo:

Para verificar esto desde la terminal, ejecute los siguientes comandos en una nueva terminal:

docker compose ls
docker compose images
docker compose ps

Ejecute nuevamente la aplicación (realice pruebas con Postman). En este caso, notará que los datos continúan almacenados en la base de datos, ya que no detuvimos la ejecución de nuestro contenedor de base de datos.

Para detener el despliegue, ejecute el siguiente comando: docker compose down.

Al concluir este tutorial, hemos recorrido Docker y Docker Compose, profundizando en la creación, gestión y despliegue de aplicaciones en contenedores. Desde la configuración de una base de datos y la creación de imágenes personalizadas hasta la automatización del despliegue utilizando Docker Compose.

Hemos explorado cómo empaquetar aplicaciones en contenedores, garantizando la portabilidad y consistencia en diversos entornos. Al emplear Docker Compose, ya podemos coordinar y orquestar múltiples contenedores para crear un entorno de desarrollo completo.

Referencias

Did you find this article valuable?

Support Jesse Padilla by becoming a sponsor. Any amount is appreciated!