Usar CloudFront con un bucket de S3 como origen para servir Single Page Application (SPA) es un enfoque popular que ofrece varios beneficios: es totalmente serverless, lo que significa que no hay infraestructura que mantener, parchear o actualizar; es eficiente en costes, ya que los costes se basan en el uso; y es flexible y escalable, con CloudFront y S3 siendo capaces de manejar millones de solicitudes de usuario.
Esta arquitectura se puede mejorar añadiendo un API Gateway como origen personalizado. Esto le permite tener su frontend y backend bajo el mismo dominio y certificado. Además, ofrece la ventaja de no tener que lidiar con configuraciones CORS.
Pero, esta solución tiene un pequeño problema. Veamos qué pasa. Cuando visita su nuevo SPA y solicita una ruta, ocurre esto.
¿Cómo ha pasado esto? #
Para comprender lo que está sucediendo, necesitamos entender qué es una ruta en un SPA y cómo CloudFront y S3 procesan una solicitud.
Rutas SPA #
Las Rutas SPA, también conocido como enrutamiento del lado del cliente, son un mecanismo que permite cargar recursos procesados en el lado del cliente, es decir, el navegador del usuario, sin necesidad de enviar solicitudes al servidor. Cada vez que se solicita una nueva ruta en el navegador del usuario, el JavaScript del SPA la interpreta y renderiza el componente apropiado.
Solicitudes a CloudFront y S3 #
En una distribución CloudFront con S3 como origen, cuando una solicitud llega a CloudFront, primero comprueba si el objeto ya está en la caché. En caso negativo, remite la solicitud a S3. En un SPA, si el recurso solicitado es un objeto, como /index.html
o /img/picture.png
, S3 devolverá el objeto a la caché de la distribución, y CloudFront lo devolverá al solicitante. Por otro lado, si el recurso solicitado es una ruta, como /route-1
, y no hay un objeto correspondiente en S3, devolverá un código de error 403
.
Una solución tradicional a la cuestión de los códigos de error 403
en las SPAs alojadas en AWS CloudFront es utilizar las páginas de error personalizadas de CloudFront. Esto implica configurar CloudFront para responder a errores HTTP 403
(que se devuelven cuando una ruta solicitada no corresponde a un objeto en el bucket S3) con una redirección al archivo index.html
. Si bien este método puede resolver el problema, no es lo ideal para nuestra arquitectura porque implica una redirección adicional, lo que podría ralentizar el tiempo de respuesta de la aplicación, y no proporciona una integración perfecta entre el frontend y el backend bajo el mismo dominio y certificado.
En términos más simples, las páginas de error personalizadas de CloudFront afectan a toda la distribución, lo que podría llevar a un comportamiento inesperado. Si API Gateway devuelve un código de error 403
debido al acceso no autorizado, obtendremos un código HTTP 200
en su lugar y el contenido de /index.html
, evitando que el solicitante (es decir, el frontend) gestione el error correctamente.
CloudFront Functions al rescate #
CloudFront Functions es una característica de AWS CloudFront que permite ejecutar funciones JavaScript ligeras en las ubicaciones edge de la red de CloudFront, cerca de los usuarios. Esta función está diseñada para operaciones de alta escala, sensibles a latencia como manipular cabeceras HTTP, reescritura de URLs, redirecciones, y otras tareas que se pueden ejecutar rápidamente sin necesidad de acceder a otros recursos o servicios. Se utilizan principalmente para manejar eventos de solicitud y respuesta del cliente, lo que le permite personalizar el contenido que CloudFront entrega.
CloudFront Functions y Lambda@Edge son parte del conjunto de soluciones serverless de AWS, pero son utilizados para diferentes propósitos y tienen diferentes entornos de ejecución.
Lambda@Edge está diseñado para operaciones más pesadas y tiene la capacidad de ejecutar lógica compleja, incluida la interacción con otros servicios de AWS. También le permite escribir funciones en varios leguajes, no solo JavaScript.
Por otro lado, CloudFront Functions están diseñadas para operaciones más ligeras y sensibles a la latencia. Sin embargo, CloudFront Functions solo soportan JavaScript.
En términos de costo, CloudFront Functions es generalmente menos costoso que Lambda@Edge, lo que lo convierte en una opción más rentable para operaciones simples.
Eso hace que CloudFront Functions sea una mejor opción para abordar nuestro problema con las rutas SPA.
CloudFront Functions puede asociarse con dos eventos diferentes.
-
Viewer request: La función se ejecuta cuando CloudFront recibe una solicitud de un cliente, antes inlcuso de comprobar si el objeto solicitado está en la caché de CloudFront.
-
Viewer response: La función se ejecuta antes de devolver el archivo solicitado al cliente. Tenga en cuenta que la función se ejecuta independientemente de si el archivo ya está en la caché de CloudFront.
Aquí hay una función sencilla que se puede asociar con el evento viewer request del behavior predeterminado de nuestra distribución CloudFront, donde se encuentra el origen S3. Esta función reescribe las rutas del path a /index.html
.
var level = 0; // subdirectory level where index.html is located.
var regexExpr = /^\/.+(\.\w+$)/; // Regex expression than matches paths requestiong an object. i.e: /route1/my-picture.png
function handler(event) {
var request = event.request;
var olduri = request.uri;
if (isRoute(olduri)) { // if is a route request. i.e: /route1
var defaultPath = '';
var parts = olduri
.replace(/^\//,'') // remove leading '/'
.replace(/\/$/,'') // remove triling '/' if any
.split('/'); // split uri into array of parts. i.e: ['route1', 'my-picture.png']
var nparts = parts.length;
// determine the limit as either level or nparts, whichever is lower
var limit = (level <= nparts) ? level : nparts;
// build the default path. i.e: /route1
for (var i = 0; i < limit; i++) {
defaultPath += '/' + parts[i];
}
var newuri = defaultPath + '/index.html';
request.uri = newuri;
console.log('Request for [' + olduri + '], rewritten to [' + newuri + ']');
}
return request;
}
// Returns true if uri is a route. i.e: /route1
// Returns false if uri is a file. i.e: /route1/index.html
function isRoute(uri) {
return !regexExpr.test(uri);
}
Cómo desplegarlo #
-
Inicie sesión en la consola de AWS y abra la consola de CloudFront en https://console.aws.amazon.com/cloudfront.
-
En el panel de navegación, elija Functions. Luego elija Create function. Introduzca un nombre de función y, a continuación, elija Continue. En Comentario, introduzca una descripción para la función como
SPA routing function
. -
Copie el código de la función.
-
Pegue el código en el editor de código en la consola, para que reemplace el código predeterminado en el editor.
-
Seleccione Save.
-
Publicar: Debe publicar la función antes de asociarla con su distribución CloudFront.
Elija la pestaña Publish*, luego elija el botón Publish para publicar la función.
-
Asociar con un comportamiento de distribución o caché: En la página *Functions, seleccione la pestaña Publish. Seleccione el botón Add association. Seleccione una distribución y/o un comportamiento de caché. No cambie el tipo de evento.
La tabla Associated distributions muestra la distribución asociada.
-
Esperar a la implementación: Espere unos minutos para que la distribución asociada termine de implementarse. Luego, para verificar el estado de la distribución, seleccione la distribución en la tabla Associated distributions y seleccione View distribution.
Cuando el estado de la distribución es Deployed, está listo para verificar que la función funciona.
Así que ahora, nuestra arquitectura tiene este aspecto.
Ahora que ya sabe cómo desplegar una Single Page Applications en la consola, para facilitar el proceso de prueba, aquí hay un enlace a un repositorio de GitLab con esta misma arquitectura, listo para desplegarse con AWS CDK.
El repositorio incluye el código de la función, una pequeña aplicación Angular para probar rutas, e incluso un API Gateway con su propio recurso de ruta /api/test
que devuelve un código HTTP 200
para demostrar que la función solo afecta al origen S3.
Para implementar esta aplicación CDK de ejemplo, asumiendo que tiene NPM y CDK instalados, simplemente clone el repositorio en su equipo y siga estos pasos.
-
Instale bibliotecas y dependencias con
npm i
. -
La primera vez que ejecute CDK, deberá iniciar su cuenta de AWS con
cdk bootstrap
. -
Si desea ver qué recursos se implementarán, puede ejecutar un
cdk diff
. -
Finalmente despliegue la aplicación cdk con
cdk deploy.
¡Juega con ello y diviértete!
Conclusión #
En conclusión, el problema de los códigos de error 403
con el enrutamiento en las SPAs en AWS se puede abordar eficazmente mediante el uso de CloudFront Functions. Esta solución permite la reescritura de las rutas a /index.html
, eliminando así los errores y garantizando el buen funcionamiento de las SPAs.