DEV Community

Cover image for AWS CDK con TypeScript para un proyecto IoT: infraestructura como código
Steven Carvajal
Steven Carvajal

Posted on Originally published at iot.gripe

AWS CDK con TypeScript para un proyecto IoT: infraestructura como código

Publicado originalmente en iot.gripe, como parte 12 de la serie "Domótica con ESP32 y AWS desde cero".

Casi todos los proyectos empiezan igual: una Lambda creada en la consola, un rol con un permiso agregado "solo para probar", una tabla creada con la CLI. Funciona, pero si mañana alguien borra un rol por error, reconstruirlo exactamente igual es una tarde de arqueología.

AWS CDK permite describir esa infraestructura con TypeScript y desplegarla con un comando. En esta parte definimos con CDK las piezas del proyecto de ejemplo de la serie y vemos cómo adoptar recursos que ya creaste a mano.

Qué es CDK

CDK es un framework que convierte código (TypeScript, Python, Java...) en plantillas de CloudFormation. Escribes clases como dynamodb.Table o lambda.Function, y CDK genera la plantilla y la despliega.

Ventajas frente a escribir CloudFormation a mano:

  • Abstracciones con buenos valores por defecto. table.grantReadData(fn) genera la política IAM exacta.
  • El lenguaje completo: bucles, funciones y tipos para no repetir configuración.
  • Revisión antes de desplegar: cdk diff muestra qué va a cambiar.

Empezar

npm install -g aws-cdk
mkdir infra && cd infra
cdk init app --language typescript
cdk bootstrap   # una vez por cuenta y región
Enter fullscreen mode Exit fullscreen mode

cdk bootstrap crea los recursos que CDK necesita para desplegar (un bucket para los assets y unos roles).

El stack del proyecto de ejemplo

import * as cdk from "aws-cdk-lib";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";
import * as iot from "aws-cdk-lib/aws-iot";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

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

    const thingName = "mi-esp32";

    // 1. Tabla de dispositivos (con estado: no se borra con el stack)
    const tabla = new dynamodb.Table(this, "Dispositivos", {
      partitionKey: { name: "casaId", type: dynamodb.AttributeType.STRING },
      sortKey: { name: "dispositivoId", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
    });

    // 2. Lambda de la API (parte 8)
    const api = new NodejsFunction(this, "ApiOrdenes", {
      entry: "lambda/api-ordenes.ts",
      runtime: lambda.Runtime.NODEJS_22_X,
      timeout: cdk.Duration.seconds(10),
      memorySize: 256,
      environment: { THING_NAME: thingName, TABLA: tabla.tableName },
    });
    tabla.grantReadData(api);
    api.addToRolePolicy(
      new iam.PolicyStatement({
        actions: ["iot:UpdateThingShadow"],
        resources: [`arn:aws:iot:${this.region}:${this.account}:thing/${thingName}`],
      })
    );
    const url = api.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.NONE });

    // 3. Política de IoT del dispositivo (parte 13)
    new iot.CfnPolicy(this, "PoliticaDispositivo", {
      policyName: "casa-demo-dispositivo",
      policyDocument: {
        Version: "2012-10-17",
        Statement: [
          {
            Effect: "Allow",
            Action: "iot:Connect",
            Resource: `arn:aws:iot:${this.region}:${this.account}:client/\${iot:Connection.Thing.ThingName}`,
          },
          // ... Subscribe, Receive y Publish sobre los tópicos de su Shadow
        ],
      },
    });

    new cdk.CfnOutput(this, "ApiUrl", { value: url.url });
  }
}
Enter fullscreen mode Exit fullscreen mode

Detalles a notar:

  • grantReadData genera los permisos de lectura sobre la tabla y sus índices, sin escribir ARNs a mano.
  • Los permisos de IoT sí van a mano, con el recurso exacto. Evita resources: ["*"].
  • El \${...} escapa la variable de política de IoT para que TypeScript no intente interpolarla.
  • NodejsFunction compila y empaqueta la Lambda con esbuild, incluidas sus dependencias.

