Cloud AWS

Arquitectura Serverless con AWS CDK: Guía completa

Aprende a diseñar e implementar una arquitectura serverless completa usando AWS CDK, Lambda, API Gateway y DynamoDB.

15 min
AWS CDK Serverless Lambda DynamoDB

AWS CDK (Cloud Development Kit) te permite definir tu infraestructura cloud usando lenguajes de programación familiares. En este artículo, construiremos una arquitectura serverless completa.

¿Por qué CDK sobre CloudFormation?

CDK ofrece ventajas significativas:

  • Type safety: Autocompletado y detección de errores en tiempo de desarrollo
  • Reutilización: Crea componentes reutilizables
  • Lenguajes familiares: TypeScript, Python, Java, etc.
  • Menor código: Menos líneas que CloudFormation equivalente

Arquitectura que construiremos

Crearemos una API REST con:

  • API Gateway para endpoints HTTP
  • Lambda functions para lógica de negocio
  • DynamoDB para persistencia
  • S3 para almacenamiento de archivos
  • CloudWatch para logs y monitoreo

Setup inicial

npm install -g aws-cdk
mkdir my-serverless-app && cd my-serverless-app
cdk init app --language=typescript
npm install @aws-cdk/aws-lambda @aws-cdk/aws-apigateway @aws-cdk/aws-dynamodb

Definiendo la infraestructura

import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import { Construct } from "constructs";

export class ServerlessStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Tabla DynamoDB
    const table = new dynamodb.Table(this, "ItemsTable", {
      partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY, // Solo para dev
    });

    // Lambda function
    const handler = new lambda.Function(this, "ItemsHandler", {
      runtime: lambda.Runtime.NODEJS_18_X,
      code: lambda.Code.fromAsset("lambda"),
      handler: "index.handler",
      environment: {
        TABLE_NAME: table.tableName,
      },
    });

    // Dar permisos a Lambda para acceder a DynamoDB
    table.grantReadWriteData(handler);

    // API Gateway
    const api = new apigateway.RestApi(this, "ItemsApi", {
      restApiName: "Items Service",
      description: "API for managing items",
    });

    const items = api.root.addResource("items");
    items.addMethod("GET", new apigateway.LambdaIntegration(handler));
    items.addMethod("POST", new apigateway.LambdaIntegration(handler));

    const item = items.addResource("{id}");
    item.addMethod("GET", new apigateway.LambdaIntegration(handler));
    item.addMethod("PUT", new apigateway.LambdaIntegration(handler));
    item.addMethod("DELETE", new apigateway.LambdaIntegration(handler));

    // Outputs
    new cdk.CfnOutput(this, "ApiUrl", {
      value: api.url,
      description: "URL del API Gateway",
    });
  }
}

Código Lambda

// lambda/index.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand, DeleteCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';

const client = new DynamoDBClient({});
const ddb = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.TABLE_NAME!;

export const handler = async (event: any) => {
const method = event.httpMethod;
const path = event.path;

try {
switch (\`\${method} \${path}\`) {
case 'GET /items':
return await listItems();
case 'GET /items/{id}':
return await getItem(event.pathParameters.id);
case 'POST /items':
return await createItem(JSON.parse(event.body));
case 'PUT /items/{id}':
return await updateItem(event.pathParameters.id, JSON.parse(event.body));
case 'DELETE /items/{id}':
return await deleteItem(event.pathParameters.id);
default:
return { statusCode: 404, body: 'Not Found' };
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};

async function listItems() {
const result = await ddb.send(new ScanCommand({ TableName: TABLE_NAME }));
return {
statusCode: 200,
body: JSON.stringify(result.Items),
};
}

async function getItem(id: string) {
const result = await ddb.send(
new GetCommand({
TableName: TABLE_NAME,
Key: { id },
})
);
return {
statusCode: 200,
body: JSON.stringify(result.Item),
};
}

async function createItem(item: any) {
await ddb.send(
new PutCommand({
TableName: TABLE_NAME,
Item: { ...item, id: crypto.randomUUID() },
})
);
return {
statusCode: 201,
body: JSON.stringify({ message: 'Item created' }),
};
}

Deploy

# Compilar TypeScript
npm run build

# Sintetizar CloudFormation
cdk synth

# Deploy
cdk deploy

Mejores prácticas

  1. Separación de stacks: Usa múltiples stacks para diferentes ambientes
  2. Variables de entorno: Usa context values para configuración
  3. Testing: Escribe tests unitarios con aws-cdk assertions
  4. Cost optimization: Usa Reserved Concurrency para controlar costos

Monitoreo y logs

CDK automáticamente crea CloudWatch Logs. Puedes agregar alarmas:

```typescript import * as cloudwatch from ‘aws-cdk-lib/aws-cloudwatch’;

const errorAlarm = new cloudwatch.Alarm(this, ‘ErrorAlarm’, {

import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';

const errorAlarm = new cloudwatch.Alarm(this, 'ErrorAlarm', {
  metric: handler.metricErrors(),
  threshold: 5,
  evaluationPeriods: 1,
});
``DK simplifica enormemente la creación de arquitecturas serverless. Con type safety y componentes reutilizables, puedes construir infraestructura robusta de manera eficiente.

¿Usas CDK en producción? Comparte tu experiencia.

¿Te gustó este artículo?

Suscríbete para recibir más contenido sobre desarrollo web y programación

Contáctame