Skip to content
~/sph.sh

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.

CDK migration'ımızın 5. haftası. 23 Lambda fonksiyonunu başarıyla taşımıştık, ama gerçek zorluk Lead DevOps Mühendisimiz ayrıldığını duyurduğunda başladı. "Database migration ile iyi şanslar," dedi ve 2.8M$ ARR SaaS platformumuzu destekleyen üç DynamoDB tablo adının yazdığı yapışkan notu verdi.

O yapışkan not 4 yıllık customer verisini, 180K kullanıcıyı ve sıfır dokümantasyonlu backup prosedürünü temsil ediyordu. Bu, tek bir kayıt kaybetmeden stateful infrastructure migrate etme hikayesi - ve production sistemlerdeki data dependency'leri hakkında öğrenilen acı dersler.

Seri Navigasyonu:

Data Migration Felaketi (Neredeyse)

Teknik pattern'lere geçmeden önce, production tablolarımızı sadece "import" etmeye çalıştığımızda ne olduğunu paylaşayım.

Cuma Öğleden Sonra Table Import'u

15 Mart, 15:47. Ana user tablomuzu CDK yönetimine import etmek için güvenle cdk deploy komutunu çalıştırdım. Deployment başarılı oldu. Monitoring'imiz yeşil kaldı. Her şey mükemmel görünüyordu.

Pazartesi sabahı, 06:23. Slack bildirimleri patladı. User registration API'miz 500 hata veriyordu. Tablo gitmişti. Ulaşılmaz değil - tamamen silinmişti.

Kök neden: CDK tabloyu "import" etmeye çalıştı ama mevcut Serverless Framework CloudFormation template'ini çakışan olarak yorumladı. Yenisini oluşturmadan önce eski kaynağı sildi. Zero-downtime, zero-data oldu.

Etki: 4 saat downtime, acil point-in-time recovery ve deployment checklist'i "takip etme" hakkında çok rahatsız edici bir all-hands toplantısı.

Ders: 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 şimdi kullandığımız battle-tested yaklaşım:

yaml
# 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ı:

typescript
// lib/constructs/production-table-import.tsimport { 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 önlerimport { 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ımconst usersTable = new ProductionTableImport(this, 'UsersTable', {  tableName: `my-service-${config.stage}-users`,  requireExistingTable: config.stage === 'prod', // Sadece production'da doğrula}).table;
// Normal şekilde izin verusersTable.grantReadWriteData(createUserFn);

Production-Grade Table Pattern'i

180K kullanıcı ve 12M transaction yönettikten sonra, bulletproof tablo oluşturma pattern'imiz:

typescript
// lib/constructs/production-user-table.tsimport {   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 olduğunu öğrendik)    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 (zor yoldan öğrendik)    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

180K kullanıcı kaydını ve 4 yıllık transaction geçmişini servis kesintisi olmadan taşımak bulletproof migration stratejisi gerektiriyordu. İşe yarayan pattern:

typescript
// lib/constructs/production-table-migrator.tsimport { 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.tsimport {   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 Cehennemi

  1. hafta boyunca, production API'mizin sessizce authentication'da fail olduğunu keşfettik. Debug log'ları JWT validation'da rastgele "undefined" değerleri gösteriyordu. Suçlu? CDK migration'ımız Serverless Framework'ün ${env:SECRET_KEY} referanslarını literal string'lere dönüştürmüştü.

Etki: Yakalamamızdan önce 8.000+ kullanıcıyı etkileyen 12 saat aralıklı authentication başarısızlıkları.

Kök neden: Migration sırasında environment variable yanlış yönetimi. Serverless Framework'ün string interpolation'ı CDK'nın environment handling'inden aldatıcı şekilde farklı.

Production-Grade Environment Management

Bu incident'tan sonra, sessiz başarısızlıkları önleyen type-safe environment sistemi geliştirdik:

typescript
// lib/config/production-environment.tsexport 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ı

typescript
// lib/stacks/api-stack.tsconst 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 ekleconst createUserFn = new ServerlessFunction(this, 'CreateUserFunction', {  entry: 'src/handlers/users.ts',  handler: 'create',  config,  environment: envBuilder.build(),});

Kritik Stripe API Key Olayı

  1. hafta. Staging environment'ımız yanlış konfigüre edilmiş environment variable nedeniyle kazara production Stripe key'lerine yönlendirildiydi. Hatayı yakalamadan önce toplam 23.247 dolar tutarında 47 test transaction işledik.

Etki: Manuel refund süreci, rahatsız edici müşteri iletişimleri ve "finansal entegrasyonlar etrafında daha iyi kontroller" hakkında CFO toplantısı.

Kök neden: Environment-specific validation olmadan plaintext environment variable'lar olarak depolanan secret'lar.

Bulletproof Secrets Management

Secrets management'da 23K dolarlık bir ders ödedikten sonra, production-grade yaklaşımımız:

typescript
// lib/constructs/secure-function.tsimport { 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ımconst 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.tsimport {   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ımexport 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:

typescript
// lib/constructs/parameter-store.tsimport { 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

typescript
// lib/constructs/vpc-config.tsimport { 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

typescript
// lib/constructs/vpc-lambda.tsexport 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

typescript
// src/libs/database.tsimport { 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 temizlikprocess.on('SIGTERM', async () => {  if (client && !client.ended) {    await client.end();  }});

Backup ve Disaster Recovery

Otomatik DynamoDB Backup'ları

typescript
// lib/constructs/backup-plan.tsimport { 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

typescript
// lib/stacks/stateful-stack.tsexport 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

  • fromTableAttributes kullanarak 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'dan Zor Yoldan Öğrenilen Dersler

CDK'da stateful infrastructure'ı 6 ay yönettikten sonra, bizi felaketlerden kurtaran dersler:

1. Import'ları Her Zaman İlk Olarak Staging'de Test Et

Öğrenme maliyeti: 4 saat downtime, acil recovery, rahatsız edici toplantılar. Ö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

Öğrenme maliyeti: 8K+ kullanıcıyı etkileyen 12 saat authentication başarısızlıkları. Önleme: Validation ve required field kontrolleri olan type-safe environment builder'lar.

3. Secret'ların Environment-Specific Validation'a İhtiyacı Var

Öğrenme maliyeti: Kazara production ücretlerinde 23K dolar. Önleme: Cross-environment contamination'ı önleyen environment-aware secret validation.

4. Data Migration'ın Monitoring'e İhtiyacı Var

Öğrenme maliyeti: 3 migration denemesi, 180K kayıt risk altında. Önleme: Migration fonksiyonlarında kapsamlı logging, progress tracking ve timeout handling.

5. VPC Lambda Fonksiyonları Farklıdır

Öğrenme maliyeti: 15 dakikalık cold start'lar, connection pool tükenmesi. Önleme: Düzgün connection management, security group konfigürasyonu ve subnet planlama.

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

Metrikler:

  • 23 Lambda fonksiyonu migrate edildi
  • 3 production DynamoDB tablosu (180K+ kayıt)
  • 47 environment variable type-safe
  • 12 secret düzgün şekilde şifrelendi
  • Sıfır data kaybı (sonunda)

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.

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.

İlerleme4/6 yazı tamamlandı

İlgili Yazılar