Migration von Serverless Framework zu AWS CDK: Teil 4 - Datenbank- und Umgebungsmanagement

Meistern Sie DynamoDB-Migrationen, Umgebungsvariablen-Management, Secrets-Behandlung und VPC-Konfigurationen beim Wechsel von Serverless Framework zu AWS CDK.

Woche 5 unserer CDK-Migration. Wir hatten erfolgreich 23 Lambda-Funktionen verschoben, aber die wahre Herausforderung begann, als unsere Lead DevOps-Ingenieurin ankündigte, dass sie das Unternehmen verlassen würde. "Viel Glück mit der Datenbankmigration", sagte sie und übergab uns einen Haftnotiz mit drei DynamoDB-Tabellennamen, die unsere $2,8M ARR SaaS-Plattform antrieben.

Dieser Haftnotiz repräsentierte 4 Jahre Kundendaten, 180K Benutzer und null dokumentierte Backup-Verfahren. Das ist die Geschichte der Migration staatsbehafteter Infrastruktur ohne den Verlust eines einzigen Datensatzes - und die schmerzhaften Lektionen über Datenabhängigkeiten in Produktionssystemen.

Seriennavigation:

Die $47K Datenmigrations-Katastrophe (Fast)#

Bevor wir in die technischen Patterns eintauchen, lassen Sie mich erzählen, was passierte, als wir versuchten, unsere Produktionstabellen einfach zu "importieren".

Der Freitagnachmittag Tabellen-Import#

  1. März, 15:47 Uhr. Ich führte selbstbewusst cdk deploy aus, um unsere Hauptbenutzertabelle in die CDK-Verwaltung zu importieren. Das Deployment war erfolgreich. Unser Monitoring blieb grün. Alles sah perfekt aus.

Montagmorgen, 6:23 Uhr. Slack-Benachrichtigungen explodierten. Unsere Benutzerregistrierungs-API warf 500-Fehler. Die Tabelle war weg. Nicht unerreichbar - vollständig gelöscht.

Grundursache: CDK versuchte die Tabelle zu "importieren", interpretierte aber das bestehende Serverless Framework CloudFormation-Template als konfliktierend. Es löschte die alte Ressource, bevor es die neue erstellte. Zero-Downtime wurde zu Zero-Data.

Auswirkung: 4 Stunden Downtime, Notfall-Point-in-Time-Recovery und ein sehr unangenehmes All-Hands-Meeting über das "Befolgen der Deployment-Checkliste".

Lektion: Vertrauen Sie niemals Imports in der Produktion ohne explizite Retention-Policies und Proben in der Staging-Umgebung.

DynamoDB-Migrationsstrategien, die tatsächlich funktionieren#

Sicheres Tabellen-Import-Pattern#

Hier ist der kampferprobte Ansatz, den wir jetzt für Produktionsmigrationen verwenden:

YAML
# serverless.yml - Ursprüngliche Tabellendefinition
resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-${opt:stage}-users
        AttributeDefinitions:
          - AttributeName: userId
            AttributeType: S
          - AttributeName: email
            AttributeType: S
        KeySchema:
          - AttributeName: userId
            KeyType: HASH
        GlobalSecondaryIndexes:
          - IndexName: email-index
            KeySchema:
              - AttributeName: email
                KeyType: HASH
            Projection:
              ProjectionType: ALL
        BillingMode: PAY_PER_REQUEST

CDK-Ansatz für bestehende Tabellen:

TypeScript
// lib/constructs/production-table-import.ts
import { Table, ITable } from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';
import { CustomResource, Duration } from 'aws-cdk-lib';

export interface ProductionTableImportProps {
  tableName: string;
  region?: string;
  account?: string;
  // Kritisch: Tabelle vor Import verifizieren
  requireExistingTable: boolean;
}

export class ProductionTableImport extends Construct {
  public readonly table: ITable;
  
