İçeriğe atla

Serverless Framework'ten AWS CDK'ya Geçiş: Bölüm 3 - Lambda Fonksiyonları ve API Gateway

Lambda fonksiyonlarını ve API Gateway konfigürasyonlarını CDK'ya taşıma: bundling optimizasyonu, memory ayarı, hata yönetimi pattern'leri.

Ayhan Sipahi Ayhan Sipahi

Lambda fonksiyonları ve API Gateway konfigürasyonları, migration karmaşıklığının yoğunlaştığı yerdir. YAML’dan TypeScript’e geçiş mekanik bir iş gibi görünür; ta ki bundling, memory boyutlandırma ve hata yönetimi ayrı birer karara dönüşene kadar.

İşe yarayan yaklaşım tek bir ortak NodejsFunction construct’ıdır: runtime, architecture, memory, tracing ve bundling varsayılanları tek yerde durur, farklı davranması gereken fonksiyonlar için her prop override edilebilir kalır. API Gateway tarafında da aynı yöntem geçerlidir; validator’lar, authorizer’lar ve integration response’lar bir kez tanımlanıp route’lar arasında yeniden kullanılır.

Seri Navigasyonu:

Fonksiyon Karmaşıklığı#

Lambda fonksiyon migration’ları, gerçek bir sistemde kaç farklı pattern olduğunu fark ettiğinizde hızla karmaşıklaşır. Fonksiyonlar genellikle gereksinimleri birbirinden ayrışan kategorilere bölünür:

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 gereksinimleri olan scheduled fonksiyonlar

Her tür farklı memory ayarlarından, timeout konfigürasyonlarından ve deployment pattern’lerinden faydalanır.

Standartlaştırılmış Lambda Construct#

Serverless Framework’te her fonksiyon tanımı kendi memory, timeout ve environment ayarlarını taşır; bu ayarlar fonksiyon başına tekrar eder:

# serverless.yml
functions:
  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

CDK karşılığı bu varsayılanları tek bir construct’a taşır; çağrı yerinde yalnızca fonksiyona özgü farklar kalır:

// lib/constructs/production-lambda.ts
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime, Tracing, Architecture } from 'aws-cdk-lib/aws-lambda';
import { Duration, Tags } from 'aws-cdk-lib';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

export interface ProductionLambdaProps extends Omit<NodejsFunctionProps, 'runtime'> {
  stage: string;
  functionName: string;
  // Fonksiyon bazlı opsiyonel override'lar
  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 kategorisine göre memory varsayılanı
      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
        esbuildArgs: { '--tree-shaking': '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 kategorisine göre memory varsayılanları
  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ği
const 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);

// API Gateway entegrasyonu: proxy entegrasyonu handler'ın statusCode,
// headers ve body değerlerini olduğu gibi geçirir; hata response'ları
// handler içinde kalır
const userIdResource = users.addResource('{id}');
userIdResource.addMethod('GET', new LambdaIntegration(getUserFn));

Lambda Layer Migration#

Serverless Framework layer’ları:

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

// lib/constructs/shared-layer.ts
import { 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ım
const 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:

// lib/constructs/optimized-function.ts
export 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',
        },
      },
    });
  }
}

Gelişmiş API Gateway Konfigürasyonları#

Request Validation#

Serverless Framework request validation:

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:

// lib/constructs/validated-api.ts
import {
  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ım
const 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:

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:

// lib/constructs/api-integration.ts
export 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 olmayan entegrasyon: selectionPattern Lambda hatasıyla eşleşir,
    // yani bu handler'lar status code döndürmek yerine hata fırlatır
    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:

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:

// lib/constructs/api-authorizer.ts
import {
  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ım
const 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ı endpoint
const profile = users.addResource('profile');
profile.addMethod('GET', new LambdaIntegration(getProfileFn), {
  authorizer,
  authorizationType: AuthorizationType.CUSTOM,
});

Error Handling Pattern’leri#

Yapılandırılmış Error Response’lar#

Sağlam bir error handling sistemi oluşturun:

// src/libs/api-gateway.ts
export class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public code?: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

export const formatError = (error: unknown): APIGatewayProxyResult => {
  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.ts
export 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ı#

// src/handlers/users.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { ApiError } from '../libs/api-gateway';
import { withErrorHandling } from '../libs/lambda';

export const get = withErrorHandling(
  async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    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 Tabanlı Versiyonlama#

// lib/stacks/versioned-api-stack.ts
export 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#

// lib/constructs/warm-function.ts
import { Rule, Schedule, RuleTargetInput } 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: RuleTargetInput.fromObject({
              source: 'warmer',
              action: 'ping',
            }),
          }),
        ],
      });

      // Handler'a warming check ekle
      this.addEnvironment('ENABLE_WARMING', 'true');
    }
  }
}

// Handler'da
export 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#

// lib/constructs/cached-method.ts
export function addCachedMethod(
  resource: IResource,
  httpMethod: string,
  integration: Integration
): Method {
  return resource.addMethod(httpMethod, integration, {
    methodResponses: [{
      statusCode: '200',
      responseParameters: {
        'method.response.header.Cache-Control': true,
      },
    }],
    // Stage cache'inin key olarak kullanabileceği query string'ler
    requestParameters: {
      'method.request.querystring.page': false,
      'method.request.querystring.limit': false,
    },
  });
}

// Caching'in kendisi stage seviyesinde açılır
const 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

Ortak Construct Ne Zaman Mantıklı#

Ortak construct, birkaç fonksiyon aynı runtime, tracing ve bundling varsayılanlarına ihtiyaç duyduğu anda kendini amorti eder. Log retention veya architecture değişikliği her stack yerine tek dosyada yapılır. Bir avuç fonksiyonun altındaysanız düz NodejsFunction çağrıları daha okunaklı kalır, construct de araya yalnızca bir katman ekler.

Bir fonksiyon gerçekten farklıysa construct’ı genişletmek yerine prop’ları override edin. 10 GB memory ve 15 dakikalık timeout isteyen bir video encoder, getOptimalMemory içine yeni bir dal eklemek yerine bu değerleri açıkça geçmelidir. Helper üçüncü özel duruma ulaştığında, isimlendirme kuralı kararı taşımayı bırakmış demektir. Bundling için de aynı kural geçerli: externalModules ve nodeModules fonksiyona özgü bilgilerdir, varsayılandan ayrıldıklarında çağrı yerinde kalmaları daha iyidir.

Sonraki Adım: Database ve Environment Yönetimi#

Lambda fonksiyonları ve API Gateway konfigürasyonları taşındıktan sonra sıra database kaynaklarına ve environment yönetimine gelir. Stateless fonksiyonların aksine database’ler kalıcı veri tutar; kolayca yeniden oluşturulamadıkları için migration adımları da farklıdır.

4. Bölüm şunları ele alıyor:

  • 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

Kaynaklar#

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.

İlerleme 3/6 yazı tamamlandı

İlgili yazılar