March 2, 2025

Migration von Serverless Framework zu AWS CDK: Teil 2 - CDK-Umgebung Einrichten

Lerne, wie du ein CDK-Projekt für serverless Anwendungen strukturierst, TypeScript für Lambda-Entwicklung konfigurierst und Muster etablierst, die die Migration vom Serverless Framework erleichtern.

Woche 1 unserer CDK-Migration. Die Entscheidung war getroffen, das Budget genehmigt, und mein 12-köpfiges Ingenieursteam schaute mich erwartungsvoll an. "Also, wo fangen wir an?"

Ich hatte CDK in persönlichen Projekten verwendet, aber es zu skalieren, um 47 Lambda-Funktionen über 4 Umgebungen mit 12 Entwicklern zu verwalten? Das war anders. Wir brauchten Struktur, Konventionen und Muster, die nicht nur für mich, sondern für das gesamte Team funktionieren würden.

Das ist die Geschichte, wie wir eine CDK-Projektstruktur entwarfen, die es 12 Ingenieuren ermöglichte, parallel zu arbeiten, ohne sich gegenseitig auf die Füße zu treten, die vertrauten Muster vom Serverless Framework beibehielt und zur Grundlage für unsere 2,8M Dollar ARR-Plattform wurde.

Serien-Navigation:

Die Projektstruktur Die Tatsächlich Skaliert#

Unser erster Versuch war eine Katastrophe. Ich kopierte naiv eine einfache CDK-Tutorial-Struktur, und innerhalb einer Woche hatten wir Merge-Konflikte, unklare Eigentumsrechte und verwirrte Ingenieure, die fragten: "Wo gehört diese Datei hin?"

Hier ist die Entwicklung vom Chaos zur Ordnung:

Bash
# Serverless Framework Struktur
my-service/
├── serverless.yml
├── package.json
├── src/
   └── handlers/
       ├── users.js
       └── products.js
├── resources/
   └── dynamodb-tables.yml
└── config/
    ├── dev.yml
    └── prod.yml

# CDK Struktur (Nach 3 gescheiterten Versuchen)
my-service/
├── cdk.json                  # CDK App-Konfiguration
├── package.json
├── bin/
   └── my-service.ts         # Einzelner Entry Point (auf die harte Tour gelernt)
├── lib/
   ├── stacks/               # Stack-Definitionen nach Domain
   ├── api-stack.ts      # API Gateway + Lambda-Funktionen
   ├── data-stack.ts     # DynamoDB-Tabellen (stateful)
   └── auth-stack.ts     # Cognito + Auth-Logik
   ├── constructs/           # Wiederverwendbare Muster (unser Geheimnis)
   ├── production-lambda.ts  # 376 Funktionen verwenden dies
   ├── api-with-auth.ts      # Jede API folgt diesem Muster
   └── monitored-table.ts    # DynamoDB mit Alarmen
   └── config/               # Umgebungsspezifische Configs
       ├── development.ts
       ├── staging.ts
       └── production.ts
├── src/
   └── handlers/             # Lambda-Code (vertrauter Ort)
       ├── users/            # Nach Domain gruppiert
   ├── create.ts
   ├── update.ts
   └── list.ts
       └── products/
           ├── catalog.ts
           └── inventory.ts
└── test/
    ├── unit/                 # Handler Unit-Tests
    ├── integration/          # API Integration-Tests
    └── infrastructure/       # CDK Stack-Tests

Die wichtigste Erkenntnis: Domain-getriebene Organisation verhindert Merge-Konflikte, wenn 12 Ingenieure parallel arbeiten.

Dein CDK-Projekt Initialisieren#

Stell zunächst sicher, dass du die Voraussetzungen hast:

Bash
# AWS CDK CLI global installieren
npm install -g aws-cdk@2

# Installation überprüfen
cdk --version  # Sollte 2.x.x zeigen

# AWS-Credentials konfigurieren (falls noch nicht geschehen)
aws configure