  constructor(scope: Construct, id: string, props: ProductionTableImportProps) {
    super(scope, id);
    
    if (props.requireExistingTable) {
      // Zuerst verifizieren, dass die Tabelle tatsächlich existiert
      const verifyFn = new NodejsFunction(this, 'VerifyTableExists', {
        entry: 'src/migrations/verify-table.ts',
        handler: 'handler',
        timeout: Duration.seconds(30),
        environment: {
          TABLE_NAME: props.tableName,
        },
      });

      // Custom Resource, die Deployment fehlschlagen lässt, wenn Tabelle nicht existiert
      new CustomResource(this, 'TableVerification', {
        serviceToken: verifyFn.functionArn,
        properties: {
          TableName: props.tableName,
        },
      });
    }
    
    // Nur nach erfolgreicher Verifikation importieren
    this.table = Table.fromTableAttributes(this, 'ImportedTable', {
      tableName: props.tableName,
      region: props.region,
      account: props.account,
      // KRITISCH: Das verhindert, dass CDK versucht, den Tabellen-Lebenszyklus zu verwalten
      tableStreamArn: undefined,
    });
  }
}

// src/migrations/verify-table.ts - Verhindert versehentliche Tabellenlöschung
import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';

const client = new DynamoDBClient({});

export const handler = async (event: any) => {
  const tableName = event.ResourceProperties.TableName;
  
  try {
    // Tabelle existiert und ist ACTIVE verifizieren
    const result = await client.send(new DescribeTableCommand({
      TableName: tableName,
    }));
    
    if (result.Table?.TableStatus !== 'ACTIVE') {
      throw new Error(`Table ${tableName} is not ACTIVE (status: ${result.Table?.TableStatus})`);
    }
    
    // Kritische Tabelleninfos für Audit-Trail loggen
    console.log('Production table verified:', {
      tableName,
      itemCount: result.Table.ItemCount || 'unknown',
      sizeBytes: result.Table.TableSizeBytes || 'unknown',
      status: result.Table.TableStatus,
    });
    
    return { PhysicalResourceId: `verified-${tableName}` };
  } catch (error) {
    console.error('Table verification failed:', error);
    throw error; // CloudFormation-Deployment fehlschlagen lassen
  }
};

// Verwendung mit Produktionssicherheitschecks
const usersTable = new ProductionTableImport(this, 'UsersTable', {
  tableName: `my-service-${config.stage}-users`,
  requireExistingTable: config.stage === 'prod', // Nur in Produktion verifizieren
}).table;

// Berechtigungen wie gewohnt erteilen
usersTable.grantReadWriteData(createUserFn);

Das Produktionsreife Tabellen-Pattern#

Nach der Verwaltung von 180K Benutzern und 12M Transaktionen ist hier unser kugelsicheres Tabellenerstellungspattern:

TypeScript
// lib/constructs/production-user-table.ts
import { 
  Table, 
  AttributeType, 
  BillingMode,
  TableEncryption,
  StreamViewType,
  ProjectionType
} from 'aws-cdk-lib/aws-dynamodb';
import { RemovalPolicy, Tags } from 'aws-cdk-lib';
import { Alarm, Metric, TreatMissingData } from 'aws-cdk-lib/aws-cloudwatch';

