Skip to content
~/sph.sh

Serverless Framework'ten AWS CDK'ya Geçiş: Bölüm 3 - DynamoDB ve S3

DynamoDB tabloları ve S3 bucket'larını CDK'ya taşıma. Data migration stratejileri ve en iyi uygulamalar.

Lambda fonksiyonları ve API Gateway konfigürasyonları, gerçek migration karmaşıklığının ortaya çıktığı yerdir. Basit bir YAML-to-TypeScript dönüşümü gibi görünen şey, hızlıca bundling optimizasyonu, memory ayarlama ve hata yönetimi pattern'lerini içeren çok katmanlı bir challenge olarak kendini gösterir.

Bu migration boyunca fonksiyon pattern'lerini standartlaştırma, cold start'ları optimize etme ve sürdürülebilir API konfigürasyonları oluşturma konularında değerli dersler öğrendim. Farklı memory ayarları, timeout konfigürasyonları ve deployment pattern'leri olan Lambda fonksiyonlarını migrate ederken öğrendiklerimi paylaşıyorum.

Seri Navigasyonu:

Fonksiyon Karmaşıklığını Anlamak

Lambda fonksiyon migration'ları, gerçek bir sistemde ne kadar farklı pattern'lerin var olduğunu fark ettiğinizde hızlıca karmaşık hale gelir. Fonksiyonlar genellikle farklı gereksinimlerle çeşitli kategorilere ayrılır:

Karşılaştığım yaygın fonksiyon türleri:

  • Farklı response pattern'leri olan API endpoint handler'ları
  • Değişken memory ihtiyaçları olan background job processor'lar
  • Hızlı response süresi gerektiren webhook handler'lar
  • Farklı timeout gereksinimlerli scheduled fonksiyonlar

Her tür farklı memory ayarları, timeout konfigürasyonları ve deployment pattern'lerinden faydalanır. Bu karmaşıklık, standartlaştırılmış bir yaklaşım oluşturmanın neden önemli olduğunu gösterir.

Standartlaştırılmış Lambda Construct Oluşturmak

Birkaç fonksiyonu manuel olarak migrate ettikten ve performans sorunlarıyla karşılaştıktan sonra, standartlaştırılmış bir construct oluşturmanın değerini öğrendim. Bu yaklaşım tutarlılığı sağlamaya yardımcı olur ve kanıtlanmış optimizasyonları içerir:

yaml
# serverless.ymlfunctions:  getUser:    handler: src/handlers/users.get    events:      - http:          path: users/{id}          method: get          cors: true    environment:      USERS_TABLE: ${self:service}-${opt:stage}-users    timeout: 10    memorySize: 256

İşte çeşitli fonksiyon migration'larından öğrenilenlerle oluşturulmuş standartlaştırılmış construct:

