2025-09-04
Serverless Framework'ten AWS CDK'ya Geçiş: Bölüm 4 - EventBridge ve SQS
Event-driven mimarileri CDK'ya taşıma. EventBridge, SQS, SNS entegrasyonları ve pattern'ler.
Stateful infrastructure’ı CDK’ya migrate etmek, Lambda fonksiyonlarını taşımaktan çok daha risklidir. Yanlış bir adım tablo silmesine, veri kaybına veya sessiz authentication başarısızlıklarına yol açabilir. Bu yazı, 23 Lambda fonksiyonunu ve üç production DynamoDB tablosunu (180K+ kayıt) sıfır veri kaybıyla CDK yönetimine taşımanın teknik pattern’lerini ele alıyor.
Seri Navigasyonu:
- Bölüm 1: Neden Geçiş Yapalım?
- Bölüm 2: CDK Environment Kurulumu
- Bölüm 3: Lambda Fonksiyonları ve API Gateway Migration
- Bölüm 4: Database ve Environment Management (bu yazı)
- Bölüm 5: Authentication, Authorization ve IAM
- Bölüm 6: Migration Stratejileri ve Best Practice’ler
Data Migration: Kritik Riskler
CDK’ya import işlemi sezgisel görünür ama ciddi bir tuzak barındırır.
Table Import Tuzağı
Production tablolarını CDK yönetimine taşırken cdk deploy komutu başarıyla tamamlanabilir ve monitoring yeşil kalabilir; ama aynı tabloyu Serverless Framework CloudFormation template’i de yönetiyorsa, CDK mevcut kaynağı “çakışma” olarak yorumlayıp silebilir ve yenisini oluşturabilir.
Kök neden: CDK tabloyu “import” etmeye çalışır ama mevcut CloudFormation template’ini çakışan olarak yorumlar; kaynağı siler ve yenisini oluşturur.
Sonuç: Sıfır veri kaybı hedefine ulaşmak için downtime, acil point-in-time recovery ve ekstra deployment adımları gerekir.
Kural: Production’da explicit retention policy’ler ve staging’de rehearsal olmadan import’lara asla güvenmeyin.
Gerçekten İşe Yarayan DynamoDB Migration Stratejileri
Güvenli Table Import Pattern’i
Production migration’ları için battle-tested yaklaşım:
# serverless.yml - Orijinal tablo tanımı
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
Mevcut tablolar için CDK yaklaşımı:
// 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;
// Kritik: Import etmeden önce tablonun var olduğunu doğrula
requireExistingTable: boolean;
}
export class ProductionTableImport extends Construct {
public readonly table: ITable;
constructor(scope: Construct, id: string, props: ProductionTableImportProps) {
super(scope, id);
if (props.requireExistingTable) {
// İlk olarak, tablonun gerçekten var olduğunu doğrula
const verifyFn = new NodejsFunction(this, 'VerifyTableExists', {
entry: 'src/migrations/verify-table.ts',
handler: 'handler',
timeout: Duration.seconds(30),
environment: {
TABLE_NAME: props.tableName,
},
});
// Tablo yoksa deployment'ı başarısız kılan custom resource
new CustomResource(this, 'TableVerification', {
serviceToken: verifyFn.functionArn,
properties: {
TableName: props.tableName,
},
});
}
// Sadece verification başarılı olduktan sonra import et
this.table = Table.fromTableAttributes(this, 'ImportedTable', {
tableName: props.tableName,
region: props.region,
account: props.account,
// KRİTİK: Bu CDK'nın tablo lifecycle'ını yönetmeye çalışmasını önler
tableStreamArn: undefined,
});
}
}
// src/migrations/verify-table.ts - Kazara tablo silme işlemini önler
import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({});
export const handler = async (event: any) => {
const tableName = event.ResourceProperties.TableName;
try {
// Tablonun var olduğunu ve ACTIVE olduğunu doğrula
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})`);
}
// Audit trail için kritik tablo bilgilerini logla
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'ını başarısız kıl
}
};
// Production güvenlik kontrolleri ile kullanım
const usersTable = new ProductionTableImport(this, 'UsersTable', {
tableName: `my-service-${config.stage}-users`,
requireExistingTable: config.stage === 'prod', // Sadece production'da doğrula
}).table;
// Normal şekilde izin ver
usersTable.grantReadWriteData(createUserFn);
Production-Grade Table Pattern’i
Sağlam tablo oluşturma pattern’i şu özellikleri kapsar:
// 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, {
// Blue-green deployment'lar için versiyonlu tablo adları
tableName: `my-service-${props.stage}-users-v3`,
partitionKey: {
name: 'userId',
type: AttributeType.STRING,
},
sortKey: {
name: 'recordType', // Single-table design pattern'lerini etkinleştirir
type: AttributeType.STRING,
},
billingMode: BillingMode.PAY_PER_REQUEST, // Provisioning tahmini yok
encryption: TableEncryption.AWS_MANAGED,
// Production'da HER ZAMAN point-in-time recovery etkinleştir
pointInTimeRecovery: props.stage === 'prod' ? true : false,
// Production verisini ASLA kazara silme
removalPolicy: props.stage === 'prod' ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
// Stream'ler real-time processing ve audit trail'leri etkinleştirir
stream: props.enableStreams ? StreamViewType.NEW_AND_OLD_IMAGES : undefined,
});
// Email-based lookup'lar için GSI (auth için kritik)
this.addGlobalSecondaryIndex({
indexName: 'EmailLookupIndex',
partitionKey: {
name: 'email',
type: AttributeType.STRING,
},
sortKey: {
name: 'recordType',
type: AttributeType.STRING,
},
projectionType: ProjectionType.KEYS_ONLY, // Maliyetleri minimize et
});
// Time-based sorgular için GSI (user activity, reporting)
this.addGlobalSecondaryIndex({
indexName: 'TimeSeriesIndex',
partitionKey: {
name: 'entityType',
type: AttributeType.STRING,
},
sortKey: {
name: 'timestamp',
type: AttributeType.STRING,
},
projectionType: ProjectionType.KEYS_ONLY,
});
// Production monitoring
this.createProductionAlarms(props.stage);
// Cost tracking tag'leri
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 - herhangi bir throttling kötü
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',
});
// Error rate 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',
});
}
}
Zero-Downtime Data Migration
Büyük hacimli kayıtları servis kesintisi olmadan taşımak için bulletproof migration stratejisi gerekir. Aşağıdaki pattern bu amaca hizmet eder:
// lib/constructs/production-table-migrator.ts
import { CustomResource, Duration } from 'aws-cdk-lib';
import { Provider } from 'aws-cdk-lib/custom-resources';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
export class ProductionTableMigrator extends Construct {
constructor(scope: Construct, id: string, props: {
sourceTable: ITable;
targetTable: ITable;
batchSize?: number;
enableDualWrite?: boolean;
}) {
super(scope, id);
// Production ayarları ile migration fonksiyonu
const migrationFn = new NodejsFunction(this, 'MigrationFunction', {
entry: 'src/migrations/production-table-migrator.ts',
handler: 'handler',
timeout: Duration.minutes(15),
memorySize: 3008, // En hızlı processing için max memory
reservedConcurrentExecutions: 5, // Diğer fonksiyonlara etkiyi sınırla
logRetention: RetentionDays.ONE_MONTH, // Migration log'larını sakla
environment: {
SOURCE_TABLE: props.sourceTable.tableName,
TARGET_TABLE: props.targetTable.tableName,
BATCH_SIZE: String(props.batchSize || 25), // DynamoDB batch limiti
ENABLE_DUAL_WRITE: String(props.enableDualWrite || false),
// Migration tracking
MIGRATION_ID: `migration-${Date.now()}`,
},
});
// Migration için kapsamlı izinler
props.sourceTable.grantFullAccess(migrationFn); // Scan/read gerekli
props.targetTable.grantFullAccess(migrationFn); // Write/verify gerekli
// Düzgün error handling ile custom resource oluştur
const provider = new Provider(this, 'Provider', {
onEventHandler: migrationFn,
logRetention: RetentionDays.ONE_MONTH,
});
new CustomResource(this, 'DataMigration', {
serviceToken: provider.serviceToken,
properties: {
SourceTable: props.sourceTable.tableName,
TargetTable: props.targetTable.tableName,
MigrationId: `migration-${Date.now()}`,
// Sadece tablolar değiştiğinde force update
TableFingerprint: this.generateTableFingerprint(props),
},
});
}
private generateTableFingerprint(props: {
sourceTable: ITable;
targetTable: ITable;
}): string {
// Tablo özelliklerine dayalı unique fingerprint oluştur
return Buffer.from(
`${props.sourceTable.tableName}-${props.targetTable.tableName}`
).toString('base64');
}
}
// src/migrations/production-table-migrator.ts
import {
DynamoDBClient,
ScanCommand,
BatchWriteItemCommand,
DescribeTableCommand,
} from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
const client = new DynamoDBClient({
maxAttempts: 5, // Başarısız request'leri retry et
requestHandler: {
connectionTimeout: 2000,
requestTimeout: 30000,
},
});
export const handler = async (event: any) => {
const { RequestType, ResourceProperties } = event;
const { SourceTable, TargetTable, MigrationId } = ResourceProperties;
console.log('Migration event:', { RequestType, SourceTable, TargetTable, MigrationId });
try {
if (RequestType === 'Create' || RequestType === 'Update') {
await migrateTableData(SourceTable, TargetTable, MigrationId);
}
return {
PhysicalResourceId: `migration-${SourceTable}-to-${TargetTable}`,
Data: {
Status: 'Success',
MigrationId,
},
};
} catch (error) {
console.error('Migration failed:', error);
throw error; // CloudFormation deployment'ını başarısız kıl
}
};
async function migrateTableData(sourceTable: string, targetTable: string, migrationId: string) {
console.log(`Starting migration: ${sourceTable} -> ${targetTable}`);
// İlk olarak, her iki tablonun da var olduğunu ve aktif olduğunu doğrula
await verifyTableState(sourceTable);
await verifyTableState(targetTable);
let lastEvaluatedKey: any = undefined;
let totalItems = 0;
let batchCount = 0;
const batchSize = parseInt(process.env.BATCH_SIZE || '25');
do {
// Kaynak tabloyu scan et
const scanResult = await client.send(new ScanCommand({
TableName: sourceTable,
Limit: batchSize,
ExclusiveStartKey: lastEvaluatedKey,
}));
if (scanResult.Items && scanResult.Items.length > 0) {
// Hedef tabloya batch write hazırla
const writeRequests = scanResult.Items.map(item => ({
PutRequest: { Item: item },
}));
// Hedef tabloya batch yaz
await client.send(new BatchWriteItemCommand({
RequestItems: {
[targetTable]: writeRequests,
},
}));
totalItems += scanResult.Items.length;
batchCount++;
console.log(`Migrated batch ${batchCount}: ${scanResult.Items.length} items (total: ${totalItems})`);
}
lastEvaluatedKey = scanResult.LastEvaluatedKey;
// Lambda timeout'unu önle - limite yaklaşırsa dur
if (process.env.AWS_EXECUTION_ENV && Date.now() > parseInt(process.env.LAMBDA_START_TIME || '0') + 840000) {
console.log('Approaching Lambda timeout, stopping migration');
break;
}
} while (lastEvaluatedKey);
console.log(`Migration completed: ${totalItems} items migrated in ${batchCount} batches`);
}
async function verifyTableState(tableName: string) {
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})`);
}
}
Environment Variable Yönetimi
CDK migration’ı sırasında yaygın bir tuzak: Serverless Framework’ün ${env:SECRET_KEY} referansları CDK’da literal string’lere dönüşebilir. Bu sessiz başarısızlıklara yol açar; JWT validation’da “undefined” değerleri görülür ama kaynak hemen belli olmaz.
Kök neden: Serverless Framework’ün string interpolation’ı CDK’nın environment handling’inden aldatıcı şekilde farklı davranır. Migration sırasında bu fark gözden kaçarsa production’da aralıklı authentication başarısızlıkları ortaya çıkar.
Production-Grade Environment Management
Sessiz başarısızlıkları önlemek için type-safe environment builder kullanımı önerilir:
// lib/config/production-environment.ts
export interface ProductionEnvironmentVariables {
// Core application config - Production'da ASLA undefined olmamalı
SERVICE_NAME: string;
STAGE: string;
REGION: string;
VERSION: string;
ENVIRONMENT: 'development' | 'staging' | 'production';
// Default'ları olan feature flag'ler
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 referansları (tablo adları, env var'larda ARN asla)
USERS_TABLE: string;
ORDERS_TABLE: string;
AUDIT_LOG_TABLE: string;
// Secret ARN'leri (gerçek secret'lar runtime'da alınır)
JWT_SECRET_ARN: string;
DATABASE_CREDENTIALS_ARN: string;
THIRD_PARTY_API_KEYS_ARN: string;
// External servis konfigürasyonu
STRIPE_WEBHOOK_ENDPOINT: string;
SENDGRID_FROM_EMAIL: string;
// Monitoring ve observability
SENTRY_DSN?: string;
DATADOG_API_KEY_ARN?: string;
LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error';
// Business logic konfigürasyonu
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) {
// Her zaman gerekli olan core variable'ları set et
this.vars.STAGE = stage;
this.vars.REGION = region;
this.vars.VERSION = version;
this.vars.ENVIRONMENT = this.mapStageToEnvironment(stage);
// Core variable'ları required olarak işaretle
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> {
// Tüm gerekli variable'ların mevcut olduğunu doğrula
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-specific default'ları set et
const defaults = this.getStageDefaults();
const merged = { ...defaults, ...this.vars };
// String record'a dönüştür, undefined değerleri filtrele
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 {
// Production için conservative, dev için aggressive default'lar
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 Kullanımı
// 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',
});
// Lambda fonksiyonuna ekle
const createUserFn = new ServerlessFunction(this, 'CreateUserFunction', {
entry: 'src/handlers/users.ts',
handler: 'create',
config,
environment: envBuilder.build(),
});
Secrets Management: Cross-Environment Riski
Staging environment’larının yanlış konfigürasyon nedeniyle production key’lerine yönlendirilmesi yaygın bir hata kaynağıdır. Environment-specific validation olmadan plaintext environment variable’lar olarak depolanan secret’lar bu riski taşır.
Kök neden: Secret’lar environment izolasyonu olmadan saklandığında staging ve production arasında cross-contamination oluşabilir.
Sağlam Secrets Management
Production-grade secrets yönetimi için önerilen yaklaşım:
// 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 ARN'lerini environment variable olarak geç
const secretEnvVars = Object.entries(secrets).reduce(
(acc, [key, secret]) => ({
...acc,
[`${key}_SECRET_ARN`]: secret.secretArn,
}),
{}
);
super(scope, id, {
...functionProps,
environment: {
...functionProps.environment,
...secretEnvVars,
},
});
// Tüm secret'lar için read izinleri ver
Object.values(secrets).forEach(secret => {
secret.grantRead(this);
});
}
}
// Kullanım
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');
}
}
// Handler'da kullanım
export const handler = async (event: APIGatewayProxyEventV2) => {
const secretArn = process.env.API_KEYS_SECRET_ARN;
const sendgridKey = await getSecret<string>(secretArn!, 'sendgrid');
// Secret'ı kullan
await sendEmail(sendgridKey, event.body);
};
Parameter Store Entegrasyonu
Hassas olmayan konfigürasyon için:
// 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'ları oluştur
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;
}
}
RDS/ElastiCache için VPC Konfigürasyonu
VPC-Enabled Lambda Fonksiyonları Oluşturma
// 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 oluştur
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 group'lar
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'nın database'e bağlanmasına izin ver
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-Enabled Lambda Fonksiyonu
// 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 erişimi ver
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;
}
// Lambda container shutdown'da temizlik
process.on('SIGTERM', async () => {
if (client && !client.ended) {
await client.end();
}
});
Backup ve Disaster Recovery
Otomatik DynamoDB Backup’ları
// 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 Practice’leri
1. Stateful Resource Stratejisi
// lib/stacks/stateful-stack.ts
export class StatefulStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, {
...props,
// Kazara silmeyi önle
terminationProtection: true,
});
// Tüm stateful kaynaklar bir stack'te
const tables = this.createTables();
const secrets = this.createSecrets();
const parameters = this.createParameters();
// Diğer stack'lerde kullanım için export et
tables.forEach((table, name) => {
new CfnOutput(this, `${name}TableName`, {
value: table.tableName,
exportName: `${this.stackName}-${name}TableName`,
});
});
}
}
2. Zero-Downtime Migration Checklist
-
fromTableAttributeskullanarak mevcut tabloları import et - Import edilmiş kaynaklarla izinleri test et
- Tablo schema’sı değişiyorsa dual-write pattern implement et
- Gradual rollout için Lambda environment variable’ları kullan
- Switch etmeden önce CloudWatch alarm’ları kur
- External servisler için circuit breaker’lar implement et
- Rollback prosedürlerini test et
Production Pattern’lerinden Dersler
CDK’da stateful infrastructure yönetiminden çıkan pratik dersler:
1. Import’ları Her Zaman İlk Olarak Staging’de Test Et
Risk: Staging rehearsal atlandığında production tablolarında beklenmedik silme gerçekleşebilir.
Önleme: Aynı data ile staging’de rehearsal yapmadan production tablolarına karşı asla cdk deploy çalıştırmayın.
2. Environment Variable’lar Konfigürasyon Değildir
Risk: Secret interpolation farklılıkları aralıklı authentication başarısızlıklarına yol açar. Önleme: Validation ve required field kontrolleri olan type-safe environment builder’lar kullanın.
3. Secret’ların Environment-Specific Validation’a İhtiyacı Var
Risk: Environment izolasyonu olmadan staging production key’lerine bağlanabilir. Önleme: Cross-environment contamination’ı önleyen environment-aware secret validation uygulayın.
4. Data Migration’ın Monitoring’e İhtiyacı Var
Risk: İzleme olmadan büyük batch migration’larda sessiz hatalar oluşabilir. Önleme: Migration fonksiyonlarında kapsamlı logging, progress tracking ve timeout handling kullanın.
5. VPC Lambda Fonksiyonları Farklıdır
Risk: Eksik connection management cold start artışlarına ve connection pool tükenmesine yol açar. Önleme: Düzgün connection management, security group konfigürasyonu ve subnet planlama yapın.
Migration Sonuçları
CDK Öncesi:
- Manuel environment management
- YAML’da plaintext secret’lar
- Tablo import validation yok
- Migration script’leri yerel olarak çalışıyor
- Sıfır disaster recovery testi
CDK Sonrası:
- Validation ile type-safe environment konfigürasyonu
- Environment izolasyonu olan şifrelenmiş secret’lar
- Verification ile production-safe tablo import’ları
- Otomatik, monitörlü data migration’ları
- Kapsamlı backup ve monitoring
Tipik Migration Ölçeği:
- 20-25 Lambda fonksiyonu
- Birden fazla production DynamoDB tablosu (100K+ kayıt)
- Onlarca environment variable type-safe
- Birden fazla secret şifrelenmiş
- Sıfır data kaybı hedefi
Sırada Ne Var
Data layer’ınız düzgün environment management ve güvenlik ile battle-tested. Stateful kaynaklar korunmuş, secret’lar şifrelenmiş ve disaster recovery otomatik.
5. Bölüm’de, authentication ve authorization implement edeceğiz:
- Production kısıtlamaları olan Cognito user pool’ları
- Gerçekten işe yarayan API Gateway authorizer’ları
- Least privilege’i takip eden IAM roller
- Bozulmayan JWT token validation
- Complexity explosion olmadan fine-grained izinler
Foundation production’u atlattı. Şimdi onu düzgün bir şekilde güvenlik altına alalım.
Kaynaklar
- DynamoDB ile tasarım için en iyi uygulamalar - DynamoDB için tablo tasarımı, tek tablo kalıpları ve güvenli CDK import stratejileri
- AWS Secrets Manager nedir? - Ortamlar arası veritabanı kimlik bilgileri ve API anahtarları için gizli bilgi yönetimi ve rotasyonu
- AWS CDK v2 Geliştirici Rehberi - Bu yazıda kullanılan DynamoDB, RDS, VPC ve Secrets Manager için CDK construct’ları
- IAM’de güvenlik en iyi uygulamaları - Veritabanlarına ve gizli bilgilere erişen Lambda fonksiyonları için en az ayrıcalık IAM politikaları
- TypeScript ile Lambda fonksiyonu oluşturma - TypeScript Lambda fonksiyonlarında environment variable yönetimi ve gizli bilgi alma kalıpları
- Amazon DynamoDB Dokümantasyonu - CDK’da DynamoDB tablo yapılandırması, GSI’lar ve point-in-time recovery ayarları için referans
Serverless Framework'ten AWS CDK'ya Geçiş Rehberi
Serverless Framework'ten AWS CDK'ya tam geçiş sürecini kapsayan 6 bölümlük kapsamlı rehber. Kurulum, uygulama pattern'leri ve best practice'ler dahil.
Serideki tüm yazılar
İlgili yazılar
AWS Secrets Manager ve Systems Manager Parameter Store'u karşılaştıran kapsamlı teknik rehber - hangi servisi ne zaman kullanmalı ve gerçek dünya implementation pattern'leri.
AWS AppSync ile ölçeklenebilir real-time API'ler geliştirmek için kapsamlı bir rehber: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.
Lambda Layers, VPC konfigürasyonu, cross-account execution ve kapsamlı maliyet optimizasyon stratejileri dahil advanced AWS Lambda pattern'lerinde ustalaşın. Production Lambda kullanımından gerçek dünya migration deneyimleri ve mimari kararlar.
Serverless Framework'ten AWS CDK'ya geçiş rehberinin ilk bölümü. Neden geçiş yapmalı, temel kavramlar ve ilk adımlar.
DynamoDB tabloları ve S3 bucket'larını CDK'ya taşıma. Data migration stratejileri ve en iyi uygulamalar.