Erstell jetzt dein Projekt:

Bash
# Projektverzeichnis erstellen
mkdir my-serverless-api && cd my-serverless-api

# CDK mit TypeScript initialisieren
cdk init app --language typescript

# Lambda-spezifische Dependencies installieren
npm install @aws-cdk/aws-lambda-nodejs-alpha @types/aws-lambda

# Entwicklungstools installieren
npm install --save-dev esbuild @types/node ts-node

TypeScript für Lambda-Entwicklung Konfigurieren#

CDK generiert eine grundlegende tsconfig.json. Lass uns sie für serverless Entwicklung optimieren:

JSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "declaration": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "inlineSourceMap": true,
    "inlineSources": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "outDir": "./dist",
    "rootDir": "./",
    "baseUrl": "./",
    "paths": {
      "@handlers/*": ["src/handlers/*"],
      "@libs/*": ["src/libs/*"],
      "@constructs/*": ["lib/constructs/*"]
    }
  },
  "include": [
    "bin/**/*",
    "lib/**/*",
    "src/**/*",
    "test/**/*"
  ],
  "exclude": [
    "cdk.out",
    "node_modules"
  ]
}

Umgebungs-Konfigurationsmanagement#

Serverless Framework verwendet YAML-Dateien für umgebungsspezifische Konfiguration. Lassen Sie uns ein TypeScript-basiertes Äquivalent erstellen:

TypeScript
// lib/config/environment.ts
export interface EnvironmentConfig {
  stage: string;
  region: string;
  account: string;
  api: {
    throttling: {
      rateLimit: number;
      burstLimit: number;
    };
    cors: {
      origins: string[];
      credentials: boolean;
    };
  };
  lambda: {
    memorySize: number;
    timeout: number;
    reservedConcurrentExecutions?: number;
  };
  monitoring: {
    alarmEmail?: string;
    enableXRay: boolean;
    logRetentionDays: number;
  };
}

// lib/config/stages/dev.ts
export const devConfig: EnvironmentConfig = {
  stage: 'dev',
  region: 'us-east-1',
  account: '123456789012',
  api: {
    throttling: {
      rateLimit: 100,
      burstLimit: 200,
    },
    cors: {
      origins: ['http://localhost:3000'],
      credentials: true,
    },
  },
  lambda: {
    memorySize: 512,
    timeout: 30,
  },
  monitoring: {
    enableXRay: true,
    logRetentionDays: 7,
  },
};

// lib/config/stages/prod.ts
export const prodConfig: EnvironmentConfig = {
  stage: 'prod',
  region: 'us-east-1',
  account: '123456789012',
  api: {
    throttling: {
      rateLimit: 1000,
      burstLimit: 2000,
    },
    cors: {
      origins: ['https://myapp.com'],
      credentials: true,
    },
  },
  lambda: {
    memorySize: 1024,
    timeout: 30,
    reservedConcurrentExecutions: 100,
  },
  monitoring: {
    alarmEmail: 'alerts@myapp.com',
    enableXRay: true,
    logRetentionDays: 30,
  },
};

// lib/config/index.ts
import { devConfig } from './stages/dev';
import { prodConfig } from './stages/prod';

export function getConfig(stage: string): EnvironmentConfig {
  switch (stage) {
    case 'dev':
      return devConfig;
    case 'prod':
      return prodConfig;
    default:
      throw new Error(`Unbekannte Stage: ${stage}`);
  }
}

Ihr Erstes Construct Erstellen#

Constructs sind CDKs Bausteine. Lassen Sie uns ein wiederverwendbares Muster für Lambda-Funktionen erstellen:

TypeScript
// lib/constructs/serverless-function.ts
import { Construct } from 'constructs';
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime, Tracing } from 'aws-cdk-lib/aws-lambda';
import { Duration } from 'aws-cdk-lib';
import { EnvironmentConfig } from '../config/environment';

