====== 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.