Recursos con estado: el cuidado principal

Si CDK gestiona un recurso, un cambio en el código puede reemplazarlo (borrar y crear otro), y borrar el stack puede eliminarlo. Para recursos sin estado (Lambdas, roles) no importa. Para recursos con estado, sí:

Recurso Riesgo si se recrea Qué hacer
Tabla de DynamoDB Pierdes todos los datos removalPolicy: RETAIN y respaldos
User Pool de Cognito Se invalidan todas las cuentas RETAIN, y cuidado con cambios que fuerzan reemplazo
Certificados de IoT Los dispositivos dejan de conectar Gestionarlos fuera del stack

Antes de cada despliegue, ejecuta cdk diff y busca la palabra replace. Algunos cambios aparentemente inocentes (como renombrar la clave de una tabla o ciertos atributos de un User Pool) obligan a reemplazar el recurso.

Otra opción válida: dejar los recursos con estado fuera del stack y solo referenciarlos por nombre o ARN para dar permisos:

const tabla = dynamodb.Table.fromTableName(this, "TablaExistente", "dispositivos");
tabla.grantReadData(api);  // funciona igual, pero CDK nunca la crea ni la borra
Enter fullscreen mode Exit fullscreen mode

Si ya creaste todo a mano

Es lo más habitual. Pasos para llevarlo a CDK sin romper nada:

  1. Haz un inventario en solo lectura: cada Lambda (runtime, memoria, variables), cada rol con el texto exacto de sus políticas (aws iam get-role-policy), cada tabla con sus índices.
  2. Escribe el código para describir exactamente lo que existe, no para mejorarlo todavía. Usa los mismos nombres.
  3. Compara la plantilla generada (cdk synth) con el inventario.
  4. Adopta los recursos uno por uno con cdk import, empezando por los más simples (roles), y comprueba después de cada uno que nada cambió.
  5. Solo entonces, mejora: quita permisos que sobran, unifica nombres.

Un efecto secundario útil: describir la infraestructura en código obliga a mirar cada permiso. Casi siempre aparecen permisos que ya nadie usa.

Comandos del día a día

npx tsc --noEmit   # el código compila
npx cdk synth      # genera la plantilla de CloudFormation
npx cdk diff       # qué va a cambiar en la cuenta
npx cdk deploy     # despliega (pide confirmación si cambian permisos IAM)
Enter fullscreen mode Exit fullscreen mode

Errores comunes

  • "This stack uses assets, so the toolkit stack must be deployed": falta cdk bootstrap en esa cuenta y región.
  • NodejsFunction no encuentra el archivo de entrada: la ruta de entry es relativa a donde ejecutas cdk, y busca un package-lock.json para saber la raíz del proyecto; si tus Lambdas están en otra carpeta, indica projectRoot y depsLockFilePath.
  • El despliegue falla porque el recurso ya existe: estás creando con CDK algo que existe a mano con el mismo nombre. Impórtalo o referéncialo.
  • Un rol que dejó de funcionar tras un despliegue: CDK reemplaza las políticas en línea que gestiona. No mezcles cambios manuales con recursos gestionados por CDK.

Preguntas frecuentes

¿CDK o Terraform?

Los dos sirven. CDK encaja si tu proyecto ya es TypeScript y solo usas AWS. Terraform tiene ventajas con varios proveedores o si tu equipo ya lo conoce. Los comparo en AWS CDK vs Terraform.

¿CDK cuesta algo?

No. Pagas los recursos que despliegas y unos céntimos por el bucket de assets del bootstrap.

¿Tengo que pasar todo a CDK de una vez?

No. Puedes empezar por las Lambdas y sus roles, que son los que más cuesta reconstruir a mano, y referenciar el resto por nombre.

Top comments (0)