Arquitectura Bare Metal Distribuida y Balanceada (Alta Disponibilidad)
La Arquitectura Bare Metal Distribuida de Mañuti desacopla el monolito en una granja de servidores físicos o virtuales (VPS) independientes sobre Ubuntu LTS, conectados a través de una Red Privada (VPC/LAN) bajo una política estricta de Zero Trust (Confianza Cero) e ISO/IEC 27001.
En esta topología, los servidores de aplicación son 100% Stateless (Sin Estado Local): cualquier nodo Web puede destruirse o clonarse horizontalmente en segundos sin perder sesiones, archivos subidos, tareas programadas ni logs de errores.
1. Topología de Red e Inventario de Nodos
flowchart TD
U(["Usuarios / Clientes Externos"])
LB["NODO LB: LOAD BALANCER NGINX<br/>(192.168.1.122 / .143)<br/>[SSL Termination + Bastión Deploy]"]
WEB["NODOS WEB (1..N)<br/>(.64 / .130 / .147..)<br/>[Nginx + PHP 8.5]"]
WORKER["NODO WORKER<br/>(.126 / .150)<br/>[Horizon + Cron]"]
RT["NODO REALTIME<br/>(.127 / .151)<br/>[Reverb Server]"]
CACHE[("NODO CACHÉ: REDIS<br/>(.124 / .146 :6379)")]
SQL[("NODO SQL<br/>MariaDB (:3306)<br/>(.123 / .144)")]
NOSQL[("NODO NoSQL<br/>MongoDB (:27017)<br/>(.120 / .145)")]
STORAGE[("NODO STORAGE<br/>MinIO S3 (:9000)<br/>(.125 / .132)")]
LOGS[("LOGS DE SISTEMA<br/>system_errors<br/>(En MongoDB)")]
U -->|"HTTPS (443/80)"| LB
U -->|"WSS (WebSockets /app)"| LB
LB -->|"Balanceo HTTP (Upstream)"| WEB
LB -->|"Proxy WSS (/app -> :8080)"| RT
WEB -->|"(1) Sesiones/Cache/Pub"| CACHE
WORKER -->|"(2) Consume Colas"| CACHE
CACHE -.->|"(3) Subscribe Pub/Sub"| RT
WEB --> SQL
WEB --> NOSQL
WEB --> STORAGE
WEB --> LOGS
Matriz de Roles de Servidores
| Capa | Rol del Nodo | Servicios Activos | Estado | Puertos Internos (UFW) |
|---|---|---|---|---|
| Acceso | Load Balancer (LB) | Nginx Reverse Proxy + Bastión SSH | Stateless | 80/tcp, 443/tcp, 22/tcp |
| App | Nodos WEB (01..N) | Nginx + PHP 8.5-FPM | Stateless | 80/tcp (Solo desde LB), 22/tcp |
| App | Nodo WORKER | Supervisor (horizon) + Cron Maestro |
Stateless | 22/tcp (Sin tráfico entrante web) |
| App | Nodo REALTIME | Supervisor (reverb:start) |
Stateless | 8080/tcp (Desde LB/LAN), 22/tcp |
| Datos | Nodo SQL | MariaDB 10.11+ | Stateful | 3306/tcp (Solo desde App Nodes) |
| Datos | Nodo NoSQL | MongoDB 7.0+ | Stateful | 27017/tcp (Solo desde App Nodes) |
| Datos | Nodo CACHÉ | Redis 7.0+ | Stateful | 6379/tcp (Solo desde App Nodes) |
| Storage | Nodo S3 | MinIO Object Storage | Stateful | 9000/tcp (API), 9001/tcp (Consola) |
2. Pilares del Código Stateless en Mañuti
Antes de repartir el tráfico entre múltiples servidores, Mañuti implementa 5 adaptaciones críticas para operar sin estado local:
2.1. Trusted Proxies (bootstrap/app.php)
Como todo el tráfico pasa por el Load Balancer, Laravel vería la IP del balanceador en lugar de la IP real del usuario, arruinando la auditoría forense. Mañuti lee TRUST_PROXIES desde el .env:
// bootstrap/app.php
$trustedProxies = env('TRUST_PROXIES', null);
if ($trustedProxies) {
$proxiesArray = $trustedProxies === '*' ? '*' : array_map('trim', explode(',', $trustedProxies));
$middleware->trustProxies(at: $proxiesArray);
}
$middleware->trustProxies(headers: Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO);
2.2. Almacenamiento Distribuido con MinIO (S3)
Ningún archivo subido (avatares, mapas SVG de arquitectura o exportaciones JSON de privacidad) se guarda en el disco duro de los nodos Web. Todo viaja vía league/flysystem-aws-s3-v3 al servidor MinIO usando FILESYSTEM_DISK=s3 y storePublicly(). Includes reglas de ciclo de vida (mc ilm rule add) para autodestruir exportaciones temporales tras 24 horas.
2.3. Scheduler Atómico (->onOneServer())
Para evitar que 3 servidores ejecuten la misma tarea programada 3 veces, Mañuti centraliza el Cron en el Nodo Worker y además protege cada tarea en routes/console.php con bloqueos atómicos en Redis (SETNX):
Schedule::command('privacy:prune-logs')->dailyAt('03:00')->onOneServer();
Schedule::call(function () {
Artisan::call('system:backup');
})->dailyAt('02:00')->name('system-backup-dynamic')->onOneServer();
2.4. Escalado Horizontal de Reverb vía Redis Pub/Sub
Al activar REVERB_SCALING_ENABLED=true, cuando el Nodo Web 2 dispara un evento (ej. Kill Switch), lo publica en el canal reverb de Redis. El Nodo Realtime está suscrito a Redis, captura el evento y lo empuja por el túnel WebSocket al navegador del usuario.
2.5. Logging Centralizado de Excepciones en MongoDB con Fallback
En lugar de fragmentar errores en archivos storage/logs/laravel.log repartidos en múltiples servidores, Mañuti utiliza App\Logging\CreateMongoLogger con un FallbackGroupHandler de Monolog:
- Intenta escribir toda excepción del sistema en la colección
system_errorsde MongoDB (incluyendo la IP del nodo que falló y el Stack Trace). - Si MongoDB sufriera una caída de red, conmuta automáticamente al archivo rotativo local sin derribar la aplicación.
- Incluye un visor administrativo en tiempo real (Monitor de Errores del Sistema) dentro del panel.
3. Configuración del Balanceador de Carga (Nodo LB)
El Nodo LB es la única cara pública del clúster. No tiene PHP ni bases de datos; únicamente ejecuta Nginx como Reverse Proxy para tráfico HTTP y túnel WebSockets (WSS).
Archivo /etc/nginx/sites-available/framedev_lb:
upstream backend_servers {
server 192.168.1.64:80;
server 192.168.1.130:80;
keepalive 32;
}
upstream reverb_server {
server 192.168.1.127:8080;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
# 1. TRÁFICO HTTP DE APLICACIÓN (Balanceado entre Nodos Web)
location / {
proxy_pass http://backend_servers;
# Header de diagnóstico para identificar qué Nodo Web respondió
add_header X-Served-By $upstream_addr always;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 90;
}
# 2. TÚNEL WEBSOCKETS (Enrutado directo al Nodo Realtime Reverb)
location ~ ^/app/ {
proxy_pass http://reverb_server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
}
4. Aprovisionamiento y Blindaje de la Capa de Datos (Stateful)
Todos los nodos deben sincronizar su reloj mediante chrony (sudo timedatectl set-timezone UTC && sudo apt install -y chrony) para garantizar la integridad de los tokens y logs forenses.
4.1. Nodo SQL (MariaDB)
- Tuning de Kernel (
/etc/sysctl.confy/etc/security/limits.conf):vm.swappiness=1yvm.vfs_cache_pressure=50.- File Descriptors (
nofile) elevados a65535para el usuariomysql.
- Red (
/etc/mysql/mariadb.conf.d/50-server.cnf):[mysqld] bind-address = 0.0.0.0 max_connections = 500 - Privilegios Mínimos por Subred:
CREATE USER 'framedev_user'@'192.168.1.%' IDENTIFIED BY 'PasswordSeguroSQL'; GRANT ALL PRIVILEGES ON framedev_db.* TO 'framedev_user'@'192.168.1.%'; FLUSH PRIVILEGES; - Firewall UFW (Zero Trust):
sudo ufw default deny incoming sudo ufw allow ssh sudo ufw allow from 192.168.1.64 to any port 3306 proto tcp comment 'WEB-01' sudo ufw allow from 192.168.1.130 to any port 3306 proto tcp comment 'WEB-02' sudo ufw allow from 192.168.1.126 to any port 3306 proto tcp comment 'WORKER' sudo ufw enable
4.2. Nodo CACHÉ (Redis)
- Tuning de Kernel (
/etc/sysctl.d/99-redis-tuning.conf):vm.overcommit_memory = 1(Evita fallos en snapshots en segundo plano).net.core.somaxconn = 65535(Cola TCP de alta concurrencia).- Desactivar Transparent Huge Pages (THP) mediante servicio
systemd(disable-thp.service).
- Configuración (
/etc/redis/redis.conf):bind 0.0.0.0 protected-mode yes requirepass "PasswordSeguroRedis" appendonly yes - Firewall UFW: Abrir el puerto
6379/tcpexclusivamente para las IPs deWEB-01,WEB-02,WORKERyREALTIME.
4.3. Nodo NoSQL (MongoDB)
- Control de Versiones (ISO 27001): Bloquear actualizaciones automáticas no planeadas con
echo "mongodb-org hold" | sudo dpkg --set-selectionsy desactivar THP. - Autenticación y Red (
/etc/mongod.conf):net: port: 27017 bindIp: 0.0.0.0 security: authorization: enabled - Cadena de Conexión con
authSource=admin:MONGODB_DSN="mongodb://framedev_mongo:[email protected]:27017/framedev_logs?authSource=admin"
5. Especialización y Saneamiento de Nodos de Aplicación
Si los nodos de aplicación se crean clonando una Máquina Virtual base, es obligatorio purgar los servicios que no corresponden a su rol para ahorrar RAM y reducir la superficie de ataque:
- En TODOS los Nodos de Aplicación (Web, Worker, Realtime): Purgar los servidores de bases de datos locales (
sudo apt purge mariadb-server redis-server mongodb-org-server -y), conservando únicamente los clientes CLI (mariadb-client,redis-tools) y las extensiones de PHP 8.5. - En Nodos WEB:
- Deshabilitar
supervisory limpiar cualquiercrontab. - Ajustar el pool de PHP-FPM (
/etc/php/8.5/fpm/pool.d/www.conf) respetando la regla matemáticamin_spare_servers (5) < start_servers (10) < max_spare_servers (15)conpm.max_children = 50. - Firewall UFW: Permitir puerto
80/tcpúnicamente desde la IP del Load Balancer.
- Deshabilitar
- En Nodo WORKER (Horizon):
- Purgar
nginxpor completo. - En
/etc/supervisor/conf.d/conservar únicamenteframedev-horizon.conf. - Activar el único Crontab maestro del clúster (
sudo crontab -u www-data -e).
- Purgar
- En Nodo REALTIME (Reverb):
- Purgar
nginxpor completo. - En
/etc/supervisor/conf.d/conservar únicamenteframedev-reverb.confejecutandophp artisan reverb:start --host=0.0.0.0 --port=8080conminfds=10000.
- Purgar