DEV Community

Cover image for Como Logré Llevar Mi Aplicación Laravel al Mundo Serverless (Y como casi muero en el intento)
Hermann D. Schimpf
Hermann D. Schimpf

Posted on

Como Logré Llevar Mi Aplicación Laravel al Mundo Serverless (Y como casi muero en el intento)

En el primer capítulo de esta travesía, exploramos por qué migrar a AWS Serverless fue la mejor decisión para mi aplicación Laravel. Hablamos de escalabilidad automática, ahorro de costos y libertad del mantenimiento tradicional. Pero como en toda gran aventura, el camino tiene sus obstáculos.

Una de las preocupaciones que compartieron la mayoría de los lectores es que, dado que serverless hace toda la magia por ustedes y escala automáticamente, su billetera también tendría que hacerlo.

Y debo ser sincero con ustedes: caí en la trampa del serverless mágico. Mientras yo celebraba la ausencia de servidores, mi código se paseaba por la nube como un turista con tarjeta de crédito ilimitada: entusiasta, pero derrochando recursos a cada paso.

Pero hey! Para eso estoy hoy aquí—para revelarles lo que ningún tutorial me advirtió: el arte de escribir PHP consciente de Lambda.

Vamos a meternos de lleno en el cómo convertir nuestra aplicación Laravel en un verdadero guerrero serverless. Sí, atacaremos las optimizaciones necesarias y las precauciones que debemos tener.

Pero empecemos liviano con algunos conceptos nuevos.


1. La Magia Serverless

a) Lambda: El Chef de la Ejecución

AWS Lambda es, en pocas palabras, el chef que cocina el código justo cuando se le necesita. Su modelo basado en eventos significa que solo se activa ante solicitudes (HTTP, eventos de SQS, cron jobs, etc.) y, como en una cocina a la carta, solo pagan por el tiempo que se utiliza la estufa. Además, el nivel gratuito de Lambda les permite arrancar sin preocupaciones, siempre y cuando mantengan un ojo en los límites de uso.

Pero: ¿sabían que AWS Lambda, entre todos los lenguajes que soporta nativamente, PHP no es uno de ellos? 😤

Para nuestra tranquilidad, Lambda permite definir nuestros propios runtimes personalizados.

b) Bref: El Puente que une PHP y Lambda

Aunque me encantaría haber creado mi propia solución para ejecutar PHP en Lambda como sugirieron algunos (y gracias de antemano por confiar tanto en mis capacidades), a veces no es necesario reinventar la rueda.

Aquí es donde entra Bref, una herramienta esencial que nos permite ejecutar aplicaciones PHP en Lambda sin reescribir todo nuestro código.

¡Buenísimo! Pero, ¿cómo funciona?

Personalmente, me encanta conocer a fondo el funcionamiento interno de las cosas, pero para mantener la simplicidad, Bref opera de la siguiente manera:

API Gateway → Lambda → Bref → Laravel

Cuando una solicitud ingresa a tu aplicación, esta es capturada por API Gateway y enviada a ejecución en la función Lambda, donde el runtime de Bref inicia PHP-FPM en background y la redigire la solicitud vía FastCGI. Una vez obtenido el resultado, lo devuelve al API Gateway, para que a su vez sea enviado finalmente como respuesta al usuario final.

¿Simple, cierto?

c) Serverless Framework: El Pegamento que Une Todos los Componentes

¿Se imaginan la cantidad de configuraciones que necesitan realizar para poner en marcha una función Lambda, con un runtime personalizado, que ejecute Bref (y con éste, PHP-FPM) para que tu código Laravel pueda funcionar?

Pues ya puedes relajarte: el framework Serverless hace todo eso por ustedes. Solo tienen que definir los componentes necesarios en un fichero de configuración serverless.yml y, con un simple serverless deploy, ¡ya están en la nube! 🚀


2. Preparando Laravel para la Jungla Serverless

Primero deben entender que migrar a un entorno serverless implica que Laravel debe ponerse en "modo nómada". Esto significa adaptarlo para que sea stateless (sin estado).

¿Recuerdan que mencioné que Lambda es efímero? La forma más simple de imaginar Lambda es como conversar con alguien que tiene una memoria muy corta: cada vez que inician la conversación, tienen que volver a presentarte (por ej, los datos de sesión). ¿Le dan un cuaderno para que les anote algo y luego se lo devuelva (upload de archivos)? Olvídenlo, ni en el limbo de Inception lo encontrarán.

