English version here.
Spoiler: El disco de Lambda existe. Es efímero, de solo lectura y se autodestruye cuando AWS recicla el entorno — y no avisa cuándo.
En el capítulo anterior convertimos a Laravel en un nómada digital: desplegamos en Lambda, servimos los assets con CloudFront y ganamos la batalla del cold-start con Octane. Pero quedó una promesa pendiente: ¿dónde van a parar los datos cuando el disco es, literalmente, una ilusión temporal?
Hablemos claro: el disco en Lambda existe. Hay un directorio /tmp escribible donde pueden tirar archivos temporales 1. Pero tratarlo como almacenamiento real es como guardar tus ahorros en un taxi compartido: técnicamente están ahí, pero buena suerte encontrándolos mañana. El resto del filesystem es de solo lectura, y cada instancia de Lambda puede desaparecer en cualquier momento llevándose /tmp con ella.
En este capítulo atacamos la persistencia en tres actos: primero los archivos (S3, la parte corta), después nuestra entrada al mundo NoSQL con caché y sesiones (DynamoDB, modo fácil), y finalmente el plato fuerte: DynamoDB como base de datos principal.
1. El Mapa del Tesoro Serverless
Antes de meternos en el código, ubiquemos dónde estamos parados. Esta es la arquitectura simplificada de mi aplicación:
En los capítulos previos ya hablamos sobre cómo utilizamos Lambda para ejecutar nuestra App Laravel, y cómo CloudFront reparte el tráfico entre S3 (assets) y Lambda (la app). Hoy toca:
- Enviar las subidas a S3.
- Almacenar los datos de sesión y caché en DynamoDB.
- y utilizar DynamoDB como nuestra base de datos principal.
Los demás servicios quedan para los próximos capítulos. Paciencia, mis jóvenes padawans.
2. Uploads a S3: La Parte Corta
Este punto es tan simple que casi da vergüenza dedicarle una sección. Casi.
a) El mismo facade de siempre, otro destino
Laravel ya abstrae el almacenamiento con el facade Storage. En un servidor tradicional apunta al disco local; en serverless, simplemente definimos el bucket 2 y cambiamos el driver a s3 3 4:
composer require league/flysystem-aws-s3-v3
provider:
environment:
FILESYSTEM_DISK: s3
AWS_BUCKET: ${construct:storage.bucketName}
constructs:
storage:
type: storage
allowAcl: true
# CORS es requerido para subir ficheros directamente desde el navegador utilizando URLs pre-firmadas
cors: ${construct:website.url}
Y listo. Toda llamada a Storage::put(), Storage::get() ahora habla con S3 sin tocar una línea de lógica de negocio. Cero refactor, pura abstracción, punto para Laravel.
b) URLs pre-firmadas: para que los uploads ni siquiera pasen por Lambda
Pero acá viene el truco fino. Si el usuario sube un archivo a través de su aplicación, están pagando tiempo de Lambda por bytes que solo están de paso. ¿Recuerdan la factura de $530 por tiempo inactivo del artículo anterior? Exacto.
La alternativa elegante: URLs pre-firmadas 5. Laravel genera una URL temporal de S3, el frontend la usa para subir el archivo directamente a S3, y Lambda ni se entera:
use Illuminate\Support\Facades\Storage;
// Laravel genera la URL, el navegador sube directo a S3
['url' => $url, 'headers' => $headers] = Storage::temporaryUploadUrl(
"uploads/{$filename}", now()->addMinutes(5)
);
El archivo viaja directo desde el navegador a S3 sin escalas. Generar la URL es una firma local, costo cero. Lambda sigue durmiendo, y tu billetera también.
Lambda (y API Gateway) limitan el payload a un máximo de 6 MB y 10 MB respectivamente 6. Pasado ese tamaño, las URLs pre-firmadas dejan de ser una optimización y se vuelven obligatorias 7.
Pero, si Lambda sigue durmiendo... ¿Cómo se entera de que se subió un archivo?
Ahí empieza lo divertido: S3 puede disparar eventos automáticamente 8 cuando se sube un archivo. Ese evento puede invocar una Lambda, transmitir un mensaje por SNS, emitir un evento a SQS o EventBridge. Pero esa caja de Pandora la abriremos en otro capítulo.
3. DynamoDB, Modo Fácil: Caché y Sesiones
Antes de usar DynamoDB como base de datos (el boss final de este artículo), empecemos por donde duele menos: el caché y las sesiones.
a) Caché: la tabla que se limpia sola
La configuración es insultantemente simple 9. Solo debemos cambiar el store de caché a dynamodb, y especificar el nombre de la tabla DynamoDB:
provider:
environment:
CACHE_STORE: dynamodb
DYNAMODB_CACHE_TABLE: !Ref CacheTable
Y agregar la definición de tabla en serverless.yml (dentro de resources, como CloudFormation) 10:
CacheTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: my-app-cache
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
KeySchema:
- AttributeName: id
KeyType: HASH
El detalle hermoso: TimeToLiveSpecification. DynamoDB borra automáticamente los items expirados según el atributo ttl 11. Sin cron jobs de limpieza, sin comandos de limpieza cache:*, sin nada. La basura se saca sola. El Roomba de DynamoDB hace el aseo mientras duermen.
Finalmente, agregamos los atributos id y ttl a la configuración de dynamodb en config/cache.php 12.
'dynamodb' => [
'driver' => 'dynamodb',
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
// ...
'attributes' => [
'key' => 'id',
'expiration' => 'ttl',
],
],
b) Un nuevo hogar para las sesiones
En el artículo anterior dejamos las sesiones en cookies como solución rápida. Funciona sí, pero tiene sus límites: las cookies viajan en cada request y tienen tope de ~4KB. Para algo más robusto, Laravel trae soporte nativo para DynamoDB 13:
provider:
environment:
SESSION_DRIVER: dynamodb
Internamente, Laravel reutiliza la misma tabla de caché 13 (con el mismo TTL automático 11). Las sesiones expiran, DynamoDB las barre, y ustedes siguen programando features en vez de mantener infraestructura.
Con caché y sesiones ya migrados, solo queda un componente stateful en pie: la base de datos.
Preparen café, que aquí se pone interesante.
4. DynamoDB como Base de Datos: El Boss Final
Acá es donde la mayoría de los devs Laravel se ponen nerviosos. Durante toda nuestra carrera fuimos criados a base de CREATE TABLE, migraciones y JOINs. DynamoDB mira todo eso y dice: "qué lindo, pero acá no hacemos nada de eso".
a) Básicos de DynamoDB para devs Laravel
Traduzcamos los conceptos a nuestro idioma:
- Tabla: igual que siempre, pero no hay migraciones, no hay columnas definidas. Cada item puede tener los atributos que quieran. Sí, lo que quieran 14. Respiren.
- Item: el equivalente a una fila. Básicamente un JSON con identidad propia.
-
PK (Partition Key): define en qué partición física vive el item. Es el
WHEREobligatorio de toda query. - SK (Sort Key): ordena los items dentro de una misma partición. PK + SK forman la clave primaria compuesta.
- GSI (Global Secondary Index) 15: una forma alternativa de organizar los items para consultarlos por otros atributos. Lo más parecido a un índice que van a encontrar.
La regla de oro que me costó interiorizar: en DynamoDB no modelas datos, modelas los patrones de acceso. Primero piensan qué queries van a hacer, después diseñan las claves. A diferencia de SQL, donde modelan normalizado y después se pelean con los índices.
b) Eloquent sobre DynamoDB: kitar/laravel-dynamodb
Para simplificar la implementación de Eloquent con DynamoDB utilicé el paquete kitar/laravel-dynamodb, que provee un driver de conexión y un query builder que traduce las llamadas de Eloquent a operaciones de DynamoDB.
composer require kitar/laravel-dynamodb
La conexión en config/database.php se ve así:
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'token' => env('AWS_SESSION_TOKEN'),
'endpoint' => env('DYNAMODB_ENDPOINT'), // útil para local/testing
'prefix' => 'my-app-', // prefijo opcional para nombres de tabla
],
provider:
environment:
DB_CONNECTION: dynamodb
Y los modelos... siguen siendo modelos Eloquent, con un par de atributos extra para las claves de la tabla 16:
use Kitar\Dynamodb\Model\Model;
class User extends Model
{
protected $table = 'users';
protected $primaryKey = 'email'; // partition key
protected $sortKey = 'type'; // sort key
protected $sortKeyDefault = 'profile';
protected $fillable = ['name', 'email', 'password', 'type'];
}
El sortKeyDefault cubre el caso más común: User::find('foo@bar.com') usa 'profile' como sort key automáticamente. Y si necesitan consultar por un índice secundario (por ejemplo, "todos los usuarios" vía un GSI):
User::index('GSI1')
->keyCondition('GSI1PK', '=', 'USER#')
->query();
El valor de
GSI1PKlo setean ustedes al guardar, es un atributo más del item.
¿Ven esas claves raras? Valores tipo USER# metidos dentro de un índice GSI1... eso tiene nombre propio...
c) Single-Table Design: el concepto (y nada más)
Esas claves raras son la puerta de entrada al Single-Table Design: en lugar de una tabla por entidad (users, posts, comments...), varias entidades comparten una sola tabla diferenciándose por sus claves. La PK puede identificar el "dueño" del dato (USER#123), la SK qué cosa es (PROFILE, POST#456), y los GSIs te dan vistas alternativas para otros patrones de acceso.
¿Suena a magia negra? Lo es. Diseñar bien una tabla "single-table" es un arte completo: requiere mapear todos tus patrones de acceso antes de escribir una línea de código, algo que en SQL pueden improvisar sobre la marcha.
No voy a profundizar acá porque el tema merece su propio artículo (y lo tendrá). Hoy solo necesitan saber que existe, y que es la razón por la que DynamoDB puede reemplazar a una base relacional completa.
d) Cuándo DynamoDB le gana a SQL, y cuándo no
Seamos honestos, sin vender humo. DynamoDB es brutal cuando:
- Conocen los patrones de acceso de antemano (get by id, list by owner, filter by status).
- Necesitan latencia predecible: lecturas de un dígito de milisegundos, a cualquier escala 17.
-
El tráfico es impredecible: picos gigantes o nada de tráfico.
PAY_PER_REQUEST: pagas por request, así que con tráfico casi nulo la factura casi nula; con tráfico sostenido y alto, evalúa el modo provisionado 18. -
Quieren escalar sin dramas: no hay nodo "master" que muera a las 4 AM, no hay réplicas de solo lectura que configurar para bypassear
max_connections. La escalabilidad deja de ser tu problema, ahora es problema de Jeff Bezos. -
DynamoDB Streams 19: cada escritura puede disparar eventos. El equivalente al
binlogde MySQL, pero administrado y capaz de ejecutar código. -
Gestión cero: sin parches, sin
VACUUM, sin mantenimiento programado.
Y no es para ustedes si:
- Necesitan JOINs y queries ad-hoc: "mostrame todos los usuarios que compraron X en el último mes agrupado por provincia" → háganlo en SQL o exporten a un data warehouse.
- Sus modelos de datos cambian cada semana: rediseñar claves en DynamoDB duele más que una migración.
- Dependen de reporting/analytics: dashboards, gráficos agregados, queries exploratorias → necesitan SQL. DynamoDB no es el lugar.
- Necesitan almacenar ítems gigantes: DynamoDB tiene un máximo de 400KB por item 20. Tus documentos van en S3, y en DynamoDB solo la referencia.
-
Scancomo estrategia 21: leen toda la tabla y pagan todo. Si tu query se resuelve con scan, el diseño falló.
Patrones de acceso tipo «posts por usuario», «comentarios por post» o lecturas directas por ID encajan perfecto en el primer grupo. Casos de uso hechos a medida para DynamoDB.
e) La infraestructura también es código
Las tablas viven declaradas como recursos CloudFormation 22. La tabla del modelo User del ejemplo se vería así:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: my-app-users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- { AttributeName: email, AttributeType: S }
- { AttributeName: type, AttributeType: S }
- { AttributeName: GSI1PK, AttributeType: S }
- { AttributeName: GSI1SK, AttributeType: S }
KeySchema:
- { AttributeName: email, KeyType: HASH }
- { AttributeName: type, KeyType: RANGE }
GlobalSecondaryIndexes:
- IndexName: GSI1
KeySchema:
- { AttributeName: GSI1PK, KeyType: HASH }
- { AttributeName: GSI1SK, KeyType: RANGE }
Projection: { ProjectionType: ALL }
Y los permisos IAM mínimos en el serverless.yml principal 23:
provider:
iam:
role:
statements:
- Effect: Allow
Resource:
- !GetAtt CacheTable.Arn
- !GetAtt UsersTable.Arn
- !Sub
- '${Resource}/index/*'
- { Resource: !GetAtt UsersTable.Arn }
Action:
- dynamodb:Query
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Sugerencia: Nunca debemos utilizar
Action: dynamodb:*oResource: *. Privilegio mínimo o nada 24.
f) Costos: la última factura fija se va
Cierre de los costos de esta serie:
-
De la configuración anterior: Nos quedó un RDS MySQL en ejecución, un
db.t4g.smallgenera ~$26/mes (≈ $0.032/h × 730 h + almacenamiento 25) por una base de datos que cobra solo por existir. -
Con DynamoDB: Podemos utilizar el modo
BillingMode: PAY_PER_REQUESTy así solo pagar por request procesado y GB almacenado. Con tráfico modesto, la factura del mes se mide en centavos: la base de datos que cobraba por respirar dejó de existir, y ya ningún componente cobra tarifa plana por recursos que nadie usa.
5. ¿Qué Sigue?
Hoy resolvimos dónde persiste todo: archivos en S3 (con uploads que ni tocan Lambda), caché y sesiones en DynamoDB con expiración automática, y una base de datos NoSQL que habla Eloquent.
Pero el mapa todavía tiene zonas inexploradas. En el próximo artículo: las colas. Veremos cómo SQS + un worker Lambda reemplazan al queue:work que corría eternamente en un supervisor, y por qué eso es mejor que rezarle a supervisord a las 3 AM.
Una pregunta para ustedes: ¿ya usaron DynamoDB con Laravel, o siguen en el equipo MySQL/PostgreSQL? Cuéntenme en los comentarios qué los frena (o qué los enamoró) del mundo NoSQL.
-
https://github.com/getlift/lift/blob/master/docs/storage.md ↩
-
https://laravel.com/framework/docs/13.x/filesystem#s3-driver-configuration ↩
-
https://laravel.com/framework/docs/13.x/filesystem#temporary-upload-urls ↩
-
https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution ↩
-
https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html ↩
-
https://bref.sh/docs/environment/storage#deploying-dynamodb-tables ↩
-
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html ↩
-
https://laravel.com/framework/docs/13.x/session#configuration ↩
-
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html ↩
-
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html ↩
-
https://github.com/kitar/laravel-dynamodb#extending-the-base-model ↩
-
https://aws.amazon.com/blogs/database/understanding-amazon-dynamodb-latency/ ↩
-
https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/capacity.html ↩
-
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html ↩
-
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-use-s3-too.html ↩
-
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html ↩
-
https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-dynamodb-table.html ↩
-
https://www.serverless.com/framework/docs/providers/aws/guide/iam ↩
-
https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege ↩



Top comments (0)