1. Introducción: Ejecutar la lógica de despliegue en segundo plano

¡Hola! En el tercer artículo configuramos el entorno del servidor de staging y creamos la estructura básica del servidor webhook FastAPI para recibir solicitudes de GitHub Webhook y validar el secreto. En este proceso, incluimos el código de las funciones handle_deploy y should_rebuild en el archivo main.py, lo que nos permitió vislumbrar la idea principal de la lógica de despliegue real.

En este cuarto artículo, volveremos a aclarar cómo funciona la lógica del manejador de despliegue tratada brevemente en el tercer artículo, y nos centraremos en cómo registrar el servidor webhook FastAPI como un servicio de Systemd para que se ejecute automáticamente y funcione de manera estable incluso tras el reinicio del servidor. ¡Su sistema de despliegue automático está ahora listo para ser más robusto!

Si no han visto el artículo anterior, les recomiendo que lo revisen antes.

① ¿Por qué implementar esto?

② Diseño de la arquitectura y proceso completo

③ Configuración del entorno del servidor de staging y construcción básica del servidor webhook FastAPI


2. Revisiting la lógica del manejador de despliegue (handle_deploy)

La función handle_deploy que escribimos en el tercer artículo se encarga de las tareas de despliegue reales que se ejecutan en segundo plano cuando se recibe una solicitud de GitHub webhook. Esta función desempeña los siguientes roles clave.

Se recomienda revisar la guía a continuación junto con el código de ejemplo del tercer artículo.

2.1. Gestión de múltiples proyectos y configuración de variables de entorno

Cuando se gestionan varios repositorios de GitHub (proyectos) con un solo servidor webhook, es necesario informar dónde se encuentra cada repositorio en el servidor. La función handle_deploy utiliza un diccionario repo_paths para ello, y estos valores están diseñados para ser leídos desde las variables de entorno del servidor.

En el código de ejemplo, aparecen variables de entorno como SAMPLE_PROJECT_1_PATH. Deben establecer la ruta real de cada proyecto en el archivo .env del servidor de staging (este archivo debe estar ubicado en el mismo directorio que main.py del servidor webhook) o en el archivo del servicio systemd de la siguiente manera.

Fragmento de código

# ~/projects/webhook_server/.env

# GitHub Webhook Secret (Configurado en el tercer artículo)
GITHUB_WEBHOOK_SECRET="your_github_webhook_secret_value"

# Rutas reales de cada proyecto en el servidor
SAMPLE_PROJECT_1_PATH="/var/www/my-project1"
SAMPLE_PROJECT_2_PATH="/home/user/another-project"
SAMPLE_PROJECT_3_PATH="/opt/third-project"

La función handle_deploy utiliza el nombre del repositorio recibido en la carga útil del webhook para buscar la ruta de ese proyecto en el diccionario repo_paths. Si no hay una ruta mapeada, se deja una advertencia de Unknown repository y se termina.

Además, también se incluye una lógica flexible para leer configuraciones adicionales como DEBUG o COLOR del archivo .env de cada proyecto (ejemplo: /var/www/my-project1/.env) para usarlas en la selección de archivos Docker Compose (docker-compose.dev.yml o docker-compose.prod.yml) o para configurar el nombre del proyecto Docker. Esto es muy útil para personalizar la forma de despliegue según las características de cada proyecto.

2.2. Decisión de reconstruir la imagen de Docker (should_rebuild)

Reconstruir imágenes de Docker cada vez que se realiza un despliegue sería muy lento e ineficiente. La función should_rebuild utiliza el comando diff de Git para detectar si se han modificado archivos clave que afectan la construcción de la imagen de Docker, como Dockerfile, requirements.txt o .env.

Esta función verifica los cambios entre el commit anterior y el actual con el comando git diff --name-only HEAD~1, y si los archivos modificados incluyen los trigger_files predefinidos (por ejemplo: Dockerfile, requirements.txt, .env, REBUILD_TRIGGER), devuelve True para indicar que se debe reconstruir la imagen.

Especialmente, el archivo REBUILD_TRIGGER, aunque esté vacío, es un truco útil que obliga a la reconstrucción si existe. Puede generarse manualmente en el servidor y utilizarse cuando se requiera una reconstrucción de la imagen por otras razones que no sean git pull. La función should_rebuild detectará este archivo y lo eliminará automáticamente para evitar que se reconstruya innecesariamente en el siguiente despliegue.

2.3. Ejecución de comandos de Git y Docker Compose