Por lo tanto, es necesario realizar algunos ajustes esenciales:

  • Logs a stderr: No tienen acceso SSH a Lambda, por lo que deben hacer que los logs se almacenen en CloudWatch.
  • Almacenamiento de solo lectura: El sistema de archivos en Lambda es de solo lectura, a excepción del directorio /tmp. Por lo que la subida de archivos debe ser almacenada en S3 (o similar).
  • Sesiones: Deben mover los datos de sesión a cookies o, mejor aún, usar DynamoDB para una solución más robusta.
  • Caché: Por simplicidad, optamos por DynamoDB.
  • Variables de entorno: Olvídense del fichero .env en producción; deberán utilizar variables definidas en el fichero de configuración serverless.yml.

El Kit de Supervivencia de Bref

Para facilitar estos ajustes, la comunidad de Bref ha creado un paquete llamado bref/laravel-bridge que automatiza la mayoría de los cambios mencionados anteriormente.

Manos a la Obra: Transformando Laravel en un Nómada Cloud

Es hora de ensuciarnos las manos.

Si quieren ir siguiendo el paso a paso, los requisitos a partir de este punto son los siguientes:

  • PHP (¡obviamente!)
  • Composer 1
  • Node.js 2
  • serverless 3

Empecemos creando un proyecto de Laravel nuevo 4:

# Instalación de Laravel
laravel new laravel-above-the-clouds
cd laravel-above-the-clouds

# Creamos y migramos la base de datos (sqlite por defecto)
php artisan migrate

# Instalamos los paquetes de Node.js (Vite.js, Tailwind CSS, etc.) y compilamos los assets
npm install && npm run build
Enter fullscreen mode Exit fullscreen mode

Instalamos los paquetes bref/bref y bref/laravel-bridge 5:

composer require bref/bref bref/laravel-bridge --update-with-dependencies
Enter fullscreen mode Exit fullscreen mode

El paquete bref/laravel-bridge, como mencioné anteriormente, resuelve los ajustes necesarios para que Laravel funcione en un ambiente serverless.

Bref ya incluye un fichero de configuración serverless.yml preconfigurado que podemos utilizar como base para nuestro primer deploy:

php artisan vendor:publish --tag=serverless-config
Enter fullscreen mode Exit fullscreen mode

Piensen en el fichero serverless.yml como las instrucciones de un set de LEGO para AWS: es la guía que le permite montar su aplicación de forma impecable.

Realicen el deploy del proyecto:

serverless deploy
Enter fullscreen mode Exit fullscreen mode

serverless deploy output

Al finalizar, en la consola obtendrán la URL del API Gateway para acceder a su aplicación en AWS!


3. Pero, ¿qué ha pasado con mi CSS?

first request, 404 on assets

Lo primero que notarán es que los assets (CSS y JS) no cargan correctamente. Esto sucede porque, en un entorno serverless, no disponemos de Apache o Nginx para servir los assets estáticos; y aquí quiero hacer una breve pausa.

Llevar nuestra aplicación a un entorno serverless implica cambiar nuestro mind-set de que un único servidor es responsable de realizar todas las tareas para hacer funcionar nuestra aplicación. En un entorno serverless, cada componente debe ser gestionado por un servicio especializado.

En nuestro caso, algunos de los componentes principales en una aplicación Laravel son los siguientes:

