Implementación de CDN privada con Varnish y Nginx reverse proxy
Introducción
En entornos de producción de alto tráfico, la latencia de red, la contención de conexiones y la sobrecarga del servidor de origen son los principales cuellos de botella. Una CDN (Content Delivery Network) tradicional resuelve estos problemas distribuyendo el contenido en múltiples PoPs (Points of Presence) geográficamente dispersos. Sin embargo, para aplicaciones con datos sensibles, requisitos de cumplimiento normativo o arquitecturas on-premise, una CDN privada se convierte en la solución óptima.
Este artículo detalla la implementación de una CDN privada de alto rendimiento utilizando Varnish Cache como acelerador de contenido estático y dinámico, y Nginx como proxy inverso y servidor de borde. El objetivo es construir una capa de almacenamiento en caché distribuida que reduzca la carga del backend, mejore los tiempos de respuesta y ofrezca control granular sobre la política de expiración.
Arquitectura de la CDN Privada
La topología propuesta consta de tres capas lógicas:
- Capa de Borde (Edge Nodes): Nginx configurado como proxy inverso, terminación SSL y balanceador de carga. Actúa como primer punto de contacto.
- Capa de Caché (Cache Layer): Varnish Cache, responsable de almacenar objetos en memoria RAM y aplicar políticas de invalidación.
- Capa de Origen (Origin Servers): Servidores web backend (Apache, Nginx, aplicaciones PHP/Python) que generan el contenido dinámico.
[Usuario Final] <--> [Nginx Edge (SSL/TLS)] <--> [Varnish Cache (RAM)] <--> [Nginx/Apache Origin]
Configuración de Nginx como Proxy Inverso de Borde
Nginx en el borde debe manejar las conexiones entrantes, descifrar SSL, y aplicar reglas de seguridad antes de pasar el tráfico a Varnish.
Instalación y Dependencias
# Para sistemas Debian/Ubuntu
sudo apt update
sudo apt install nginx nginx-extras -y
# Para sistemas RHEL/CentOS/Rocky
sudo dnf install nginx nginx-mod-stream -y
Configuración del VirtualHost para Terminación SSL y Proxy
El bloque server principal debe escuchar en el puerto 443, realizar la terminación SSL y reenviar el tráfico a Varnish, que típicamente escucha en el puerto 80 o 8080.
# /etc/nginx/sites-available/cdn-edge.conf
upstream varnish_backend {
server 127.0.0.1:80; # Varnish escucha en localhost:80
keepalive 16; # Conexiones persistentes para reducir latencia
}
server {
listen 443 ssl http2;
server_name cdn.ejemplo.com;
# Archivos de certificado SSL
ssl_certificate /etc/ssl/certs/ejemplo.com.crt;
ssl_certificate_key /etc/ssl/private/ejemplo.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Headers de seguridad
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
# Logging detallado para debugging
access_log /var/log/nginx/cdn_access.log;
error_log /var/log/nginx/cdn_error.log;
location / {
proxy_pass http://varnish_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
# Tiempos de espera ajustados para Varnish
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Archivos estáticos servidos directamente por Nginx para reducir carga en Varnish
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
root /var/www/static; # Ruta local si es posible
try_files $uri @varnish; # Fallback a Varnish si no existe localmente
}
location @varnish {
proxy_pass http://varnish_backend;
proxy_set_header Host $host;
}
}
Explicación de decisiones técnicas:
keepalive 16: Reduce la sobrecarga de establecer nuevas conexiones TCP entre Nginx y Varnish, mejorando la latencia en picos de tráfico.proxy_set_header Connection "": Fuerza el uso de HTTP/1.1 con keepalive, necesario para que Varnish maneje correctamente las conexiones persistentes.location ~* \.(jpg|...)$: Sirve archivos estáticos directamente desde Nginx si están en disco, evitando que Varnish procese solicitudes de objetos que ya están en caché local. Esto libera RAM de Varnish para otros contenidos.
Configuración de Varnish como Cache Layer
Varnish es el corazón de la CDN privada. Su configuración principal se define en el archivo VCL (Varnish Configuration Language).
Instalación
# Debian/Ubuntu
curl -s https://packagecloud.io/install/repositories/varnishcache/varnish73/script.deb.sh | sudo bash
sudo apt install varnish -y
# RHEL/Rocky
curl -s https://packagecloud.io/install/repositories/varnishcache/varnish73/script.rpm.sh | sudo bash
sudo dnf install varnish -y
Configuración del Puerto de Escucha
Por defecto, Varnish escucha en el puerto 6081. Debe cambiarse a un puerto estándar (80) para recibir tráfico de Nginx.
# /lib/systemd/system/varnish.service (o /etc/varnish/varnish.params)
ExecStart=/usr/sbin/varnishd -a :80 -f /etc/varnish/default.vcl -s malloc,4G -p thread_pool_min=200 -p thread_pool_max=4000
Parámetros clave:
-s malloc,4G: Asigna 4 GB de RAM al almacenamiento de objetos. Ajustar según la memoria disponible. Varnish es extremadamente rápido en RAM.-p thread_pool_min=200y-p thread_pool_max=4000: Define el pool de hilos para manejar concurrencia. En servidores con muchos núcleos, aumentarthread_pool_maxmejora la capacidad de respuesta.
Archivo VCL (default.vcl) para CDN Privada
Este VCL implementa políticas de caché agresivas para contenido estático, caché condicional para dinámico, y purga de objetos.
vcl 4.1;
import std;
import directors;
# Backend de origen (el servidor web real)
backend origin {
.host = "127.0.0.1";
.port = "8080"; # Nginx o Apache del backend
.connect_timeout = 5s;
.first_byte_timeout = 30s;
.between_bytes_timeout = 10s;
.max_connections = 100;
}
# ACL para purga de caché
acl purgers {
"127.0.0.1";
"10.0.0.0/8"; # Red interna del datacenter
}
sub vcl_recv {
# Normalizar el método de solicitud
if (req.method == "PURGE") {
if (!client.ip ~ purgers) {
return (synth(405, "Method not allowed"));
}
# Purga por URL exacta
ban("req.url == " + req.url + " && req.http.host == " + req.http.host);
return (synth(200, "Purged"));
}
# No cachear métodos no idempotentes
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# Eliminar cookies de bots y rastreadores para mejorar cacheabilidad
if (req.http.User-Agent ~ "(googlebot|bingbot|slurp|yandex)") {
unset req.http.Cookie;
}
# Normalizar URL: minúsculas y eliminar parámetros de tracking
set req.url = std.tolower(req.url);
if (req.url ~ "\?utm_") {
set req.url = regsub(req.url, "\?utm_.*", "");
}
# Cachear contenido estático por mucho tiempo
if (req.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot|pdf)$") {
unset req.http.Cookie;
return (hash);
}
# Cachear contenido dinámico si no tiene cookies de sesión
if (req.http.Cookie !~ "(PHPSESSID|JSESSIONID|wordpress_logged_in)") {
unset req.http.Cookie;
return (hash);
}
return (pass);
}
sub vcl_backend_response {
# Eliminar cookies de la respuesta antes de almacenar en caché
if (beresp.ttl > 0s) {
unset beresp.http.set-cookie;
}
# Forzar TTL para contenido estático
if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot|pdf)$") {
set beresp.ttl = 30d;
set beresp.grace = 1h; # Stale content para mantener disponibilidad
set beresp.keep = 2h; # Tiempo extra para mantener objeto en caché
}
# Cachear páginas HTML dinámicas por 5 minutos
if (beresp.http.Content-Type ~ "text/html") {
set beresp.ttl = 5m;
set beresp.grace = 30s;
}
# Activar cacheo de ESI (Edge Side Includes) si es necesario
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
set beresp.do_esi = true;
}
return (deliver);
}
sub vcl_deliver {
# Añadir header para depuración: HIT o MISS
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
set resp.http.X-Cache-Hits = obj.hits;
}
Explicación de bloques clave:
sub vcl_recv: Aquí se decide qué entra en caché. La ACL de purga permite invalidar objetos desde scripts de CI/CD. La normalización de URLs evita duplicados por parámetros de tracking.sub vcl_backend_response: Define los TTLs.beresp.gracepermite servir contenido obsoleto mientras se refresca en segundo plano (stale-while-revalidate), mejorando la percepción de velocidad.sub vcl_deliver: Los headersX-Cacheson esenciales para monitorear la efectividad de la caché.
Estrategias de Purga e Invalidación de Caché
La invalidación es crítica en una CDN privada. Varnish ofrece dos métodos principales:
1. Purga por URL
curl -X PURGE http://localhost/static/css/main.css
2. Purga por expresión regular (Ban)
sub vcl_recv {
if (req.method == "BAN") {
if (!client.ip ~ purgers) {
return (synth(405, "Method not allowed"));
}
# Ban por patrón en URL
ban("req.url ~ " + req.url);
return (synth(200, "Banned"));
}
}
Automatización con Script
#!/bin/bash
# purge.sh - Purga todos los objetos de un directorio específico
PURGE_URL="$1"
if [ -z "$PURGE_URL" ]; then
echo "Uso: $0 /ruta/a/purgar"
exit 1
fi
# Enviar solicitud BAN a Varnish
curl -X BAN "http://127.0.0.1:80${PURGE_URL}" -H "Host: cdn.ejemplo.com"
¿Por qué usar Ban en lugar de Purge? Ban es más eficiente para purgas masivas (ej: toda la carpeta /css/), ya que Varnish marca los objetos con un timestamp y los compara en cada petición, sin necesidad de recorrer toda la caché.
Monitoreo y Métricas de Rendimiento
Para validar la efectividad de la CDN privada, es necesario monitorear:
| Métrica | Herramienta | Comando/Configuración |
|---|---|---|
| Hit Rate | varnishstat | varnishstat -1 -f MAIN.hit_rate |
| Objetos en caché | varnishstat | varnishstat -1 -f MAIN.n_object |
| Uso de memoria | varnishstat | varnishstat -1 -f SMA.s0.g_bytes |
| Latencia de backend | varnishlog | varnishlog -g request -q "VCL_Log[Backend]" |
| Tráfico por segundo | nginx status | stub_status on; en Nginx |
Integración con Prometheus y Grafana
# En Nginx, habilitar stub_status
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
Para Varnish, usar varnish_exporter:
wget https://github.com/jonnenauha/prometheus_varnish_exporter/releases/download/v1.3/varnish_exporter-1.3.linux-amd64.tar.gz
tar -xzf varnish_exporter-1.3.linux-amd64.tar.gz
./varnish_exporter -web.listen-address=":9131"
Seguridad y Hardening
Protección contra DDoS en Nginx
# Limitar tasa de conexiones por IP
limit_req_zone $binary_remote_addr zone=cdn_limit:10m rate=30r/s;
server {
location / {
limit_req zone=cdn_limit burst=20 nodelay;
proxy_pass http://varnish_backend;
}
}
Restricción de Backend en Varnish
Evitar que el backend sea accesible directamente:
sub vcl_recv {
# Bloquear acceso directo a IP del backend
if (req.http.host == "10.0.0.10") {
return (synth(403, "Forbidden"));
}
}
Escalado Horizontal de la CDN Privada
Para manejar más tráfico, se pueden añadir múltiples nodos Varnish detrás de un balanceador de carga.
Configuración de Varnish con Backend Múltiple
# /etc/varnish/default.vcl
import directors;
backend origin1 {
.host = "10.0.1.10";
.port = "8080";
}
backend origin2 {
.host = "10.0.1.11";
.port = "8080";
}
sub vcl_init {
new vdir = directors.round_robin();
vdir.add_backend(origin1);
vdir.add_backend(origin2);
}
sub vcl_recv {
set req.backend_hint = vdir.backend();
}
Sincronización de Caché entre Nodos
Varnish no tiene replicación de caché nativa. Para una CDN privada multi-nodo, se recomienda:
- Hash por URL: Cada nodo almacena todos los objetos (no sharding).
- Invalidación centralizada: Usar un script que envíe purgas a todos los nodos.
#!/bin/bash
# purge_all_nodes.sh
NODES=("10.0.2.10" "10.0.2.11" "10.0.2.12")
PURGE_URL="$1"
for node in "${NODES[@]}"; do
curl -X PURGE "http://${node}:80${PURGE_URL}" -H "Host: cdn.ejemplo.com" &
done
wait
Consideraciones de Rendimiento y Ajuste Fino
Tuning del Kernel para Varnish
# /etc/sysctl.d/99-varnish.conf
net.core.rmem_max = 134217728
