Cómo crear aplicaciones serverless con CDK y SAM
CDK y SAM pueden usarse juntos para crear aplicaciones serverless. Usarás CDK para definir y crear tus recursos AWS, y luego usarás SAM para probar tus recursos serverless.
AWS SAM
Guía completa de AWS Serverless Application Model (SAM) para construir, probar y desplegar aplicaciones serverless.
3 articles
1. Introducción
Una aplicación serverless es más que solo una función Lambda. Es una combinación de funciones Lambda, fuentes de eventos, APIs, bases de datos y otros recursos que trabajan juntos para realizar tareas.
Crear recursos serverless a través de la consola de AWS es rápido y fácil, pero es aconsejable usar este enfoque solo para propósitos de prueba durante la fase de aprendizaje. Después de eso, y pensando en cómo usar los recursos en un proyecto real, necesitarás:
- IaC: para crear tus recursos de una manera que te permita recrearlos fácilmente
- Control de versiones: para rastrear todas las modificaciones del código
- CI/CD: para automatizar el proceso de lanzamiento
¿Alguien usa CloudFormation o Terraform para gestionar sus recursos serverless? Posiblemente, pero vamos, ¡hay una mejor manera!
Para gestionar tus recursos serverless, la forma natural de hacerlo es usando las siguientes tecnologías IaC:
SAM (Serverless Application Model) / Serverless Framework: opción declarativa con plantillas. Es un framework específico para aplicaciones Serverless.CDK / Pulumi: añaden un nivel de abstracción y te permiten definir la infraestructura con lenguajes de programación modernos.
En este artículo, revisaremos el enfoque de combinar ambos CDK + SAM.
Por cierto, CDK + SAM es mi enfoque preferido: ¡obtienes lo mejor de las 2 opciones! ¿Qué piensas? ¡Puedes compartir tu opinión en los comentarios!
2. CDK vs SAM
En los artículos enlazados a continuación, encontrarás información sobre CDK y SAM.
- Conceptos básicos de CDK: Cómo crear infraestructura con CDK
- Conceptos básicos de SAM: Cómo crear aplicaciones serverless con SAM
2.1. ¿Qué tienen en común CDK y SAM?
Ambos…
- Son frameworks de desarrollo de software de código abierto con licencia Apache
- Proporcionan Infraestructura como Código (IaC)
- Usan AWS CloudFormation detrás de escena para desplegar recursos
- Proporcionan un CLI para construir y desplegar aplicaciones
- Están bien integrados con los pipelines de compilación de AWS
- Soportan reutilización de componentes
2.2. ¿Cuáles son las principales diferencias entre CDK y SAM?
| CDK | SAM | |
|---|---|---|
| Para declarar recursos | Usa lenguajes de programación familiares | Usa JSON o YAML |
| Referencias dinámicas | Capacidades nativas del lenguaje | Pseudo parámetros y funciones lógicas |
| Pruebas | No soportado nativamente (podrías usar SAM) | Soportado (también depuración) |
| Recursos IaC | Todos | Enfocado en serverless pero puedes usar CloudFormation para crear cualquier recurso AWS |
| Complejidad | Muy baja | Media, basada en CloudFormation |
| Mantenibilidad | Mayor | Media |
2.3. ¿Por qué CDK vs SAM? Usemos CDK + SAM
Desde el 6 de enero de 2022, AWS anunció la disponibilidad general del soporte del CLI de AWS SAM para pruebas locales de aplicaciones AWS CDK. Significa que ¡puedes usar SAM sobre tu proyecto CDK para probar tus recursos!.
- Ahora, han pasado más de 2 años, ¡así que esta integración está madura!
3. Práctica: CDK + SAM
Usaremos un nuevo proyecto CDK para mostrar cómo funciona la combinación de CDK + SAM.
El código fuente está disponible aquí. Este repositorio tiene varios proyectos CDK pero primero, usaremos el v1-simple
3.1. Preparar para probar
Con CDK, cuando ejecutas cdk synth, sintetizará un stack definido en tu app en una plantilla de CloudFormation en un archivo json en la carpeta cdk.out.
Sin embargo, SAM usa una plantilla yaml, template.yaml o template.yml, en la carpeta raíz.
Además, para probar localmente, necesitarás este archivo creado o recibirás un error
1 2 3 > sam local invoke Error: Template file not found at /.../aws-cdk-simple-webservice/template.yml
Por lo tanto, tenemos que ejecutar cdk synth y almacenar el resultado en un archivo con el nombre template.yml.
Tienes que usar --no-staging porque es requerido para que el CLI de SAM depure localmente los archivos fuente. Deshabilitará la copia de assets lo que permite que la depuración local haga referencia a los archivos fuente originales
1
cdk synth --no-staging > template.yml
3.2. Probando funciones Lambda
Ahora, tienes un archivo template.yml y puedes ejecutar el comando SAM para probar tu función Lambda.
1
2
3
4
5
6
7
8
9
10
> sam local invoke
Invoking index.handler (nodejs14.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-nodejs14.x:rapid-1.46.0-x86_64.
Mounting /Users/alazaroc/Documents/MyProjects/github/aws/cdk/aws-cdk-simple-webservice/v1-simple/functions/simplest-example as /var/task:ro,delegated inside runtime container
START RequestId: 03d4ef7d-47b4-4ad2-a491-d0e5fc797ece Version: $LATEST
2022-04-22T21:00:38.952Z 03d4ef7d-47b4-4ad2-a491-d0e5fc797ece INFO request: {}
END RequestId: 03d4ef7d-47b4-4ad2-a491-d0e5fc797ece
REPORT RequestId: 03d4ef7d-47b4-4ad2-a491-d0e5fc797ece Init Duration: 0.39 ms Duration: 205.98 ms Billed Duration: 206 ms Memory Size: 512 MB Max Memory Used: 512 MB
{"statusCode":200,"headers":{"Content-Type":"text/html"},"body":"You have connected with the Lambda!"}%
La Lambda devuelve el siguiente body: You have connected with the Lambda!
3.3. Probando funciones Lambda con datos de entrada
Si tus funciones Lambda necesitan datos de entrada, puedes generarlos desde el CLI de SAM con el comando generate-event
1
sam local generate-event [OPTIONS] COMMAND [ARGS]...
Puedes usar este comando para generar payloads de ejemplo de diferentes fuentes de eventos como S3, API Gateway, SNS, etc. Estos payloads contienen la información que las fuentes de eventos envían a tus funciones Lambda.
Alternativamente, puedes añadir los datos de entrada usando la opción -e con el comando invoke.
1
2
3
4
5
6
7
8
> sam local invoke -e test/events/simple-event.json
Invoking index.handler (nodejs14.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-nodejs14.x:rapid-1.46.0-x86_64.
Mounting /Users/alazaroc/Documents/MyProjects/github/aws/cdk/aws-cdk-simple-webservice/v1-simple/functions/simplest-example as /var/task:ro,delegated inside runtime container
} "rawPath": "/test"706Z 992499c1-83c4-408d-966b-2e13f5955cbc INFO input: {
{"statusCode":200,"headers":{"Content-Type":"text/html"},"body":"You have connected with the Lambda!"}END RequestId: 992499c1-83c4-408d-966b-2e13f5955cbc
REPORT RequestId: 992499c1-83c4-408d-966b-2e13f5955cbc Init Duration: 0.89 ms Duration: 244.32 ms Billed Duration: 245 ms Memory Size: 512 MB Max Memory Used: 512 MB
3.4. Probando API Gateway
Tienes que ejecutar sam local start-api
1
2
3
4
> sam local start-api
Mounting simplest-lambda at http://127.0.0.1:3000$default [X-AMAZON-APIGATEWAY-ANY-METHOD]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2022-04-23 00:03:58 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
Puedes acceder a
http://127.0.0.1:3000/para conectarte con tu función Lambda
Si revisas tu consola anterior, se actualizará cuando accedas a tu API Gateway:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> sam local start-api
default [X-AMAZON-APIGATEWAY-ANY-METHOD]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2022-04-23 00:03:58 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
Invoking index.handler (nodejs14.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-nodejs14.x:rapid-1.46.0-x86_64.
Mounting /Users/alazaroc/Documents/MyProjects/github/aws/cdk/aws-cdk-simple-webservice/v1-simple/functions/simplest-example as /var/task:ro,delegated inside runtime container
START RequestId: 8ed4a0a8-18bc-43af-b759-ad0668784351 Version: $LATEST
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Ge "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng} "isBase64Encoded": falsehost"01 +0000",-11ba1c012bf1",
END RequestId: 8ed4a0a8-18bc-43af-b759-ad0668784351
REPORT RequestId: 8ed4a0a8-18bc-43af-b759-ad0668784351 Init Duration: 0.51 ms Duration: 230.55 ms Billed Duration: 231 ms Memory Size: 512 MB Max Memory Used: 512 MB
2022-04-23 00:11:05 127.0.0.1 - - [23/Apr/2022 00:11:05] "GET / HTTP/1.1" 200 -
Invoking index.handler (nodejs14.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-nodejs14.x:rapid-1.46.0-x86_64.
Mounting /Users/alazaroc/Documents/MyProjects/github/aws/cdk/aws-cdk-simple-webservice/v1-simple/functions/simplest-example as /var/task:ro,delegated inside runtime container
START RequestId: 5759a74a-40b5-4a7e-8362-eec719ae44a7 Version: $LATEST
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Ge} "isBase64Encoded": falseco"t"01 +0000",-11ba1c012bf1",g+xml,image/*,*/*;q=0.8",
END RequestId: 5759a74a-40b5-4a7e-8362-eec719ae44a7
REPORT RequestId: 5759a74a-40b5-4a7e-8362-eec719ae44a7 Init Duration: 0.50 ms Duration: 238.45 ms Billed Duration: 239 ms Memory Size: 512 MB Max Memory Used: 512 MB
2022-04-23 00:11:06 127.0.0.1 - - [23/Apr/2022 00:11:06] "GET /favicon.ico HTTP/1.1" 200 -
3.5. Bonus: Probando DynamoDB
Ok, probar una función Lambda simulada es el ejemplo “hello world” y no muy útil, pero ¿qué tal una función Lambda que se conecta a una tabla DynamoDB?
Actualizaremos nuestra función Lambda para almacenar los datos en una tabla DynamoDB, así que usaremos el ejemplo v2-dynamodb en el repositorio.
Este código está basado en el patrón definido en la web cdkpatterns como el simple webservice
3.5.1. Caso 1: Probando DynamoDB en la nube
Cuando intentas probar localmente una función Lambda que almacena datos en una tabla DynamoDB, intentará automáticamente conectarse al servicio DynamoDB de tu cuenta AWS.
Así que, para probar tus tablas DynamoDB de tu cuenta, no tienes que hacer nada.
1
2
3
4
5
6
7
8
9
10
> sam local invoke -e test/events/simple-event.json
Invoking index.handler (nodejs14.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-nodejs14.x:rapid-1.46.0-x86_64.
Mounting /Users/alazaroc/Documents/MyProjects/github/aws/cdk/aws-cdk-simple-webservice/v2-dynamodb/functions/dynamodb-example as /var/task:ro,delegated inside runtime container
} "rawPath": "/test"492Z 69d093d2-083c-46e7-a318-636ed94d7e47 INFO request: {
2022-04-23T06:38:56.797Z 69d093d2-083c-46e7-a318-636ed94d7e47 ERROR Invoke Error {"errorType":"ResourceNotFoundException","errorMessage":"Requested resource not found","code":"ResourceNotFoundException","message":"Requested resource not found","time":"2022-04-23T06:38:56.787Z","requestId":"RCJDUN8DRCPPOS3SR3034ETK8VVV4KQNSO5AEMVJF66Q9ASUAAJG","statusCode":400,"retryable":false,"retryDelay":49.42319019990148,"stack":["ResourceNotFoundException: Requested resource not found"," at Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/json.js:52:27)"," at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20)"," at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:78:10)"," at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:686:14)"," at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)"," at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)"," at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10"," at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)"," at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:688:12)"," at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:116:18)"]}
{"errorType":"ResourceNotFoundException","errorMessage":"Requested resource not found","trace":["ResourceNotFoundException: Requested resource not found"," at Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/json.js:52:27)"," at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20)"," at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:78:10)"," at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:686:14)"," at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)"," at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)"," at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10"," at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)"," at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:688:12)"," at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequentialEND RequestId: 69d093d2-083c-46e7-a318-636ed94d7e47
REPORT RequestId: 69d093d2-083c-46e7-a318-636ed94d7e47 Init Duration: 0.98 ms Duration: 928.39 ms Billed Duration: 929 ms Memory Size: 512 MB Max Memory Used: 512 MB
_executor.js:116:18)"]}%
Si no despliegas tu proyecto CDK antes de intentar probarlo, obtendrás el siguiente ERROR:
"errorType":"ResourceNotFoundException","errorMessage":"Requested resource not found"
Por supuesto, puedes configurar tu cuenta AWS en tu CLI de SAM usando el comando profile.
1
2
3
$ sam local invoke -e test/events/simple-event.json profile test
Invoking index.handler (nodejs14.x)
...
3.5.2. Caso 2: Probando DynamoDB local
Puede que quieras probar tu función Lambda localmente en lugar de conectarte a tu cuenta DynamoDB, así que haremos lo siguiente:
- Descargar la imagen Docker de DynamoDB
- Ejecutar la imagen Docker de DynamoDB localmente
- Configurar DynamoDB: crear tablas, insertar datos y probarlo
- Cambiar el código de tu función Lambda
- Probar DynamoDB localmente
3.5.2.1. Descargar la imagen Docker de DynamoDB
Primero, descarga la imagen Docker de DynamoDB.
1
2
3
4
5
6
7
8
9
> docker pull amazon/dynamodb-local
Using default tag: latest
latest: Pulling from amazon/dynamodb-local
3a461b3ae562: Pull complete
14d349bd5978: Pull complete
3e361eec6409: Pull complete
Digest: sha256:07e740ad576acdcfdc48676f9a153a93a8e35436ea36942d4c14939caeca8851
Status: Downloaded newer image for amazon/dynamodb-local:latest
docker.io/amazon/dynamodb-local:latest
3.5.2.2. Ejecutar la imagen Docker de DynamoDB localmente
A continuación, ejecuta la imagen Docker de DynamoDB descargada localmente.
Esta pestaña de terminal se mantendrá ejecutándose y tendrás que abrir otra.
1
2
3
4
5
6
7
8
> docker run -p 8000:8000 amazon/dynamodb-local
Initializing DynamoDB Local with the following configuration:
Port: 8000
InMemory: true
DbPath: null
SharedDb: false
shouldDelayTransientStatuses: false
CorsParams: *
Este comando no persistirá datos en el DynamoDB local.
3.5.2.3. Crear una tabla DynamoDB local
Para crear una tabla DynamoDB local llamada hits con una clave de partición path, ejecuta el siguiente comando:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
> aws dynamodb create-table --table-name hits --attribute-definitions AttributeName=path,AttributeType=S --key-schema AttributeName=path,KeyType=HASH --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 --endpoint-url http://localhost:8000
{
"TableDescription": {
"TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/hits",
"AttributeDefinitions": [
{
"AttributeName": "path",
"AttributeType": "S"
}
],
"ProvisionedThroughput": {
"NumberOfDecreasesToday": 0,
"WriteCapacityUnits": 1,
"LastIncreaseDateTime": 0.0,
"ReadCapacityUnits": 1,
"LastDecreaseDateTime": 0.0
},
"TableSizeBytes": 0,
"TableName": "hits",
"TableStatus": "ACTIVE",
"KeySchema": [
{
"KeyType": "HASH",
"AttributeName": "path"
}
],
"ItemCount": 0,
"CreationDateTime": 1650668228.617
}
}
3.5.2.4. Añadir valores a nuestra tabla DynamoDB local
Añadiremos dos elementos:
- path: “/test”
- path: “/hello”
1
2
aws dynamodb put-item --table-name hits --item '{ "path": {"S": "/test"} }' --return-consumed-capacity TOTAL --endpoint-url http://localhost:8000
aws dynamodb put-item --table-name hits --item '{ "path": {"S": "/hello"} }' --return-consumed-capacity TOTAL --endpoint-url http://localhost:8000
3.5.2.5. Escanear tu tabla localmente
Verificamos que nuestra tabla tiene los elementos creados:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> aws dynamodb scan --table-name hits --endpoint-url http://localhost:8000
{
"Count": 2,
"Items": [
{
"path": {
"S": "/test"
}
},
{
"path": {
"S": "/hello"
}
}
],
"ScannedCount": 2,
"ConsumedCapacity": null
}
3.5.2.6. Cambiar el código de la función Lambda
Necesitamos actualizar el código de nuestra función Lambda para decirle a DynamoDB que lea desde nuestro servicio DynamoDB local:
Tienes que usar tu endpoint Docker específico
1
2
3
4
5
6
7
8
if (process.env.AWS_SAM_LOCAL) {
// mac
dynamo.endpoint = new AWS.Endpoint("http://docker.for.mac.localhost:8000/");
// windows
// dynamo.endpoint = new AWS.Endpoint("http://docker.for.windows.localhost:8000/");
// linux
// dynamo.endpoint = new AWS.Endpoint("http://127.0.0.1:8000");
}
3.5.2.7. Probar DynamoDB localmente
En resumen, tenemos:
- el servicio DynamoDB ejecutándose localmente
- una tabla (hits)
- 2 elementos
- path=/test
- path=/hello
Ahora vamos a probar nuestra función Lambda que insertará datos en nuestra tabla DynamoDB local.
1
2
3
4
5
6
7
8
9
10
> sam local invoke -e test/events/simple-event.json
Invoking index.handler (nodejs14.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-nodejs14.x:rapid-1.46.0-x86_64.
Mounting /Users/alazaroc/Documents/MyProjects/github/aws/cdk/aws-cdk-simple-webservice/v2-dynamodb/functions/dynamodb-example as /var/task:ro,delegated inside runtime container
} "rawPath": "/test"036Z eaf85e61-e9a2-4b49-9953-d247f9794fb8 INFO request: {
2022-04-22T23:54:20.130Z eaf85e61-e9a2-4b49-9953-d247f9794fb8 INFO inserted counter for /test
END RequestId: eaf85e61-e9a2-4b49-9953-d247f9794fb8
REPORT RequestId: eaf85e61-e9a2-4b49-9953-d247f9794fb8 Init Duration: 0.48 ms Duration: 697.40 ms Billed Duration: 698 ms Memory Size: 512 MB Max Memory Used: 512 MB
{"statusCode":200,"headers":{"Content-Type":"text/html"},"body":"You have connected with the Lambda and store the data in the DynamoDB table!"}
Si escaneamos la tabla nuevamente, podemos revisar que en el elemento “/test” habrá una nueva columna hits y valores 2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> aws dynamodb scan --table-name hits --endpoint-url http://localhost:8000
{
"Count": 2,
"Items": [
{
"path": {
"S": "/test"
},
"hits": {
"N": "2"
}
},
{
"path": {
"S": "/hello"
}
}
],
"ScannedCount": 2,
"ConsumedCapacity": null
}
Si ejecutas tu función más veces, el valor de hits se actualizará.
Y, por supuesto, también puedes probarlo desde API Gateway:
1
2
3
4
> sam local start-api
Mounting dynamodb-lambda at http://127.0.0.1:3000$default [X-AMAZON-APIGATEWAY-ANY-METHOD]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2022-04-25 19:53:01 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
4. Conclusión
En este artículo hemos visto una forma práctica de combinar AWS CDK y AWS SAM para construir aplicaciones serverless sin añadir complejidad innecesaria. Ambas herramientas resuelven problemas distintos y, dependiendo del tamaño y la madurez de la arquitectura, usarlas juntas puede ser una opción muy sólida.
- AWS CDK encaja especialmente bien cuando necesitas estructura, reutilización y un mayor control sobre la infraestructura, sobre todo a medida que los proyectos crecen.
- AWS SAM, por su parte, mantiene el foco en el desarrollo serverless, facilitando la definición, pruebas y despliegue de funciones Lambda y APIs.
Desde un punto de vista realista, este enfoque funciona mejor cuando quieres mantener los workloads serverless simples, pero al mismo tiempo necesitas una forma limpia y escalable de gestionar recursos compartidos e infraestructura. CDK puede encargarse de la orquestación y las dependencias entre stacks, mientras que SAM permanece cerca de la lógica de la aplicación.
Esto no va de elegir CDK en lugar de SAM, o al revés. Va de entender cuándo tiene sentido cada herramienta y combinarlas cuando la arquitectura lo requiere.