Componente Servidor Dedicado AWS Serverless
Rutas, Controladores, ... PHP-FPM Lambda
Assets estáticos (imágenes, javascript, css) Apache / Nginx S3
Datos de sesión Base de Datos / Cookies RDS / DynamoDB
Caché Disco local DynamoDB / Redis
Upload de archivos Disco local S3
Tareas programadas (Schedule) crontab EventBrigde
Cola de Eventos SSH $ php artisan queue:work / quizas supervisor SQS
Comandos artisan SSH $ php artisan ... Lambda
Log de Aplicación SSH $ tail -f storage/logs/* CloudWatch

Pero vayamos con calma, resolviendo un punto a la vez.

De la Complejidad a la Simplicidad: La Orquestación Serverless

De los puntos mencionados, el paquete bref/laravel-bridge ya resuelve el log de la aplicación y los datos de sesión. Se crea un grupo de logs en CloudWatch para los logs de aplicación, y los datos de sesión son almacenados en cookies (más adelante veremos cómo llevar los datos de sesión a DynamoDB).

Para poder servir los assets estáticos, tenemos dos alternativas:

  1. La difícil: Agregar la creación de un Bucket S3 a nuestra configuración serverless.yml, obtener el nombre del bucket, armar la URL pública y asignarla en la variable de entorno ASSET_URL.

  2. La simple: Utilizar el plugin serverless-lift 6, y dejar que la magia fluya.

Aunque aprendí mucho por el camino difícil (vengo acompañando el crecimiento de Bref desde 2020 7), ustedes pueden aprovechar que la comunidad ha desarrollado herramientas que nos simplifican la vida.

Instalamos el plugin serverless-lift y agregamos una nueva sección 8 dentro del fichero serverless.yml:

serverless plugin install -n serverless-lift
Enter fullscreen mode Exit fullscreen mode
service: laravel

- # ...

functions:
  web:
    handler: public/index.php
    runtime: php-82-fpm

- # ...

  artisan:
    handler: artisan
    runtime: php-82-console

- # ...

+ constructs:
+   website:
+     type: server-side-website
+     assets:
+       '/build/*': public/build
+       # Agreguen aquí cualquier archivo o directorio que necesite ser servido desde S3

plugins:
  - ./vendor/bref/bref
+   - serverless-lift
Enter fullscreen mode Exit fullscreen mode

Dentro de la sección constructs hemos definido un componente website del tipo server-side-website. Esto le dice al plugin serverless-lift que cree una distribución de CloudFront, la cual actuará como CDN para servir los archivos estáticos desde un Bucket S3, y a su vez como proxy reverso, redirigiendo las solicitudes a las rutas de tu aplicación PHP a través de API Gateway => Lambda.

Serverless with CloudFront

serverless deploy
Enter fullscreen mode Exit fullscreen mode

Este deploy demorará unos 5-7 minutos adicionales por única vez, ya que las distribuciones de CloudFront son creadas globalmente. Los siguientes deploys serán más rápidos, siempre que no modifiquen configuraciones que afecten a la distribución de CloudFront.

En el resultado de la consola verán que ahora disponen de dos URLs, una del API Gateway (que ya no utilizaremos), y otra de CloudFront, el cual servirá los assets estáticos desde el Bucket S3, y el resto de las rutas serán procesadas por Lambda.

serverless deploy output with CloudFront

¡Y ahora el CSS se carga correctamente! Felicidades, han desplegado su primera aplicación Laravel en ambiente serverless.

Laravel on AWS Serverless


4. Optimizando para el Mundo Real: La Batalla Contra el Cold-Start

Seguramente habrán notado que la primera vez que ingresaron a su aplicación, esta demoró unos segundos adicionales en responder.

Así como mencioné en el artículo anterior, esto es llamado cold-start y ocurre cuando Lambda inicializa una nueva instancia para ejecutar su aplicación. Esta inicialización puede llevar 250ms o más, especialmente si tu aplicación es grande.

Lambda mantiene viva una instancia hasta 10 minutos después de procesar una solicitud, luego es destruida automáticamente.

Podemos abordar esto desde varios frentes.

a) Mantener siempre viva una instancia Lambda 9

En aplicaciones con poco tráfico, es normal que hayan periodos mayores a 10 minutos de inactividad, por lo que podremos tener un porcentaje mayor de solicitudes procesadas en un cold-start.

Bref dispone de un evento especial que podemos utilizar para mantener viva una instancia de Lambda. Simplemente debemos agregar un evento schedule en nuestro fichero de configuración serverless.yml con el payload {warmer: true}. Bref reconocerá este evento especial y responderá instantáneamente con el código de estado 100 10 sin ejecutar tu código, manteniendo así viva la instancia.

service: laravel

- # ...

functions:
  web:
    handler: public/index.php
    runtime: php-82-fpm
    events:
      - httpApi: '*'
+       - schedule:
+         rate: rate(5 minutes)
+         input:
+           warmer: true
Enter fullscreen mode Exit fullscreen mode

b) Reducir el tamaño de su aplicación

Durante un cold-start, AWS Lambda descarga el paquete de la aplicación, lo descomprime en un entorno temporal y carga el runtime junto con todas las dependencias y configuraciones necesarias. Este proceso, aunque está optimizado, añade latencia a la primera solicitud. Por ello, reducir el tamaño del paquete (por ejemplo, eliminando dependencias de desarrollo o módulos innecesarios) puede acortar considerablemente este tiempo de inicialización.

El fichero serverless.yml por defecto de Bref ya ignora ciertos directorios que no son necesarios en PHP, como por ejemplo node_modules.

Paquetes de desarrollo

Antes de realizar el deploy, podemos desinstalar los paquetes de desarrollo. Esto reducirá en gran manera el tamaño del directorio vendor, y en consecuencia, el tamaño total del código enviado a Lambda.

composer install --no-dev
Enter fullscreen mode Exit fullscreen mode

Dependencias de servicios AWS

En la mayoría de mis aplicaciones, el paquete de composer que más ocupa espacio es aws/aws-sdk-php (requerido por bref/laravel-bridge).

Lo bueno es que la comunidad ha desarrollado un script de composer para limpiar los paquetes de servicios AWS no utilizados en nuestra aplicación 11. Solo debemos especificar en el fichero composer.json los servicios que sí utilizamos:

{
  "name": "laravel/laravel",
  "type": "project",
  ...
  "require": {
    "php": "^8.2",
    "bref/bref": "^2.3",
    "bref/laravel-bridge": "^2.5",
    "laravel/framework": "^11.31",
    "laravel/octane": "^2.8",
    ...
  },
  "scripts": {
+     "pre-autoload-dump": "Aws\\Script\\Composer\\Composer::removeUnusedServices",
    "post-autoload-dump": [
      "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
      "@php artisan package:discover --ansi"
    ],
    ...
  },
  "extra": {
    "laravel": {
      "dont-discover": []
    },
+     "aws/aws-sdk-php": [
+       "DynamoDb",
+       "S3",
+       "Sqs"
+     ]
  },
}
Enter fullscreen mode Exit fullscreen mode

El script de limpieza correrá cada vez que ejecutemos composer install o composer update.

c) Impulsando el Rendimiento con Laravel Octane

Incluso cuando se producen cold-starts, Laravel Octane entra en acción para mantener la aplicación en memoria y reducir significativamente los tiempos de respuesta. Al evitar un arranque completo de Laravel en cada solicitud, se mejora notablemente la experiencia del usuario final.

Laravel Octane actúa como un acelerador que "mantiene caliente" la aplicación. Una vez que Lambda inicia la aplicación, Octane se encarga de conservarla en memoria, de modo que las siguientes solicitudes se atiendan casi de inmediato. Es como encender el motor de un coche y dejarlo listo para arrancar en cualquier momento, en lugar de encenderlo desde cero cada vez.

Pasos para implementar Laravel Octane:

composer require laravel/octane
php artisan octane:install
Enter fullscreen mode Exit fullscreen mode

Una vez instalado, actualicen el fichero serverless.yml para utilizar Laravel Octane como handler 12. Esto asegurará que tu aplicación se ejecute con el rendimiento mejorado que ofrece Laravel Octane:

service: laravel

- # ...

functions:
  web:
    handler: Bref\LaravelBridge\Http\OctaneHandler
    runtime: php-82
- # ...
Enter fullscreen mode Exit fullscreen mode

Finalmente, desplieguen su aplicación con los cambios realizados:

serverless deploy
Enter fullscreen mode Exit fullscreen mode

d) Métricas que Validan el Progreso

Aplicando todas las mejoras mencionadas, hemos reducido el tiempo de inicialización (cold-start) de nuestra aplicación. Y con Laravel Octane, no solo hemos mejorado el tiempo del cold-start, sino que mejoramos el tiempo de respuesta general de nuestra aplicación.

Podemos visualizar los tiempos de respuesta en los logs de CloudWatch que ya se configuraron automáticamente por nosotros.

Cold-start sin optimizaciones (499ms):

php-fpm coldstart

Cold-start con Laravel Octane y optimizaciones (190ms):

Laravel Octane coldstart

Tengan en cuenta que al utilizar Laravel Octane, se mantiene la ejecución de Laravel en memoria, por lo que deben cuidarse de los memory leaks 13.


5. Caídas y Aprendizajes: Los Errores que Todo Migrante Serverless Debe Conocer

Seguramente se dieron cuenta que hay una función llamada artisan en el fichero serverless.yml. Esa función utiliza la versión "consola" del runtime de Bref 14, la cual nos permite ejecutar comandos de Laravel Artisan en AWS Lambda.

Como no tenemos acceso SSH a las instancias de Lambda, Bref nos disponibiliza un puente para poder ejecutar comandos cli mediante el comando bref:cli de serverless.

serverless bref:cli --args="<comando artisan y sus opciones>"
serverless bref:cli --args="route:list"
Enter fullscreen mode Exit fullscreen mode

Optimizaciones

Un punto muy importante que quiero reforzar es que debemos cambiar nuestra mentalidad al desarrollar aplicaciones para entornos serverless. En este nuevo paradigma, es crucial diseñar nuestros procesos y flujos de trabajo considerando las características únicas de la computación serverless, como la naturaleza efímera de las funciones y el modelo de facturación basado en el uso.

Serverless no es caro... pero el código mal adaptado sí lo es.

En mi primer mes migrando una de mis aplicaciones al entorno serverless, cometí el error de no revisar detalladamente ciertos procesos. Uno de ellos se encargaba de sincronizar los productos de la tienda en línea con un sistema de facturación externo, incluyendo la actualización de imágenes de productos. Y como mencioné en el artículo anterior, algunos procesos no son ideales para ejecutarse en un entorno serverless. En este caso, estaba pagando por tiempo muerto, durante la descarga de imágenes, mi código en Lambda no estaba realizando nada más que esperar 5-8 segundos por imágen. Con 65,000 artículos en la tienda y dos sincronizaciones diarias, esto resultó en una factura de ~$530 solo por el tiempo inactivo durante la descarga de imágenes.

Este incidente me llevó a rediseñar el proceso de sincronización. En un primer intento de optimización, implementé la descarga de múltiples imágenes en paralelo utilizando PHP ZTS, lo que me llevó a desarrollar la librería hds-solutions/parallel-sdk 15. Esta versión redujo los costos a ~160$, pero aún no era suficiente; mi objetivo era que los costos estuvieran por debajo de lo que gastaba originalmente en EC2.

En un segundo intento, trasladé el proceso de descarga de imágenes a una instancia EC2 dedicada. Aunque esto redujo significativamente los costos a ~50$, la aplicación quedó dividida en dos componentes separados.

Finalmente, en la tercera versión del proceso de sincronización, tras conversar con el cliente, implementé un pequeño script en el servidor del sistema de facturación. En lugar de que mi aplicación descargara las imágenes, este script enviaba las imágenes directamente desde el servidor del sistema de facturación a S3 utilizando URLs pre-firmadas 16. Esto eliminó por completo los costos asociados a la descarga de imágenes, quedando únicamente el costo de almacenamiento en S3.


6. ¿Qué Sigue?

¡Con esto, cerramos otro capítulo en nuestro viaje hacia el mundo serverless! Hoy vimos cómo transformar a Laravel en un auténtico nómada en la nube, adaptándolo para vivir sin el peso de un servidor tradicional. Pero esto es solo el comienzo.

En el próximo artículo nos adentraremos en el corazón de la integración: veremos cómo trasladar los datos de sesión y caché a DynamoDB, cómo aprovechar S3 para el manejo de archivos, configurar SQS para gestionar colas y utilizar EventBridge para orquestar tareas programadas. ¡Nos vemos en la siguiente entrega!


Mientras tanto, ¿han migrado su aplicación Laravel a serverless? ¡Compartan sus experiencias en los comentarios! Y para los que aún no lo han hecho, les animo a experimentar con lo que hemos implementado hasta ahora y a compartir sus dudas y experiencias.


  1. https://getcomposer.org/download 

  2. https://github.com/nvm-sh/nvm#installation-and-update 

  3. https://www.serverless.com/framework/docs/getting-started 

  4. https://laravel.com/docs/installation 

  5. https://bref.sh/docs/laravel/getting-started 

  6. https://bref.sh/docs/use-cases/websites 

  7. https://github.com/brefphp/bref/releases/tag/0.5.20 

  8. https://github.com/getlift/lift/blob/master/docs/server-side-website.md 

  9. https://bref.sh/docs/use-cases/http/advanced-use-cases#cold-starts 

  10. https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100 

  11. https://github.com/aws/aws-sdk-php/tree/master/src/Script/Composer 

  12. https://bref.sh/docs/laravel/octane 

  13. https://laravel.com/docs/11.x/octane#managing-memory-leaks 

  14. https://bref.sh/docs/runtimes/console 

  15. https://github.com/hschimpf/parallel-sdk 

  16. https://docs.aws.amazon.com/es_es/AmazonS3/latest/userguide/PresignedUrlUploadObject.html 

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay