SecNot

mar 22, 2014

Cacheando Amazon S3 con Varnish

Amazon S3 es un gran servicio para alojar los archivos de una página web, ofrece alta disponibilidad, fiabilidad, y seguridad (configurado correctamente). Pero en páginas con demasiado tráfico, con una gran cantidad de peticiones, y/o ancho de banda, como por ejemplo páginas que alojen gran cantidad de imágenes, puede hacer el coste de S3 prohibitivo.

En estos casos la alternativa es usar un CDN, o gestionar tu propio sistema de distribución. Un CDN puede resultar algo más barato dependiendo del tráfico, pero no significativamente. Gestionar tu propio servidor aumenta la complejidad de la aplicación, los posibles puntos de fallo, es menos escalable, y más difícil de administrar.

Existe una solución intermedia, que consiste en seguir almacenando los archivos en S3, y usar Varnish como proxy cache. De esta manera cuando una petición de un archivo llega, Varnish lo obtiene de S3, se lo envía al cliente, y guarda una copia en caché. Sucesivas peticiones de ese archivo se sirven directamente desde cache.

Con esta configuración no es necesario modificar el backend de almacenamiento ya que se sigue usando S3, es altamente escalable, facil de administrar, y permite seguir usando S3 como fallback.

Configuración de Varnish

Este artículo no es un tutorial de Varnish, si eso es lo que estas buscando, hay muchos disponibles en la red, pero te recomiendo que empieces por los enlaces al final del articulo.

Archivo de configuración para Varnish con S3 default.vcl:

/* Amazon S3 Backend */
backend s3 {
    .host = "nombrebucket.s3.amazonaws.com";
    .port = "80";
}


/* vcl_recv es llamado cada vez que una petición es recibida */
sub vcl_recv {

    /* Solo servimos peticiones GET y HEAD */
    if (req.request != "GET" && req.request != "HEAD") {
        return (pass);
    }

    /* Solo servimos archivos, asi que ni Cookies ni Autorizaciones */
    if (req.http.Authorization || req.http.Cookie) {
        return (pass);
    }

    set req.grace = 120s;
    set req.http.X-Forwarded-For = client.ip;

    /* Establecer backend para la peticion */
    set req.backend = s3;
    set req.http.host = "nombrebucket.s3.amazonaws.com";

    /* Eliminar de la cabecera todos los campos que pueden afectar la cache */
    unset req.http.Cache-Control;
    unset req.http.Pragma;
    unset req.http.Expires;

    /* Si el archivo es una imagen desactivamos compresion */
    if (req.url ~ "\.(jpeg|jpg|png|ico|svg|gif)$") {
        unset req.http.Accept-Encoding;
        return (lookup);
    }
    else if (req.url ~ "\.(css|js|txt)$") {
        return (lookup);
    }

    /* Demas tipos no soportados */
    return (pass);
}


/* Llamado cuando hay un hit en cache */
sub vcl_hit {
    return (deliver);
}


/* Llamado cuando hay un miss en cache */
sub vcl_miss {
    return (fetch);
}


/* Llamado cuando un documento se ha descargado exitosamente del backend */
sub vcl_fetch {
    /* Se eliminan cookies antes de que el objeto sea introducido en cache */
    unset beresp.http.Set-Cookie;

    /* Si es una imagen se desactiva compresion (Solo precaucion) */
    if (req.url ~ "\.(jpg|jpeg|png|ico|svg|gif)$") {
        set beresp.do_gzip = false;
    }

    /* Establecer fecha de caducidad para los datos en cache */
    set beresp.ttl = 1w; /* Una semana */
    set beresp.grace = 120s;

    /* Forzar cacheado por el cliente */
    set beresp.http.Expires = beresp.ttl;
    set beresp.http.Cache-Control = "max-age=604800"; /* 7 dias */

    /* Ocultar información añadida por amazon antes de almacenar */
    unset beresp.http.Server;
    unset beresp.http.x-amz-id-2;
    unset beresp.http.x-amz-request-id;
    unset beresp.http.x-amz-version-id;
    unset beresp.http.ETag;

    return (deliver);
}


sub vcl_deliver {

    /* Eliminar informacion superflua antes de enviar respuesta */
    unset resp.http.Via;
    unset resp.http.X-Varnish;

    /* DEBUG: Util para comprobar el correcto funcionamiento de la cache */
    /* unset resp.http.Age; */

    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }

    return (deliver);
}

Descargar

El archivo de configuración del demonio es /etc/default/varnish, y en el hay que modificar el puerto de escucha y especificar como queremos que Varnish almacene la cache, esto dependerá de la cantidad de memoria disponible, el tamaño del bucket, y la latencia del disco duro. En mi caso el servidor es un VPS con 512MB de RAM y una SSD de 20GB alojado en Digital Ocean, y como no tiene suficiente RAM, uso un archivo para la cache:

DAEMON_OPTS="-a :80 \
    -T localhost:6082 \
    -f /etc/varnish/default.vcl \
    -S /etc/varnish/secret \
    -s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,4G"

Si tienes disponible suficiente memoria para almacenar la cache en RAM, podrías usar la configuración:

DAEMON_OPTS="-a :80 \
    -T localhost:6082 \
    -f /etc/varnish/default.vcl \
    -S /etc/varnish/secret \
    -s malloc,4G"

Una vez configurado y reiniciado, puedes comprobar que esta funcionando correctamente con curl. Al descargar cualquier documento por primera vez debería contener X-Cache: MISS, sucesivas peticiones del mismo documento contendrán X-Cache: HIT si la cache esta funcionando.

$ curl -I static.tudominio.com/imagen.jpg
Last-Modified: Mon, 17 Mar 2014 17:51:08 GMT
Content-Type: image/jpeg
Expires: 604800.000
Cache-Control: max-age=604800
Content-Length: 9920
Accept-Ranges: bytes
Date: Sat, 22 Mar 2014 02:55:40 GMT
Age: 0
Connection: keep-alive
X-Cache: MISS

$ curl -I static.tudominio.com/imagen.jpg
Last-Modified: Mon, 17 Mar 2014 17:51:08 GMT
Content-Type: image/jpeg
Expires: 604800.000
Cache-Control: max-age=604800
Content-Length: 9920
Accept-Ranges: bytes
Date: Sat, 22 Mar 2014 02:55:40 GMT
Age: 8
Connection: keep-alive
X-Cache: HIT

Cuando compruebes que funciona ya puedes comentar la sección que añade el campo X-Cache.

Configurar Django

Si usas Django, una vez Varnish este funcionando hay que indicar a Storage, como generar la dirección para los archivos que antes se accedían a traves de s3.amazoaws.com. El parámetro AWS_S3_CUSTOM_DOMAIN permite apuntar al dominio que estés usando para tu servidor Varnish.

AWS_S3_CUSTOM_DOMAIN = 'servidorvarnish.tudominio.com'

Si no estás usando el puerto 80 puedes espeficicarlo con:

AWS_S3_CUSTOM_DOMAIN = 'www.tudominio.com:8080'
Click to read and post comments