====== 10 - Serverless: Lambdas en local ====== El **monolito modular** con un único despliegue cubre el flujo principal de la aplicación, pero no toda la relación con el exterior. Los proveedores de nube ofrecen **FaaS** (*Function as a Service*): funciones pequeñas, sin servidor que administrar, activadas por eventos y facturadas por ejecución. En **AWS** el servicio se llama **Lambda**. Este tema estudia el modelo y, sin cuenta en la nube, cómo desarrollarlo en local con **LocalStack** en **docker**. ===== 1. El modelo serverless ===== En un **FaaS**, el proveedor provisiona, escala y ejecuta la función en respuesta a un evento. La función se escribe como un ''handler'' con una entrada y una salida; se despliega el código, no una aplicación permanente. ^ Característica ^ Función de nube ^ | Ciclo de vida | Se activa por eventos; no está siempre encendida | | Estado | **Nulo**: debe caber en la llamada; lo persistente queda en el almacén o en la cola que lo activó | | Escalado | Gestionado por el proveedor según la carga | | Configuración | Memoria y **timeout** fijados por función; secretos por variables de entorno | | Facturación | Por ejecución y recursos consumidos | Una función "fría" sufre **cold start**: la primera invocación paga la inicialización del runtime. En Java, las herramientas de snapshots la reducen bastante, pero el diseño prudente mantiene el arranque ligero (sin inicializar frameworks completos por invocación). ^ Situación ^ Diseño que encaja ^ | El flujo principal del negocio | El monolito modular y sus módulos | | Tareas aisladas, activadas por un evento, con picos irregulares | Una **función** | Comparada con una tarea dentro del monolito, una función aporta independencia de despliegue y de escalado, pero su ejecución vive en otro proceso: comunicarse exige contratos explícitos y gestión de errores, como con cualquier servicio aparte. ===== 2. AWS Lambda ===== Una **Lambda** es una función registrada en **AWS** con: * Un **runtime**: por ejemplo Java 17 o 21. * Un **handler**: la clase y el método de entrada. * **Activadores** (*triggers*): quién invoca la función — **API Gateway** (petición HTTP), **SQS** (mensaje en cola), **S3** (fichero subido) o un temporizador (**EventBridge**). El evento llega como datos (para los activadores HTTP, habitualmente JSON) y la respuesta sale como datos. La función no sirve páginas ni mantiene conexiones persistentes: procesa y devuelve. public class FraudCheckHandler implements RequestHandler { public FraudDecision handleRequest(ChargeEvent event, Context context) { context.getLogger().log("Revisando " + event.transactionId()); return fraudRules().evaluate(event.amount(), event.country()); } } El ''handler'' solo trata el ''ChargeEvent'' y devuelve la decisión. El SDK de AWS no entra en el handler salvo que la propia función necesite llamar a otros servicios (consultar una cola, leer un fichero). Los secretos y los límites de recursos se configuran en el entorno de la función (variables de entorno del proveedor), nunca dentro del código, igual que en el resto de la aplicación. ===== 3. LocalStack en docker ===== **LocalStack** es un contenedor que replica las **APIs de AWS** en local detrás de un único **endpoint** (puerto ''4566''), sin credenciales ni cuenta reales. La función se registra y se activa contra la API emulada igual que contra la real: services: aws: image: localstack/localstack ports: - "4566:4566" environment: - SERVICES=lambda,sqs Con el CLI configurado contra el endpoint local (''awslocal''), cada operación es la misma que se lanzaría contra AWS real. El entorno emulado también ejecuta las funciones Java: la función corre de verdad dentro del contenedor. Es desarrollo local, no un simulador de respuestas. ===== 4. Despliegue y prueba local ===== La función se empaqueta como un ''jar'' con sus dependencias y se registra en LocalStack: mvn package awslocal lambda create-function \ --function-name fraud-check \ --runtime java17 \ --handler com.example.FraudCheckHandler::handleRequest \ --zip-file fileb://target/fraud-check.jar \ --timeout 30 --memory-size 512 awslocal lambda invoke \ --function-name fraud-check \ --payload file://event.json result.json La invocación directa devuelve la salida del handler: la forma más simple de comprobar el comportamiento. Para activarla vía HTTP, una **function URL** (el equivalente mínimo de API Gateway) la expone: awslocal lambda create-function-url-config \ --function-name fraud-check --auth-type NONE curl -X POST \ -H "Content-Type: application/json" -d @charge.json Y para desacoplar el disparo, la función se vincula a una cola y se ejecuta con cada mensaje que llega: awslocal sqs create-queue --queue-name charge-events awslocal lambda create-event-source-mapping \ --function-name fraud-check \ --event-source-arn El monolito publica en la cola; el activador recoge el mensaje y ejecuta la función. Los pasos son los mismos que contra la nube real; solo cambian los endpoints. Eso permite ensayar el despliegue en local antes de repetirlo en producción (pregunta si hay que hacerlo luego). ===== 5. La función en la arquitectura del curso ===== La función es un componente **externo** al monolito: no importa sus modelos ni consulta su base de datos. Se comunica con la misma disciplina de **API First**: el formato del evento es un **contrato** estable, y cualquier dato del negocio que la función necesite se lo pide a la API pública por HTTP, como cualquier otro cliente. @startuml component "Monolito modular" as M queue "SQS / broker" as Q component "Lambda\n(handler)" as F M -> Q : evento externalizado Q -> F : activa la función F -> M : consulta la API pública\n(si necesita datos) @enduml ^ Colaboración ^ Mecanismo ^ | El flujo principal encarga trabajo asíncrono | Evento en **SQS** (o mensaje del broker del tema 08) | | La función necesita datos del negocio | Llamada HTTP a la API pública del monolito | | El monolito necesita el resultado | La función devuelve en la cola de respuesta o expone **function URL** | El ''handler'' debe mantenerse mínimo y sin lógica de negocio duplicada. Las reglas del negocio del monolito las aplica el monolito o su API pública; la función aporta solo su propio comportamiento. ===== 6. Pruebas ===== * **Handler**: pruebas unitarias sin dependencias: entrada, reglas propias y salida. Son las más baratas y la base. * **Adaptadores de infraestructura** (cola, fichero, etc.): probarlos contra **LocalStack** o **Testcontainers**, igual que cualquier pieza técnica. * **Integración local**: prueba de humo contra el compose: activar (HTTP o cola) y comprobar el resultado, sin despliegue real. class FraudCheckHandlerTest { @Test void rejectsChargesFromSanctionedCountry() { var event = new ChargeEvent("tx-1", new BigDecimal("30"), "KP"); var decision = new FraudCheckHandler().handleRequest(event, null); assertEquals(FraudDecision.rejected(), decision); } } El ''Context'' puede sustituirse por un doble simple: no aporta a la prueba del handler; la plataforma solo participa en las pruebas de integración. La lógica del handler es una clase ordinaria independiente de la plataforma. Las pruebas de unidad se centran en las reglas y no necesitan levantar LocalStack; el entorno emulado se reserva para dar identidad real: registro, activadores y respuestas de la plataforma.