Introducción: El Costo Oculto de “Simplemente Funciona”
Cuando migré por primera vez esta Base de Conocimiento a Hugo y configuré mi pipeline de Integración y Despliegue Continuo (CI/CD) usando GitHub Actions, mi objetivo principal era la simplicidad. Necesitaba una forma de hacer un push de mis archivos Markdown a GitHub y que allí un contenedor compilara automáticamente el HTML estático para enviarlo a mi servidor Nginx alojado en un VPS remoto.
Para lograr esto, usé una Acción de Despliegue SFTP muy popular y lista para usar. Durante los primeros días, fue mágico. Publicaba un nuevo post y, en cuestión de 2 minutos, el sitio estaba en vivo.
“Simplemente funciona”, pensé, y pasé a otros proyectos.
Sin embargo, a medida que el blog crecía —pasando de 10 posts a 30, y luego a más de 50 archivos repletos de diagramas de arquitectura de ciberseguridad en alta resolución— un problema exponencial y silencioso se estaba gestando en mis registros de despliegue. Mis tiempos empezaron a subir a 8 minutos, luego a 16 minutos, y eventualmente, corregir un simple error tipográfico en un post antiguo tomaba casi 25 minutos en publicarse.
Para alguien que opera en el nivel gratuito de GitHub Actions (que te limita a 2,000 minutos de CI/CD al mes), esto era un cuello de botella que debía ser resuelto. En este post, explicaré por qué SFTP —aunque excelente por su simplicidad inicial— puede convertirse en una carga a medida que tu sitio crece, y cómo cambiar a una arquitectura diferencial de sincronización con rsync redujo mis tiempos de despliegue en un asombroso 98%.
El Problema: SFTP es un Protocolo “Tonto”
La causa raíz de este tiempo de despliegue inflado era el protocolo SFTP en sí mismo. En mi deploy.yml original, el pipeline se veía así:
- name: Build Hugo site
run: hugo --source mxlit-site/mxlit-blog --minify
- name: SFTP Deploy
uses: wlixcc/[email protected]
with:
username: $\{{ secrets.FTP_USERNAME }}
# ... (credenciales ssh)
local_path: './mxlit-site/mxlit-blog/public/*'
remote_path: '/home/deploy-mxlit/mxlit-site/public'
Cuando le dices a un cliente SFTP que suba un directorio que contiene 500 archivos, hace exactamente eso, a ciegas. No tiene ningún concepto de “estado” o “diferencias”. Cada vez que se ejecutaba el GitHub Action:
- Hugo compilaba el sitio desde cero (tomando cerca de 1 segundo).
- La Acción de SFTP abría la conexión encriptada y empezaba a subir
index.html. - Luego subía
image1.png. - Luego
image2.png. - …y así sucesivamente, reescribiendo cada uno de los archivos en la carpeta
public.
Incluso si yo solo había cambiado una sola coma en un archivo Markdown, SFTP sobrescribía despiadadamente Megabytes de imágenes, CSS y HTML estructural que no habían sufrido ningún cambio en el servidor Nginx remoto. Era un desperdicio masivo de ancho de banda y tiempo de cómputo.
(La captura anterior muestra el momento en que mi tiempo de despliegue alcanzó los 25 minutos usando el método SFTP.)
La Solución: Sincronización Diferencial con Rsync
Para resolver este embotellamiento, debemos recurrir a una herramienta diseñada específicamente para reflejar o espejear (“mirroring”) el estado de dos servidores: Rsync.
A diferencia de SFTP, rsync es un protocolo diferencial “inteligente”. Cuando un cliente rsync se conecta a un servidor destino a través de SSH, primero le pide al servidor un manifiesto de sus archivos actuales y sus hashes de modificación. Luego, compara matemáticamente el manifiesto remoto con los archivos locales que intenta subir.
Si detecta que 499 de 500 archivos son completamente idénticos hasta el último byte, simplemente los ignora. Solo transmite a través de la red el único archivo que sufrió modificaciones.
Reescribiendo la Acción en GitHub
Inmediatamente arranqué la acción de SFTP de mi .github/workflows/deploy.yml y la reemplacé por el módulo easingthemes/ssh-deploy, el cual utiliza poderosamente Rsync por debajo del capó.
Así quedó el paso de despliegue ultrarrápido:
- name: Deploy via Rsync to VPS
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: $\{{ secrets.FTP_SSH_KEY }}
REMOTE_HOST: $\{{ secrets.FTP_SERVER }}
REMOTE_USER: $\{{ secrets.FTP_USERNAME }}
REMOTE_PORT: $\{{ secrets.FTP_PORT }}
SOURCE: 'mxlit-site/mxlit-blog/public/'
TARGET: '/home/deploy-mxlit/mxlit-site/public/'
# -rltgoDzvO (Sincronización preservando permisos, comprimido en el envío)
# --delete (Purga archivos fantasma en Nginx que ya hayan sido borrados de GitHub)
ARGS: "-rltgoDzvO --delete"
Quiero hacer hincapié en la variable crítica ARGS, específicamente en el uso del argumento --delete. Esta instrucción obliga a Nginx a convertirse exactamente en un espejo del espacio de trabajo estático de GitHub Actions. Si yo elimino una imagen antigua de mi repositorio de GitHub, Rsync rastreará el contenedor local de Nginx y eliminará silenciosamente los archivos huérfanos, evitando que el almacenamiento de mi VPS acumule basura estéril con el paso de los años, algo de lo que SFTP carece totalmente.
(Con Rsync, incluso con cientos de archivos, el despliegue ahora se completa en segundos ya que solo se transmiten los cambios.)
Añadiendo el Gatillo “Cron” para Publicaciones Automáticas
Mientras reestructuraba el Pipeline, me percaté de otro defecto fatal al desplegar sitios estáticos exclusivamente por hacer push manualmente. Hugo te permite programar el parámetro date: con la fecha orientada al futuro dentro del frontmatter en tu markdown en orden para retener la publicación de las entradas. Naturalmente, Hugo descarta compilar estáticamente HTML de cualquier entrada “atrapada en el futuro” relativa al reloj local o del servidor que está despachando la compilación.
Dado a que mi Nginx es solo un servidor web “tonto” que sirve lo que GitHub Action le entrega empaquetado como HTML, un post debidamente programado para las 11:00 AM nunca vería la luz si mi ultimo push en GitHub ocurrió a las 9:00 AM. Sencillamente porque el contenedor de Ubuntu en aquél entonces no compiló esos archivos HTML.
Para remediar esto definitivamente, inyecté el gatillo (trigger) de programación schedule dentro del comienzo de mi deploy.yml:
on:
push:
branches:
- main
paths:
- 'mxlit-site/mxlit-blog/**'
schedule:
- cron: '0 * * * *'
Actualmente, mis instáncias en GitHub Actions despiertan sigilosamente y de forma independiente en cada inicio de hora (en formato UTC), ejecutan hugo, descubren cuáles artículos cruzan el lindero de publicáción y se comunican ágilmente en cuestión de segundos vía Rsync para añadir de 2 o 3 paginas en mi arquitectura en Francia sin que yo mueva un solo dedo.
Conclusión
Se requiere bastante madurez técnica para examinar y admitir mediante un registro de depuración demorado (tardanza de 25 min) que, en ocasiones, tus elecciones iniciales y arquitectónicas, aunque perfectas para empezar, necesitan evolucionar. Estar resguardado bajo la estricta ideología “Si no está roto, no lo arregles” tiene sus límites en la Ingeniería de Sistemas (Systems Engineering). SFTP “no estaba roto”; tan solo dejó de ser la herramienta adecuada para una Base de Conocimiento en crecimiento.
Tras realizar el cambio crítico hacia la estamina moderna de Rsync, no solamente mis periodos en los tiempos de despliegue cedieron contundentemente pasando de ~25 minutos a lapsos triviales y ridículos (segundos), sino que el pipeline adoptó atributos íntegros con capacidades reflejadas state-mirroring (--delete), sumadas también, de una verdadera capacidad programable remota sin supervisión. La ideología propia del término “Optimización” siempre resulta en realizar recorridos redituables en espirales de mejora, y con frecuencia las más sabias y crudas grandes lecciones suceden cuando el reloj dictamina límites críticos.