La función handle_deploy utiliza el módulo subprocess de Python para ejecutar los comandos git y docker compose en el servidor.

  • subprocess.run(["git", "-C", repo_path, "pull"], check=True): La opción check=True genera un subprocess.CalledProcessError si se produce un error al ejecutar el comando de Git, permitiendo que el código de Python lo detecte y registre adecuadamente el error. La opción -C permite ejecutar comandos de Git en el directorio especificado.

  • subprocess.run(["docker", "compose", "-p", project_name, "-f", compose_file, "up", "-d", "--build"], check=True): Dependiendo del resultado de la función should_rebuild, se ejecuta up -d o se agrega la opción --build para reconstruir la imagen. La opción -p especifica el nombre del proyecto de Docker Compose para evitar conflictos entre múltiples proyectos.

A través de esta lógica, podemos obtener el código más reciente del proyecto con una sola solicitud de webhook y actualizar eficientemente el servicio reconstruyendo la imagen solo cuando es necesario.


3. Operando el servidor webhook FastAPI como un servicio de Systemd

Hasta ahora, hemos ejecutado el servidor webhook manualmente con el comando uvicorn main:app --reload. Sin embargo, si el servidor se reinicia, este proceso desaparece y la sesión de terminal se cierra, deteniendo el servicio. Para prevenir esto y asegurar un funcionamiento estable, debemos registrar el servidor webhook FastAPI como un servicio de Systemd.

3.1. ¿Por qué usar Systemd?

  • Inicio automático: El servicio webhook se iniciará automáticamente incluso después de un reinicio del servidor.

  • Ejecución continua: Se ejecuta en segundo plano sin depender de la sesión de terminal.

  • Fácil gestión: A través del comando systemctl, es sencillo iniciar, detener, reiniciar, comprobar el estado y ver los registros del servicio.

  • Eficiencia de recursos: Como se explicó en el segundo artículo, el servidor webhook en sí es una ligera aplicación de Python ejecutada a través de Systemd, y herramientas pesadas como Git o Docker se aprovechan directamente sin necesidad de configuraciones innecesarias de Docker in Docker o aumento del tamaño de contenedores.

3.2. Creación del archivo de servicio de Systemd (*.service)

Un servicio de Systemd se define a través de un archivo .service que debe estar ubicado en el directorio /etc/systemd/system/. Vamos a crear un archivo llamado github-webhook-deployer.service. Elegí un nombre largo para explicar mejor, pero pueden elegir un nombre que sea breve y comprensible.

# Crear el archivo (se necesitan privilegios de sudo)
sudo nano /etc/systemd/system/github-webhook-deployer.service

Contenido del archivo:

# /etc/systemd/system/github-webhook-deployer.service

[Unit]
Description=Servicio GitHub Webhook Deployer
After=network.target # Este servicio se inicia después de que la red esté activa.

[Service]
User=your_username # Cuenta de usuario que ejecutará este servicio (ejemplo: ubuntu, your_user)
Group=your_username # Grupo del usuario que ejecutará este servicio (ejemplo: ubuntu, your_user)
WorkingDirectory=/home/your_username/projects/webhook_server # Directorio donde se encuentra la aplicación FastAPI

# Configuración de variables de entorno:
# 1. Dado que la aplicación FastAPI carga su propio archivo .env, las variables que utiliza internamente como GITHUB_WEBHOOK_SECRET,
#    no es necesario que se carguen mediante EnvironmentFile. Pero si se desea gestionar todas las variables de entorno de manera consistente a nivel de systemd,
#    se puede mantener la ruta de EnvironmentFile.
# EnvironmentFile=/home/your_username/projects/webhook_server/.env

# 2. Para llamar comandos del sistema como git, docker, docker compose usando subprocess.run(),
#    es necesario añadir explícitamente la ruta donde se encuentran esos comandos en la variable de entorno PATH.
#    La siguiente PATH es un ejemplo que incluye la ruta del sistema Linux común y la ruta bin de un entorno virtual de Python.
#    Asegúrate de adaptar 'your_username' y 'venv' a tu entorno real.
#    La ruta exacta se puede verificar mediante el archivo .bashrc del servidor o el comando 'echo $PATH'.
Environment="PATH=/home/your_username/projects/webhook_server/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

ExecStart=/home/your_username/projects/webhook_server/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 # Usar la ruta del uvicorn en el entorno virtual
Restart=always # El servicio se reinicia automáticamente si se detiene.
StandardOutput=journal # Enviar la salida estándar al journal de Systemd.
StandardError=journal # Enviar los errores estándar al journal de Systemd.

[Install]
WantedBy=multi-user.target # Habilitar el servicio para que se inicie en modo multiusuario (estado de arranque normal del servidor).

Explicación:

  • [Unit] sección: Define información general sobre el servicio (descripción, dependencias). After=network.target indica que el servicio debe iniciarse después de que se active el servicio de red.

  • [Service] sección: Define cómo se ejecutará el servicio.

    • User, Group: Usuario y grupo que ejecutarán el servicio. Debe configurarse un usuario con permisos de Docker (ejemplo: configurado con sudo usermod -aG docker your_username).

    • WorkingDirectory: Directorio donde se encuentra el archivo main.py de la aplicación FastAPI.

    • Environment="PATH=...": Esta parte es clave. Cuando llamas comandos externos como git, docker, docker compose usando subprocess.run(), la variable de entorno PATH puede diferir de la que está configurada en .bashrc. Debes especificar explícitamente la ruta para la ejecución de uvicorn en el entorno virtual (/home/your_username/projects/webhook_server/venv/bin) y las rutas de ejecutables del sistema (/usr/local/bin, /usr/bin, etc.).

    • EnvironmentFile (opcional): La aplicación FastAPI carga su archivo .env usando python-dotenv, por lo que no es necesario cargar las variables que utiliza internamente (ejemplo: GITHUB_WEBHOOK_SECRET, SAMPLE_PROJECT_N_PATH) a través de esta configuración. Sin embargo, si deseas que systemd cargue estas variables antes de iniciar el servicio para proporcionar un ambiente consistente a los procesos que ejecuta ExecStart, usar EnvironmentFile es una opción válida, dependiendo de tus preferencias.

    • ExecStart: El comando real que inicia el servicio. Asegúrate de especificar correctamente la ruta del ejecutable de uvicorn en tu entorno virtual. --host 0.0.0.0 --port 8000 permite recibir solicitudes en el puerto 8000 desde todas las IP.

    • Restart=always: systemd intentará reiniciar automáticamente el servicio si este se detiene por cualquier motivo.

    • StandardOutput=journal, StandardError=journal: Envía toda la salida y errores del servicio al sistema de logs integrado de systemd, conocido como journalctl.

  • [Install] sección: Define bajo qué objetivo (target) se activará el servicio. WantedBy=multi-user.target indica que el servicio se iniciará automáticamente cuando el servidor bootee en modo multiusuario.

3.3. Registro e inicio del servicio de Systemd

Después de crear el archivo del servicio, debes asegurarte de que Systemd reconozca el archivo y luego iniciar el servicio.

# Notificar a Systemd sobre el nuevo archivo de servicio.
sudo systemctl daemon-reload

# Configurar el servicio para que se inicie automáticamente al arrancar.
sudo systemctl enable github-webhook-deployer.service

# Iniciar el servicio.
sudo systemctl start github-webhook-deployer.service

# Verificar el estado del servicio. Debería aparecer como 'active (running)'.
sudo systemctl status github-webhook-deployer.service

Ahora su servidor webhook FastAPI se iniciará automáticamente después de un reinicio del servidor y esperará a recibir solicitudes webhook de manera estable en segundo plano.

Ejemplo de estado del servicio Systemd


4. Monitoreo y depuración del servicio webhook FastAPI

Usar un servicio de Systemd facilita la verificación de logs y el diagnóstico de problemas.

  • Comprobación del estado del servicio:
sudo systemctl status github-webhook-deployer.service

Este comando muestra el estado actual del servicio, la hora de la última ejecución, el ID del proceso, etc.

  • Verificación de logs en tiempo real:
sudo journalctl -u github-webhook-deployer.service -f

La opción -u muestra los logs de un servicio específico, y la opción -f permite que se muestren nuevos logs en tiempo real. Cada vez que se reciba una solicitud webhook, los mensajes de logging configurados en main.py aparecerán aquí.

Si el servicio no se inicia o no funciona como se esperaba, se debe revisar los logs de journalctl primero para buscar mensajes de error.


5. Conclusión: Próximo capítulo

En este cuarto artículo, volvimos a aclarar el funcionamiento de la lógica del manejador de despliegue implementada en el tercer artículo, y aprendimos cómo registrar el servidor webhook FastAPI como un servicio de Systemd para operar de manera estable. Ahora su servidor webhook funcionará sin problemas incluso después de un reinicio del servidor.

En el próximo quinto artículo, ensamblaremos las últimas piezas del rompecabezas del sistema. Trataremos el proceso de configurar Nginx como un proxy inverso para exponer el servidor webhook de manera segura al exterior, aplicar HTTPS para aumentar la seguridad y finalmente vincular el webhook al repositorio de GitHub para probar el despliegue automático real. ¡Esperen ansiosos!