export interface ServerlessFunctionProps {
  entry: string;
  handler?: string;
  environment?: Record<string, string>;
  config: EnvironmentConfig;
  memorySize?: number;
  timeout?: number;
}

export class ServerlessFunction extends NodejsFunction {
  constructor(scope: Construct, id: string, props: ServerlessFunctionProps) {
    const { config, ...functionProps } = props;

    super(scope, id, {
      runtime: Runtime.NODEJS_20_X,
      handler: props.handler || 'handler',
      entry: props.entry,
      memorySize: props.memorySize || config.lambda.memorySize,
      timeout: Duration.seconds(props.timeout || config.lambda.timeout),
      tracing: config.monitoring.enableXRay ? Tracing.ACTIVE : Tracing.DISABLED,
      environment: {
        NODE_OPTIONS: '--enable-source-maps',
        STAGE: config.stage,
        ...props.environment,
      },
      bundling: {
        minify: config.stage === 'prod',
        sourceMap: true,
        sourcesContent: false,
        target: 'es2022',
        keepNames: true,
        // AWS SDK v3 ausschließen (in Lambda Runtime bereitgestellt)
        externalModules: [
          '@aws-sdk/*',
        ],
      },
      reservedConcurrentExecutions: config.lambda.reservedConcurrentExecutions,
    });
  }
}

Ihren Ersten Stack Einrichten#

Lassen Sie uns jetzt einen Stack erstellen, der unser Construct verwendet:

TypeScript
// lib/stacks/api-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { RestApi, LambdaIntegration, Cors } from 'aws-cdk-lib/aws-apigateway';
import { ServerlessFunction } from '../constructs/serverless-function';
import { EnvironmentConfig } from '../config/environment';

export interface ApiStackProps extends StackProps {
  config: EnvironmentConfig;
}

export class ApiStack extends Stack {
  public readonly api: RestApi;

  constructor(scope: Construct, id: string, props: ApiStackProps) {
    super(scope, id, props);

    const { config } = props;

    // API Gateway erstellen
    this.api = new RestApi(this, 'ServerlessApi', {
      restApiName: `my-service-${config.stage}`,
      deployOptions: {
        stageName: config.stage,
        throttlingRateLimit: config.api.throttling.rateLimit,
        throttlingBurstLimit: config.api.throttling.burstLimit,
      },
      defaultCorsPreflightOptions: {
        allowOrigins: config.api.cors.origins,
        allowCredentials: config.api.cors.credentials,
        allowMethods: Cors.ALL_METHODS,
        allowHeaders: [
          'Content-Type',
          'Authorization',
          'X-Api-Key',
        ],
      },
    });

    // Lambda-Funktionen erstellen
    const createUserFn = new ServerlessFunction(this, 'CreateUserFunction', {
      entry: 'src/handlers/users.ts',
      handler: 'create',
      config,
      environment: {
        // Umgebungsvariablen werden in Teil 4 hinzugefügt
      },
    });

    // Routen einrichten
    const users = this.api.root.addResource('users');
    users.addMethod('POST', new LambdaIntegration(createUserFn));
  }
}

CDK App Entry Point#

Aktualisieren Sie den CDK App Entry Point, um unser Konfigurationssystem zu verwenden:

TypeScript
// bin/my-service.ts
#!/usr/bin/env node
import 'source-map-support/register';
import { App } from 'aws-cdk-lib';
import { ApiStack } from '../lib/stacks/api-stack';
import { getConfig } from '../lib/config';

const app = new App();

// Stage aus Kontext oder Umgebung abrufen
const stage = app.node.tryGetContext('stage') || process.env.STAGE || 'dev';
const config = getConfig(stage);

new ApiStack(app, `MyServiceApiStack-${stage}`, {
  config,
  env: {
    account: config.account,
    region: config.region,
  },
  tags: {
    Stage: stage,
    Service: 'my-service',
    ManagedBy: 'cdk',
  },
});

Ihr Erster Lambda Handler#

Erstellen Sie einen Lambda-Handler mit TypeScript:

