🎨 Sysprovider Code
Sysprovider LogoWiki
🇪🇸Hosting español para ecommerce

Implementación de CDN privada con Varnish y Nginx reverse proxy

Actualizado el 29 de abril de 2026

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:

  1. Capa de Borde (Edge Nodes): Nginx configurado como proxy inverso, terminación SSL y balanceador de carga. Actúa como primer punto de contacto.
  2. Capa de Caché (Cache Layer): Varnish Cache, responsable de almacenar objetos en memoria RAM y aplicar políticas de invalidación.
  3. 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=200 y -p thread_pool_max=4000: Define el pool de hilos para manejar concurrencia. En servidores con muchos núcleos, aumentar thread_pool_max mejora 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.grace permite servir contenido obsoleto mientras se refresca en segundo plano (stale-while-revalidate), mejorando la percepción de velocidad.
  • sub vcl_deliver: Los headers X-Cache son 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étricaHerramientaComando/Configuración
Hit Ratevarnishstatvarnishstat -1 -f MAIN.hit_rate
Objetos en cachévarnishstatvarnishstat -1 -f MAIN.n_object
Uso de memoriavarnishstatvarnishstat -1 -f SMA.s0.g_bytes
Latencia de backendvarnishlogvarnishlog -g request -q "VCL_Log[Backend]"
Tráfico por segundonginx statusstub_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:

  1. Hash por URL: Cada nodo almacena todos los objetos (no sharding).
  2. 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

¿Necesitas ayuda?Son dos de nuestros técnicos, Agustín y Mikel, y están disponibles para resolver cualquier problema.

Hablar con ellos ahora
Agustín y Mikel