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:
- Teil 1: Warum wechseln?
- Teil 2: CDK-Umgebung einrichten
- Teil 3: Migration von Lambda-Funktionen und API Gateway
- Teil 4: Datenbank- und Umgebungsmanagement (dieser Beitrag)
- Teil 5: Authentifizierung, Autorisierung und IAM
- Teil 6: Migrationsstrategien und Best Practices
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#
- 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:
# 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:
// 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:
// 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:
// 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#
// 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:
// 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#
// 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:
// 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#
// 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#
// 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#
// 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#
// 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#
// 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.
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!
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!