export class ProductionUserTable extends Table {
  constructor(scope: Construct, id: string, props: {
    stage: string;
    enableStreams?: boolean;
    enableBackup?: boolean;
  }) {
    super(scope, id, {
      // Versionierte Tabellennamen für Blue-Green-Deployments
      tableName: `my-service-${props.stage}-users-v3`,
      partitionKey: {
        name: 'userId',
        type: AttributeType.STRING,
      },
      sortKey: {
        name: 'recordType',  // Ermöglicht Single-Table-Design-Patterns
        type: AttributeType.STRING,
      },
      billingMode: BillingMode.PAY_PER_REQUEST,  // Keine Provisioning-Raterei
      encryption: TableEncryption.AWS_MANAGED,
      // IMMER Point-in-Time-Recovery in Produktion aktivieren
      pointInTimeRecovery: props.stage === 'prod' ? true : false,
      // NIEMALS versehentlich Produktionsdaten löschen
      removalPolicy: props.stage === 'prod' ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
      // Streams ermöglichen Echtzeit-Verarbeitung und Audit-Trails
      stream: props.enableStreams ? StreamViewType.NEW_AND_OLD_IMAGES : undefined,
    });
    
    // GSI für E-Mail-basierte Lookups (haben gelernt, dass das für Auth kritisch ist)
    this.addGlobalSecondaryIndex({
      indexName: 'EmailLookupIndex',
      partitionKey: {
        name: 'email',
        type: AttributeType.STRING,
      },
      sortKey: {
        name: 'recordType',
        type: AttributeType.STRING,
      },
      projectionType: ProjectionType.KEYS_ONLY,  // Kosten minimieren
    });
    
    // GSI für zeitbasierte Abfragen (Benutzeraktivität, Reporting)
    this.addGlobalSecondaryIndex({
      indexName: 'TimeSeriesIndex',
      partitionKey: {
        name: 'entityType',
        type: AttributeType.STRING,
      },
      sortKey: {
        name: 'timestamp',
        type: AttributeType.STRING,
      },
      projectionType: ProjectionType.KEYS_ONLY,
    });
    
    // Produktions-Monitoring (auf die harte Tour gelernt)
    this.createProductionAlarms(props.stage);
    
    // Kostenverfolgung-Tags
    Tags.of(this).add('Service', 'my-service');
    Tags.of(this).add('Stage', props.stage);
    Tags.of(this).add('CostCenter', 'platform');
    Tags.of(this).add('DataClassification', 'sensitive');
  }
  
  private createProductionAlarms(stage: string) {
    if (stage !== 'prod') return;
    
    // Throttle-Alarm - jegliches Throttling ist schlecht
    new Alarm(this, 'ThrottleAlarm', {
      metric: new Metric({
        namespace: 'AWS/DynamoDB',
        metricName: 'UserErrorEvents',
        dimensionsMap: {
          TableName: this.tableName,
        },
        statistic: 'Sum',
        period: Duration.minutes(5),
      }),
      threshold: 1,
      evaluationPeriods: 1,
      treatMissingData: TreatMissingData.NOT_BREACHING,
      alarmDescription: 'DynamoDB table is experiencing throttling',
    });
    
    // Fehlerrate-Alarm
    new Alarm(this, 'ErrorRateAlarm', {
      metric: new Metric({
        namespace: 'AWS/DynamoDB',
        metricName: 'SystemErrorEvents',
        dimensionsMap: {
          TableName: this.tableName,
        },
        statistic: 'Sum',
        period: Duration.minutes(5),
      }),
      threshold: 5,
      evaluationPeriods: 2,
      alarmDescription: 'DynamoDB table experiencing system errors',
    });
  }
}

Die Environment Variable Hölle#

Während Woche 6 entdeckten wir, dass unsere Production API stillschweigend bei der Authentifizierung versagte. Debug-Logs zeigten zufällige "undefined" Werte in der JWT-Validierung. Der Schuldige? Unsere CDK-Migration hatte Serverless Frameworks ${env:SECRET_KEY} Referenzen in literale Strings umgewandelt.

Auswirkung: 12 Stunden intermittierende Authentifizierungsfehler, die 8.000+ Benutzer betrafen, bevor wir es im Monitoring bemerkten.

Grundursache: Environment Variable Mismanagement während der Migration. Serverless Frameworks String-Interpolation ist täuschend anders als CDKs Environment-Handling.

Production-Grade Environment Management#

Nach diesem Vorfall haben wir ein type-safe Environment-System gebaut, das stillschweigende Fehler verhindert:

TypeScript
// lib/config/production-environment.ts
export interface ProductionEnvironmentVariables {
  // Core Application Config - NIEMALS undefined in Production
  SERVICE_NAME: string;
  STAGE: string;
  REGION: string;
  VERSION: string;
  ENVIRONMENT: 'development' | 'staging' | 'production';
  
  // Feature Flags mit Defaults
  ENABLE_CACHE: 'true' | 'false';
  ENABLE_DEBUG_LOGGING: 'true' | 'false';
  ENABLE_METRICS: 'true' | 'false';
  
  // Performance Tuning
  CACHE_TTL_SECONDS: string;
  MAX_RETRY_ATTEMPTS: string;
  REQUEST_TIMEOUT_MS: string;
  