TypeScript
// src/handlers/users.ts
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';

export const create = async (
  event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {
  console.log('Event:', JSON.stringify(event, null, 2));

  try {
    const body = JSON.parse(event.body || '{}');

    // Handler-Logik hier (wird in Teil 3 erweitert)

    return {
      statusCode: 201,
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        message: 'Benutzer erfolgreich erstellt',
        stage: process.env.STAGE,
      }),
    };
  } catch (error) {
    console.error('Fehler:', error);

    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        error: 'Interner Serverfehler',
      }),
    };
  }
};

Deployment-Befehle#

Fügen Sie diese Skripte zu Ihrer package.json hinzu:

JSON
{
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "cdk": "cdk",
    "bootstrap": "cdk bootstrap",
    "deploy:dev": "cdk deploy --context stage=dev",
    "deploy:prod": "cdk deploy --context stage=prod",
    "diff:dev": "cdk diff --context stage=dev",
    "diff:prod": "cdk diff --context stage=prod",
    "synth": "cdk synth",
    "test": "jest",
    "test:watch": "jest --watch"
  }
}

Erstes Deployment#

Bootstrappen Sie Ihre AWS-Umgebung (einmalige Einrichtung):

Bash
npm run bootstrap

Deployen Sie auf Development:

Bash
npm run deploy:dev

CDK zeigt Ihnen, welche Ressourcen es zu erstellen plant. Überprüfen und bestätigen Sie.

Lokale Entwicklungsumgebung#

Anders als Serverless Frameworks serverless-offline bietet CDK keine integrierte lokale API Gateway-Emulation. Für lokale Entwicklung haben Sie mehrere Optionen:

  1. SAM CLI Integration (Empfohlen):
Bash
# SAM CLI installieren
brew install aws-sam-cli  # macOS
# oder folgen: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html

# CloudFormation-Template generieren
cdk synth --no-staging > template.yaml

# Lokale API starten
sam local start-api -t template.yaml
  1. Direktes Handler-Testing:
TypeScript
// test/handlers/users.test.ts
import { create } from '../../src/handlers/users';
import { APIGatewayProxyEventV2 } from 'aws-lambda';

describe('Users Handler', () => {
  it('sollte einen Benutzer erstellen', async () => {
    const event: Partial<APIGatewayProxyEventV2> = {
      body: JSON.stringify({ name: 'John Doe' }),
    };

    const result = await create(event as APIGatewayProxyEventV2);

    expect(result.statusCode).toBe(201);
    expect(JSON.parse(result.body!)).toHaveProperty('message');
  });
});

Wichtige Unterschiede Zu Beachten#

AspektServerless FrameworkCDK
KonfigurationYAML-DateienTypeScript-Code
Umgebungsvariablen${self:provider.stage}Config-Objekte
Lokale Entwicklungserverless-offlineSAM CLI oder Testing
Deploymentserverless deploycdk deploy
Ressourcenreferenzen!Ref oder ${cf:stackName.output}Direkte Objektreferenzen

Was Als Nächstes#

Sie haben jetzt eine solide CDK-Grundlage, die Serverless Framework-Konventionen widerspiegelt, während sie CDKs Typsicherheit und Komponierbarkeit umarmt. Ihre Lambda-Funktionen leben an vertrauten Orten, aber Ihre Infrastruktur ist jetzt Code - echter, testbarer TypeScript-Code.

In Teil 3 werden wir Lambda-Funktionen und API Gateway-Konfigurationen migrieren, einschließlich:

  • Request/Response-Transformationen
  • API Gateway-Modelle und Validatoren
  • Lambda-Layer und Dependencies
  • Error-Handling-Muster
  • API-Versionierungsstrategien

Die Grundlage ist gelegt. Lassen Sie uns Ihre serverless API bauen.

Loading...

Kommentare (0)

An der Unterhaltung teilnehmen

Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren

Noch keine Kommentare

Sei der erste, der deine Gedanken zu diesem Beitrag teilt!

Related Posts