typescript
// lib/constructs/production-lambda.tsimport { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';import { Runtime, Tracing, Architecture } from 'aws-cdk-lib/aws-lambda';import { Duration, Stack } from 'aws-cdk-lib';import { RetentionDays } from 'aws-cdk-lib/aws-logs';
export interface ProductionLambdaProps extends Omit<NodejsFunctionProps, 'runtime'> {  stage: string;  functionName: string;  // Migration deneyiminden öğrenilen performans optimizasyonları  enableProvisioning?: boolean;  enableSnapStart?: boolean;}
export class ProductionLambda extends NodejsFunction {  constructor(scope: Construct, id: string, props: ProductionLambdaProps) {    super(scope, id, {      ...props,      runtime: Runtime.NODEJS_20_X,      architecture: Architecture.ARM_64,  // ARM64 daha iyi price-performance sunar
      // Fonksiyon profiling'e dayalı memory optimizasyonu      memorySize: props.memorySize || ProductionLambda.getOptimalMemory(props.functionName),
      // Timeout stratejisi: 28s max (API Gateway limiti 29s)      timeout: props.timeout || Duration.seconds(28),
      // Tracing sadece production'da aktif      tracing: props.stage === 'prod' ? Tracing.ACTIVE : Tracing.DISABLED,
      // Maliyet vs compliance için optimize edilmiş log retention      logRetention: props.stage === 'prod' ? RetentionDays.ONE_MONTH : RetentionDays.ONE_WEEK,
      // Her fonksiyonun ihtiyaç duyduğu environment variable'lar      environment: {        NODE_OPTIONS: '--enable-source-maps --max-old-space-size=896',        AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',        STAGE: props.stage,        FUNCTION_NAME: props.functionName,        ...props.environment,      },
      // Cold start iyileştirmesi için bundling optimizasyonları      bundling: {        minify: props.stage === 'prod',        sourceMap: true,        sourcesContent: false,        target: 'node20',        keepNames: true,        // Daha küçük bundle boyutları için tree shaking        treeShaking: true,        // External module'ler (Lambda runtime tarafından sağlanan)        externalModules: [          '@aws-sdk/*',  // Lambda runtime'da AWS SDK v3          'aws-lambda',  // Lambda types        ],        // Büyük fonksiyonlar için bundle analizi        metafile: props.stage !== 'prod',        // Production debugging için custom banner        banner: props.stage === 'prod'          ? '/* Production Lambda - Generated by CDK */'          : undefined,        // Dead code elimination için define        define: {          'process.env.NODE_ENV': props.stage === 'prod' ? '"production"' : '"development"',        },      },
      // Kritik fonksiyonlar için reserved concurrency      reservedConcurrentExecutions: props.enableProvisioning ? 10 : undefined,    });
    // Tüm fonksiyonlar için standart tag'ler    Tags.of(this).add('Stage', props.stage);    Tags.of(this).add('FunctionName', props.functionName);    Tags.of(this).add('ManagedBy', 'CDK');  }
  // Fonksiyon profiling'e dayalı memory optimizasyonu  private static getOptimalMemory(functionName: string): number {    // API fonksiyonları: CPU-bound, daha fazla memory'den yarar görür    if (functionName.includes('api-')) return 1024;    // Background jobs: Memory-intensive processing    if (functionName.includes('job-')) return 2048;    // Webhook'lar: Hızlı response gerekli    if (functionName.includes('webhook-')) return 512;    // Default: Balanced performance/cost    return 1024;  }}
// Standartlaştırılmış pattern'lerle kullanım örneğiconst getUserFn = new ProductionLambda(this, 'GetUserFunction', {  stage: config.stage,  functionName: 'api-get-user',  entry: 'src/handlers/users/get.ts',  handler: 'handler',  environment: {    USERS_TABLE: usersTable.tableName,  },});
// Type-safe izinler (artık wildcard IAM policy yok)usersTable.grantReadData(getUserFn);
// Düzgün error handling ile API Gateway entegrasyonuconst userIdResource = users.addResource('{id}');userIdResource.addMethod('GET', new LambdaIntegration(getUserFn, {  // Düzgün error handling için integration response'lar  integrationResponses: [    {      statusCode: '200',      responseTemplates: {        'application/json': '$input.path("$")',      },    },    {      statusCode: '404',      selectionPattern: '.*"statusCode":404.*',      responseTemplates: {        'application/json': '{"error": "User not found"}',      },    },  ],}));

Lambda Layer Migration

Serverless Framework layer'ları:

yaml
layers:  shared:    path: layers/shared    compatibleRuntimes:      - nodejs20.x
functions:  createUser:    handler: src/handlers/users.create    layers:      - {Ref: SharedLambdaLayer}

Daha iyi type safety ile CDK yaklaşımı:

typescript
// lib/constructs/shared-layer.tsimport { LayerVersion, Code, Runtime } from 'aws-cdk-lib/aws-lambda';import { Construct } from 'constructs';
export class SharedLayer extends LayerVersion {  constructor(scope: Construct, id: string) {    super(scope, id, {      code: Code.fromAsset('layers/shared'),      compatibleRuntimes: [Runtime.NODEJS_20_X],      description: 'Shared utilities and dependencies',    });  }}
// Stack'te kullanımconst sharedLayer = new SharedLayer(this, 'SharedLayer');
const createUserFn = new ServerlessFunction(this, 'CreateUserFunction', {  entry: 'src/handlers/users.ts',  handler: 'create',  config,  layers: [sharedLayer],});

Function Bundling ve Dependency'ler

CDK'nın NodejsFunction'ı gelişmiş bundling seçenekleri sağlıyor:

typescript
// lib/constructs/optimized-function.tsexport class OptimizedFunction extends ServerlessFunction {  constructor(scope: Construct, id: string, props: ServerlessFunctionProps) {    super(scope, id, {      ...props,      bundling: {        minify: props.config.stage === 'prod',        sourceMap: true,        sourcesContent: false,        target: 'es2022',        keepNames: true,
        // External module'ler (bundle edilmez)        externalModules: [          '@aws-sdk/*',  // Lambda runtime tarafından sağlanan AWS SDK v3          'aws-lambda',   // Sadece type'lar        ],
        // Belirli module'leri zorla dahil et        nodeModules: ['bcrypt', 'sharp'],  // Native dependency'ler
        // Build environment        environment: {          NODE_ENV: props.config.stage === 'prod' ? 'production' : 'development',        },
        // Custom esbuild plugin'leri        esbuildArgs: {          '--log-level': 'warning',          '--tree-shaking': 'true',        },      },    });  }}

API Gateway Gelişmiş Konfigürasyonları

Request Validation

Serverless Framework request validation:

yaml
functions:  createUser:    handler: src/handlers/users.create    events:      - http:          path: users          method: post          request:            schemas:              application/json: ${file(schemas/create-user.json)}

Inline model'ler ve validator'lar ile CDK:

typescript
// lib/constructs/validated-api.tsimport {  RestApi,  Model,  JsonSchema,  JsonSchemaType,  RequestValidator,  MethodOptions} from 'aws-cdk-lib/aws-apigateway';
export class ValidatedApi extends RestApi {  private validator: RequestValidator;
  constructor(scope: Construct, id: string, props: RestApiProps) {    super(scope, id, props);
    // Reusable validator oluştur    this.validator = new RequestValidator(this, 'BodyValidator', {      restApi: this,      validateRequestBody: true,      validateRequestParameters: false,    });  }
  addValidatedMethod(    resource: IResource,    httpMethod: string,    integration: Integration,    schema: JsonSchema  ): Method {    // Schema'dan model oluştur    const model = new Model(this, `${httpMethod}${resource.path}Model`, {      restApi: this,      contentType: 'application/json',      schema,    });
    // Validation ile method ekle    return resource.addMethod(httpMethod, integration, {      requestValidator: this.validator,      requestModels: {        'application/json': model,      },    });  }}
// Kullanımconst createUserSchema: JsonSchema = {  type: JsonSchemaType.OBJECT,  required: ['email', 'name'],  properties: {    email: {      type: JsonSchemaType.STRING,      format: 'email',    },    name: {      type: JsonSchemaType.STRING,      minLength: 1,      maxLength: 100,    },    age: {      type: JsonSchemaType.INTEGER,      minimum: 0,      maximum: 150,    },  },};
api.addValidatedMethod(  users,  'POST',  new LambdaIntegration(createUserFn),  createUserSchema);

Response Transformations

Serverless Framework response template'leri:

yaml
functions:  getUsers:    handler: src/handlers/users.list    events:      - http:          path: users          method: get          response:            headers:              Content-Type: "'application/json'"            template: $input.path(')            statusCodes:              200:                pattern: ''              404:                pattern: '.*"statusCode":404.*'                template: $input.path('$.errorMessage')

CDK integration response konfigürasyonu:

typescript
// lib/constructs/api-integration.tsexport function createLambdaIntegration(  fn: IFunction,  options?: {    enableCors?: boolean;    responseMapping?: Record<string, IntegrationResponse>;  }): LambdaIntegration {  const responseParameters: Record<string, string> = {};
  if (options?.enableCors) {    responseParameters['method.response.header.Access-Control-Allow-Origin'] = "'*'";  }
  return new LambdaIntegration(fn, {    proxy: false,    integrationResponses: [      {        statusCode: '200',        responseParameters,        responseTemplates: {          'application/json': '$input.path("$")',        },      },      {        statusCode: '404',        selectionPattern: '.*"statusCode":404.*',        responseParameters,        responseTemplates: {          'application/json': '$input.path("$.errorMessage")',        },      },      {        statusCode: '500',        selectionPattern: '.*"statusCode":5\\d{2}.*',        responseParameters,        responseTemplates: {          'application/json': '{"error": "Internal Server Error"}',        },      },    ],  });}

API Gateway Authorizer'lar

Serverless Framework authorizer'larından migration:

yaml
functions:  auth:    handler: src/handlers/auth.handler
  getProfile:    handler: src/handlers/users.profile    events:      - http:          path: users/profile          method: get          authorizer: auth

CDK Lambda authorizer implementasyonu:

typescript
// lib/constructs/api-authorizer.tsimport {  TokenAuthorizer,  IdentitySource,  IRestApi} from 'aws-cdk-lib/aws-apigateway';import { Duration } from 'aws-cdk-lib';
export class ApiAuthorizer extends TokenAuthorizer {  constructor(scope: Construct, id: string, props: {    api: IRestApi;    authorizerFunction: IFunction;  }) {    super(scope, id, {      restApi: props.api,      handler: props.authorizerFunction,      identitySource: IdentitySource.header('Authorization'),      resultsCacheTtl: Duration.minutes(5),      authorizerName: `${props.api.restApiName}-authorizer`,    });  }}
// Stack'te kullanımconst authFn = new ServerlessFunction(this, 'AuthorizerFunction', {  entry: 'src/handlers/auth.ts',  handler: 'handler',  config,});
const authorizer = new ApiAuthorizer(this, 'ApiAuthorizer', {  api: this.api,  authorizerFunction: authFn,});
// Korumalı endpointconst profile = users.addResource('profile');profile.addMethod('GET', new LambdaIntegration(getProfileFn), {  authorizer,  authorizationType: AuthorizationType.CUSTOM,});

Error Handling Pattern'leri

Structured Error Response'lar

Sağlam bir error handling sistemi oluşturun:

typescript
// src/libs/api-gateway.tsexport class ApiError extends Error {  constructor(    public statusCode: number,    message: string,    public code?: string  ) {    super(message);    this.name = 'ApiError';  }}
export const formatError = (error: unknown): APIGatewayProxyResultV2 => {  console.error('Error:', error);
  if (error instanceof ApiError) {    return {      statusCode: error.statusCode,      headers: { 'Content-Type': 'application/json' },      body: JSON.stringify({        error: {          code: error.code || 'UNKNOWN_ERROR',          message: error.message,        },      }),    };  }
  return {    statusCode: 500,    headers: { 'Content-Type': 'application/json' },    body: JSON.stringify({      error: {        code: 'INTERNAL_SERVER_ERROR',        message: 'An unexpected error occurred',      },    }),  };};
// src/libs/lambda.tsexport const withErrorHandling = <T extends (...args: any[]) => any>(  handler: T): T => {  return (async (...args: Parameters<T>) => {    try {      return await handler(...args);    } catch (error) {      return formatError(error);    }  }) as T;};

Handler'larda Error Handling Kullanımı

typescript
// src/handlers/users.tsimport { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';import { ApiError, withErrorHandling } from '../libs/api-gateway';
export const get = withErrorHandling(  async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> => {    const { id } = event.pathParameters || {};
    if (!id) {      throw new ApiError(400, 'User ID is required', 'MISSING_PARAMETER');    }
    // Database lookup simülasyonu    const user = await getUserById(id);
    if (!user) {      throw new ApiError(404, 'User not found', 'USER_NOT_FOUND');    }
    return {      statusCode: 200,      headers: { 'Content-Type': 'application/json' },      body: JSON.stringify({ user }),    };  });

API Versioning Stratejileri

Path-Based Versioning

typescript
// lib/stacks/versioned-api-stack.tsexport class VersionedApiStack extends Stack {  constructor(scope: Construct, id: string, props: ApiStackProps) {    super(scope, id, props);
    const api = new RestApi(this, 'VersionedApi', {      restApiName: `my-service-${props.config.stage}`,    });
    // Version 1    const v1 = api.root.addResource('v1');    this.setupV1Routes(v1, props.config);
    // Breaking change'ler ile Version 2    const v2 = api.root.addResource('v2');    this.setupV2Routes(v2, props.config);  }
  private setupV1Routes(parent: IResource, config: EnvironmentConfig) {    const users = parent.addResource('users');
    // Legacy response format    const getUserV1Fn = new ServerlessFunction(this, 'GetUserV1Function', {      entry: 'src/handlers/v1/users.ts',      handler: 'get',      config,    });
    users.addResource('{id}').addMethod('GET',      new LambdaIntegration(getUserV1Fn)    );  }
  private setupV2Routes(parent: IResource, config: EnvironmentConfig) {    const users = parent.addResource('users');
    // Pagination ile yeni response format    const getUserV2Fn = new ServerlessFunction(this, 'GetUserV2Function', {      entry: 'src/handlers/v2/users.ts',      handler: 'get',      config,    });
    users.addResource('{id}').addMethod('GET',      new LambdaIntegration(getUserV2Fn)    );  }}

Performans Optimizasyonları

Lambda Cold Start Optimizasyonu

typescript
// lib/constructs/warm-function.tsimport { Rule, Schedule } from 'aws-cdk-lib/aws-events';import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
export class WarmFunction extends ServerlessFunction {  constructor(scope: Construct, id: string, props: ServerlessFunctionProps & {    warmingSchedule?: Schedule;  }) {    super(scope, id, props);
    if (props.config.stage === 'prod' && props.warmingSchedule) {      // Warming rule oluştur      new Rule(this, 'WarmingRule', {        schedule: props.warmingSchedule,        targets: [          new LambdaFunction(this, {            event: {              source: 'warmer',              action: 'ping',            },          }),        ],      });
      // Handler'a warming check ekle      this.addEnvironment('ENABLE_WARMING', 'true');    }  }}
// Handler'daexport const handler = async (event: any) => {  // Warming invocation'larını atla  if (event.source === 'warmer') {    return { statusCode: 200, body: 'Warmed' };  }
  // Normal handler logic};

API Gateway Caching

typescript
// lib/constructs/cached-method.tsexport function addCachedMethod(  resource: IResource,  httpMethod: string,  integration: Integration,  cachingEnabled: boolean = true,  ttl: Duration = Duration.minutes(5)): Method {  return resource.addMethod(httpMethod, integration, {    methodResponses: [{      statusCode: '200',      responseParameters: {        'method.response.header.Cache-Control': true,      },    }],    requestParameters: {      'method.request.querystring.page': false,      'method.request.querystring.limit': false,    },  });}
// Stage level'da caching aktif etconst api = new RestApi(this, 'CachedApi', {  deployOptions: {    cachingEnabled: true,    cacheClusterEnabled: true,    cacheClusterSize: '0.5',    cacheTtl: Duration.minutes(5),    cacheDataEncrypted: true,  },});

Migration Checklist

Production'a geçmeden önce şunları ele aldığınızdan emin olun:

  • Tüm Lambda fonksiyonları uygun memory/timeout ayarlarıyla migrate edildi
  • Environment variable'lar düzgün scope'lanmış ve şifrelenmiş
  • API Gateway route'ları mevcut path'lerle tam olarak eşleşiyor
  • CORS konfigürasyonu mevcut ayarlarla eşleşiyor
  • Request validation schema'ları migrate edildi
  • Custom authorizer'lar implement edildi ve test edildi
  • Error response'lar backward compatibility'yi koruyor
  • Lambda layer'lar düzgün konfigüre edildi
  • Cold start optimizasyonları yerinde
  • API caching stratejisi implement edildi
  • Monitoring ve alarm'lar konfigüre edildi

Öğrenilen Temel Dersler

Bu migration deneyimi boyunca önemli birkaç pattern ortaya çıktı:

Standardizasyon Karşılığını Verir

Tutarlı bir fonksiyon construct'ı oluşturmak, konfigürasyon drift'ini ortadan kaldırır ve performans optimizasyonlarını otomatik hale getirir. Yeni fonksiyonlar özel konfigürasyon gerektirmek yerine kanıtlanmış pattern'leri miras alır.

Memory ve Architecture Seçimleri Önemlidir

ARM64 architecture ve doğru boyutlandırılmış memory allocation hem performansı hem de maliyeti önemli ölçüde etkileyebilir. Farklı fonksiyon türleri farklı memory konfigürasyonlarından faydalanır.

Bundling Stratejisi Kritiktir

Tree shaking ve external module exclusion ile düşünceli bundling, cold start sürelerini azaltır. AWS SDK v3 Lambda runtime'da mevcut olduğu için bundle'lardan çıkarmak yardımcı olur.

Error Handling Yapı Gerektirir

API Gateway error handling dikkatli integration response konfigürasyonu gerektirir. Tüm fonksiyonlarda tutarlı error response pattern'lerine sahip olmak debugging ve client handling'i iyileştirir.

Sonraki Adımlar: Database ve Environment Yönetimi

Lambda fonksiyonları ve API Gateway konfigürasyonları migrate edildiğinde, sonraki challenge database kaynakları ve environment yönetimini içerir. Stateless fonksiyonların aksine, database'ler kalıcı veri içerdikleri için dikkatli handling gerektirir çünkü kolayca yeniden oluşturulamazlar.

4. Bölüm'de şunları keşfedeceğiz:

  • DynamoDB tabloları ve RDS instance'larını migrate etmek
  • Environment variable yönetimi ve secret handling
  • Database erişimi için VPC konfigürasyonları
  • Backup ve disaster recovery stratejileri
  • Cross-environment tutarlılık pattern'leri

Database migration, özellikle data güvenliği ve environment izolasyonu konularında, function migration'dan farklı stratejiler gerektirir.

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.

İlerleme3/6 yazı tamamlandı

İlgili Yazılar