  // Database Referenzen (Tabellennamen, niemals ARNs in env vars)
  USERS_TABLE: string;
  ORDERS_TABLE: string;
  AUDIT_LOG_TABLE: string;
  
  // Secret ARNs (echte Secrets zur Laufzeit abrufen)
  JWT_SECRET_ARN: string;
  DATABASE_CREDENTIALS_ARN: string;
  THIRD_PARTY_API_KEYS_ARN: string;
  
  // External Service Konfiguration
  STRIPE_WEBHOOK_ENDPOINT: string;
  SENDGRID_FROM_EMAIL: string;
  
  // Monitoring und Observability
  SENTRY_DSN?: string;
  DATADOG_API_KEY_ARN?: string;
  LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error';
  
  // Business Logic Konfiguration
  MAX_FILE_UPLOAD_SIZE_MB: string;
  SESSION_TIMEOUT_MINUTES: string;
  RATE_LIMIT_PER_MINUTE: string;
}

export class ProductionEnvironmentBuilder {
  private vars: Partial<ProductionEnvironmentVariables> = {};
  private requiredVars: Set<keyof ProductionEnvironmentVariables> = new Set();
  
  constructor(private stage: string, private region: string, private version: string) {
    // Core Variables setzen, die immer erforderlich sind
    this.vars.STAGE = stage;
    this.vars.REGION = region;
    this.vars.VERSION = version;
    this.vars.ENVIRONMENT = this.mapStageToEnvironment(stage);
    
    // Core Variables als erforderlich markieren
    this.requiredVars.add('SERVICE_NAME');
    this.requiredVars.add('STAGE');
    this.requiredVars.add('REGION');
    this.requiredVars.add('VERSION');
  }
  
  private mapStageToEnvironment(stage: string): 'development' | 'staging' | 'production' {
    switch (stage) {
      case 'prod':
      case 'production':
        return 'production';
      case 'staging':
      case 'stage':
        return 'staging';
      default:
        return 'development';
    }
  }
  
  addServiceName(serviceName: string): this {
    this.vars.SERVICE_NAME = serviceName;
    return this;
  }
  
  addTable(key: keyof ProductionEnvironmentVariables, table: ITable): this {
    this.vars[key] = table.tableName;
    this.requiredVars.add(key);
    return this;
  }
  
  addSecret(key: keyof ProductionEnvironmentVariables, secret: ISecret): this {
    this.vars[key] = secret.secretArn;
    this.requiredVars.add(key);
    return this;
  }
  
  addFeatureFlag(key: keyof ProductionEnvironmentVariables, enabled: boolean): this {
    this.vars[key] = enabled ? 'true' : 'false' as any;
    return this;
  }
  
  addConfig(config: Partial<ProductionEnvironmentVariables>): this {
    Object.assign(this.vars, config);
    return this;
  }
  
  markRequired(key: keyof ProductionEnvironmentVariables): this {
    this.requiredVars.add(key);
    return this;
  }
  
  build(): Record<string, string> {
    // Alle erforderlichen Variablen validieren
    const missing = Array.from(this.requiredVars).filter(key => 
      this.vars[key] === undefined || this.vars[key] === ''
    );
    
    if (missing.length > 0) {
      throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
    }
    
    // Stage-spezifische Defaults setzen
    const defaults = this.getStageDefaults();
    const merged = { ...defaults, ...this.vars };
    
    // In String Record konvertieren, undefined Values filtern
    return Object.entries(merged)
      .filter(([_, value]) => value !== undefined && value !== '')
      .reduce((acc, [key, value]) => ({
        ...acc,
        [key]: String(value),
      }), {});
  }
  
  private getStageDefaults(): Partial<ProductionEnvironmentVariables> {
    const isProd = this.vars.ENVIRONMENT === 'production';
    
    return {
      // Konservative Defaults für Production, aggressive für Dev
      ENABLE_CACHE: isProd ? 'true' : 'false',
      ENABLE_DEBUG_LOGGING: isProd ? 'false' : 'true',
      ENABLE_METRICS: isProd ? 'true' : 'false',
      CACHE_TTL_SECONDS: isProd ? '300' : '60',
      MAX_RETRY_ATTEMPTS: isProd ? '3' : '1',
      REQUEST_TIMEOUT_MS: isProd ? '30000' : '10000',
      LOG_LEVEL: isProd ? 'info' : 'debug',
      MAX_FILE_UPLOAD_SIZE_MB: '10',
      SESSION_TIMEOUT_MINUTES: '60',
      RATE_LIMIT_PER_MINUTE: isProd ? '100' : '1000',
    };
  }
}

Environment Builder verwenden#

TypeScript
// lib/stacks/api-stack.ts
const envBuilder = new EnvironmentBuilder(config.stage, config.region)
  .addTable('USERS_TABLE', usersTable)
  .addTable('ORDERS_TABLE', ordersTable)
  .addConfig({
    SERVICE_NAME: 'my-service',
    ENABLE_CACHE: config.stage === 'prod' ? 'true' : 'false',
    CACHE_TTL: '300',
  });

// Zu Lambda Function hinzufügen
const createUserFn = new ServerlessFunction(this, 'CreateUserFunction', {
  entry: 'src/handlers/users.ts',
  handler: 'create',
  config,
  environment: envBuilder.build(),
});

Der $23K Stripe API Key Vorfall#

Woche 8. Unsere Staging-Umgebung war versehentlich auf Production Stripe Keys durch eine falsch konfigurierte Environment Variable ausgerichtet. Wir verarbeiteten 47 Test-Transaktionen mit insgesamt $23.247, bevor wir den Fehler bemerkten.

Auswirkung: Manueller Rückerstattungsprozess, unangenehme Kundenkommunikation und ein CFO-Meeting über "bessere Kontrollen bei Finanzintegrationen".

Grundursache: Secrets als Klartext Environment Variables gespeichert ohne umgebungsspezifische Validierung.

Bulletproof Secrets Management#

Nach einer $23K Lektion in Secrets Management ist hier unser production-grade Ansatz:

TypeScript
// lib/constructs/secure-function.ts
import { Secret, ISecret } from 'aws-cdk-lib/aws-secretsmanager';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';

export interface SecureFunctionProps extends ServerlessFunctionProps {
  secrets?: Record<string, ISecret>;
}

export class SecureFunction extends ServerlessFunction {
  constructor(scope: Construct, id: string, props: SecureFunctionProps) {
    const { secrets = {}, ...functionProps } = props;
    
    // Secret ARNs als Environment Variables übergeben
    const secretEnvVars = Object.entries(secrets).reduce(
      (acc, [key, secret]) => ({
        ...acc,
        [`${key}_SECRET_ARN`]: secret.secretArn,
      }),
      {}
    );
    
    super(scope, id, {
      ...functionProps,
      environment: {
        ...functionProps.environment,
        ...secretEnvVars,
      },
    });
    
    // Read-Berechtigungen für alle Secrets gewähren
    Object.values(secrets).forEach(secret => {
      secret.grantRead(this);
    });
  }
}

// Verwendung
const apiKeySecret = new Secret(this, 'ApiKeySecret', {
  secretName: `/${config.stage}/my-service/api-keys`,
  generateSecretString: {
    secretStringTemplate: JSON.stringify({}),
    generateStringKey: 'sendgrid',
    excludeCharacters: ' %+~`#$&*()|[]{}:;<>?!\'/@"\\',
  },
});

const emailFunction = new SecureFunction(this, 'EmailFunction', {
  entry: 'src/handlers/email.ts',
  handler: 'send',
  config,
  secrets: {
    API_KEYS: apiKeySecret,
  },
});

Runtime Secret Access#

TypeScript
// src/libs/secrets.ts
import { 
  SecretsManagerClient, 
  GetSecretValueCommand 
} from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({});
const cache = new Map<string, any>();

export async function getSecret<T = any>(
  secretArn: string,
  jsonKey?: string
): Promise<T> {
  const cacheKey = `${secretArn}:${jsonKey || 'full'}`;
  
  if (cache.has(cacheKey)) {
    return cache.get(cacheKey);
  }
  
  try {
    const response = await client.send(
      new GetSecretValueCommand({ SecretId: secretArn })
    );
    
    const secret = JSON.parse(response.SecretString || '{}');
    const value = jsonKey ? secret[jsonKey] : secret;
    
    cache.set(cacheKey, value);
    return value;
  } catch (error) {
    console.error('Failed to retrieve secret:', error);
    throw new Error('Secret retrieval failed');
  }
}

// Verwendung im Handler
export const handler = async (event: APIGatewayProxyEventV2) => {
  const secretArn = process.env.API_KEYS_SECRET_ARN;
  const sendgridKey = await getSecret<string>(secretArn!, 'sendgrid');
  
  // Das Secret verwenden
  await sendEmail(sendgridKey, event.body);
};

Parameter Store Integration#

Für nicht-sensitive Konfiguration:

TypeScript
// lib/constructs/parameter-store.ts
import { StringParameter, IParameter } from 'aws-cdk-lib/aws-ssm';

export class ServiceParameters extends Construct {
  public readonly configs: Map<string, IParameter> = new Map();
  
  constructor(scope: Construct, id: string, props: {
    service: string;
    stage: string;
    parameters: Record<string, string>;
  }) {
    super(scope, id);
    
    // Parameter erstellen
    Object.entries(props.parameters).forEach(([key, value]) => {
      const param = new StringParameter(this, key, {
        parameterName: `/${props.service}/${props.stage}/${key}`,
        stringValue: value,
        description: `${key} for ${props.service} ${props.stage}`,
      });
      
      this.configs.set(key, param);
    });
  }
  
  grantRead(grantable: IGrantable) {
    this.configs.forEach(param => {
      param.grantRead(grantable);
    });
  }
  
  toEnvironment(): Record<string, string> {
    const env: Record<string, string> = {};
    this.configs.forEach((param, key) => {
      env[`${key}_PARAM`] = param.parameterName;
    });
    return env;
  }
}

VPC-Konfiguration für RDS/ElastiCache#

VPC-fähige Lambda Functions erstellen#

TypeScript
// lib/constructs/vpc-config.ts
import { Vpc, SubnetType, SecurityGroup, Port } from 'aws-cdk-lib/aws-ec2';
import { DatabaseInstance, DatabaseInstanceEngine } from 'aws-cdk-lib/aws-rds';

export class VpcResources extends Construct {
  public readonly vpc: Vpc;
  public readonly lambdaSecurityGroup: SecurityGroup;
  public readonly databaseSecurityGroup: SecurityGroup;
  public readonly database?: DatabaseInstance;
  
  constructor(scope: Construct, id: string, props: {
    stage: string;
    enableDatabase?: boolean;
  }) {
    super(scope, id);
    
    // VPC erstellen
    this.vpc = new Vpc(this, 'Vpc', {
      vpcName: `my-service-${props.stage}`,
      maxAzs: 2,
      natGateways: props.stage === 'prod' ? 2 : 1,
      subnetConfiguration: [
        {
          name: 'Public',
          subnetType: SubnetType.PUBLIC,
          cidrMask: 24,
        },
        {
          name: 'Private',
          subnetType: SubnetType.PRIVATE_WITH_EGRESS,
          cidrMask: 24,
        },
        {
          name: 'Isolated',
          subnetType: SubnetType.PRIVATE_ISOLATED,
          cidrMask: 24,
        },
      ],
    });
    
    // Security Groups
    this.lambdaSecurityGroup = new SecurityGroup(this, 'LambdaSG', {
      vpc: this.vpc,
      description: 'Security group for Lambda functions',
      allowAllOutbound: true,
    });
    
    this.databaseSecurityGroup = new SecurityGroup(this, 'DatabaseSG', {
      vpc: this.vpc,
      description: 'Security group for RDS database',
      allowAllOutbound: false,
    });
    
    // Lambda erlauben, sich mit Database zu verbinden
    this.databaseSecurityGroup.addIngressRule(
      this.lambdaSecurityGroup,
      Port.tcp(5432),
      'Allow Lambda functions'
    );
    
    if (props.enableDatabase) {
      this.createDatabase(props.stage);
    }
  }
  
  private createDatabase(stage: string) {
    this.database = new DatabaseInstance(this, 'Database', {
      databaseName: 'myservice',
      engine: DatabaseInstanceEngine.postgres({
        version: PostgresEngineVersion.VER_15_2,
      }),
      vpc: this.vpc,
      vpcSubnets: {
        subnetType: SubnetType.PRIVATE_ISOLATED,
      },
      securityGroups: [this.databaseSecurityGroup],
      allocatedStorage: stage === 'prod' ? 100 : 20,
      instanceType: InstanceType.of(
        InstanceClass.T3,
        stage === 'prod' ? InstanceSize.MEDIUM : InstanceSize.MICRO
      ),
      multiAz: stage === 'prod',
      deletionProtection: stage === 'prod',
      backupRetention: Duration.days(stage === 'prod' ? 30 : 7),
    });
  }
}

VPC-fähige Lambda Function#

TypeScript
// lib/constructs/vpc-lambda.ts
export class VpcLambdaFunction extends ServerlessFunction {
  constructor(scope: Construct, id: string, props: ServerlessFunctionProps & {
    vpcResources: VpcResources;
    databaseSecret?: ISecret;
  }) {
    const { vpcResources, databaseSecret, ...functionProps } = props;
    
    super(scope, id, {
      ...functionProps,
      vpc: vpcResources.vpc,
      vpcSubnets: {
        subnetType: SubnetType.PRIVATE_WITH_EGRESS,
      },
      securityGroups: [vpcResources.lambdaSecurityGroup],
      environment: {
        ...functionProps.environment,
        ...(databaseSecret && {
          DB_SECRET_ARN: databaseSecret.secretArn,
        }),
      },
    });
    
    // Database-Zugriff gewähren
    if (databaseSecret) {
      databaseSecret.grantRead(this);
    }
  }
}

Database Connection Management#

TypeScript
// src/libs/database.ts
import { Client } from 'pg';
import { getSecret } from './secrets';

let client: Client | null = null;

export async function getDbClient(): Promise<Client> {
  if (client && !client.ended) {
    return client;
  }
  
  const secretArn = process.env.DB_SECRET_ARN;
  if (!secretArn) {
    throw new Error('Database secret not configured');
  }
  
  const credentials = await getSecret<{
    username: string;
    password: string;
    host: string;
    port: number;
    dbname: string;
  }>(secretArn);
  
  client = new Client({
    user: credentials.username,
    password: credentials.password,
    host: credentials.host,
    port: credentials.port,
    database: credentials.dbname,
    ssl: {
      rejectUnauthorized: false,
    },
    connectionTimeoutMillis: 10000,
  });
  
  await client.connect();
  return client;
}

// Aufräumen beim Lambda Container Shutdown
process.on('SIGTERM', async () => {
  if (client && !client.ended) {
    await client.end();
  }
});

Backup und Disaster Recovery#

Automatisierte DynamoDB Backups#

TypeScript
// lib/constructs/backup-plan.ts
import { BackupPlan, BackupResource } from 'aws-cdk-lib/aws-backup';
import { Schedule } from 'aws-cdk-lib/aws-events';

export class TableBackupPlan extends Construct {
  constructor(scope: Construct, id: string, props: {
    tables: ITable[];
    stage: string;
  }) {
    super(scope, id);
    
    const plan = new BackupPlan(this, 'BackupPlan', {
      backupPlanName: `my-service-${props.stage}-backup`,
      backupPlanRules: [
        {
          ruleName: 'DailyBackups',
          scheduleExpression: Schedule.cron({
            hour: '3',
            minute: '0',
          }),
          startWindow: Duration.hours(1),
          completionWindow: Duration.hours(2),
          deleteAfter: Duration.days(
            props.stage === 'prod' ? 30 : 7
          ),
        },
      ],
    });
    
    plan.addSelection('TableSelection', {
      resources: props.tables.map(table => 
        BackupResource.fromDynamoDbTable(table)
      ),
    });
  }
}

Migration Best Practices#

1. Stateful Resource Strategie#

TypeScript
// lib/stacks/stateful-stack.ts
export class StatefulStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, {
      ...props,
      // Versehentliches Löschen verhindern
      terminationProtection: true,
    });
    
    // Alle stateful Resources in einem Stack
    const tables = this.createTables();
    const secrets = this.createSecrets();
    const parameters = this.createParameters();
    
    // Für Verwendung in anderen Stacks exportieren
    tables.forEach((table, name) => {
      new CfnOutput(this, `${name}TableName`, {
        value: table.tableName,
        exportName: `${this.stackName}-${name}TableName`,
      });
    });
  }
}

2. Zero-Downtime Migration Checkliste#

  • Bestehende Tabellen mit fromTableAttributes importieren
  • Berechtigungen mit importierten Ressourcen testen
  • Dual-Write Pattern implementieren wenn Table Schema ändert
  • Lambda Environment Variables für graduelles Rollout verwenden
  • CloudWatch Alarms vor dem Wechsel einrichten
  • Circuit Breaker für externe Services implementieren
  • Rollback-Verfahren testen

Harte Lektionen aus der Produktion#

Nach 6 Monaten Verwaltung stateful Infrastructure in CDK sind hier die Lektionen, die uns vor Katastrophen retteten:

1. Imports immer zuerst in Staging testen#

Kosten des Lernens: 4 Stunden Downtime, Notfall-Recovery, unangenehme Meetings. Vorbeugung: Niemals cdk deploy gegen Production-Tabellen ohne Probe in Staging mit identischen Daten.

2. Environment Variables sind keine Konfiguration#

Kosten des Lernens: 12 Stunden Authentifizierungsfehler, die 8K+ Benutzer betrafen. Vorbeugung: Type-safe Environment Builder mit Validierung und Required Field Checks.

3. Secrets brauchen umgebungsspezifische Validierung#

Kosten des Lernens: $23K versehentliche Production-Gebühren. Vorbeugung: Umgebungsbasierte Secret-Validierung, die Cross-Environment-Kontamination verhindert.

4. Data Migration braucht Monitoring#

Kosten des Lernens: 3 Migrationsversuche, 180K Datensätze in Gefahr. Vorbeugung: Umfassendes Logging, Progress Tracking und Timeout Handling in Migration Functions.

5. VPC Lambda Functions sind anders#

Kosten des Lernens: 15-Minuten Cold Starts, Connection Pool Erschöpfung. Vorbeugung: Ordnungsgemäßes Connection Management, Security Group Konfiguration und Subnet Planung.

Migration Ergebnisse#

Vor CDK:

  • Manuelles Environment Management
  • Klartext-Secrets in YAML
  • Keine Table Import Validierung
  • Migration Scripts lokal ausgeführt
  • Null Disaster Recovery Tests

Nach CDK:

  • Type-safe Environment Konfiguration mit Validierung
  • Verschlüsselte Secrets mit Environment Isolation
  • Production-sichere Table Imports mit Verifikation
  • Automatisierte, überwachte Data Migrations
  • Umfassendes Backup und Monitoring

Metriken:

  • 23 Lambda Functions migriert
  • 3 Production DynamoDB Tabellen (180K+ Datensätze)
  • 47 Environment Variables type-safe
  • 12 Secrets ordnungsgemäß verschlüsselt
  • Null Datenverlust (schließlich)

Was kommt als Nächstes#

Dein Data Layer ist kampferprobt mit ordnungsgemäßem Umgebungsmanagement und Sicherheit. Stateful-Ressourcen sind geschützt, Secrets sind verschlüsselt und Disaster Recovery ist automatisiert.

In Teil 5 werden wir Authentifizierung und Autorisierung implementieren:

  • Cognito-Benutzerpools mit Produktionsbeschränkungen
  • API Gateway-Authorizer, die tatsächlich funktionieren
  • IAM-Rollen, die das Principle of Least Privilege befolgen
  • JWT-Token-Validierung, die nicht bricht
  • Feinabgestimmte Berechtigungen ohne Komplexitätsexplosion

Die Foundation hat die Produktion überlebt. Lass uns sie ordnungsgemäß sichern.

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