İçeriğe atla

Serverless Framework'ten AWS CDK'ya Geçiş: Bölüm 6 - Migration Stratejileri

Serverless Framework'ten AWS CDK'ya geçişi planlayın: blue-green deployment, rollback prosedürleri, altyapı testleri ve performans ayarı.

Ayhan Sipahi Ayhan Sipahi

Bir CDK migrasyonunun son aşamasında mesele, üretilen altyapının doğruluğu değildir. Mesele, geçiş anındaki operasyonel hazırlıktır: trafiği almak için bir plan, migrasyon production’ı bozarsa geri dönmek için bir plan ve canlı yük altında ortaya çıkan drift’i gidermek için bir plan. Onlarca Lambda fonksiyonu ve birden fazla DynamoDB tablosu içeren bir migrasyonda geçiş planı, migrasyon kodunun kendisinden daha büyüktür.

Varsayılan olarak alarma bağlı otomatik rollback’li blue-green deployment’ı seçin. Bedeli, geçiş boyunca ikinci bir stack’i ayakta tutmaktır; karşılığı ise CloudFormation rollback’ini beklemeden saniyeler içinde geri alınabilen bir anahtardır. Geri kalan iş gösterişsizdir: bir stack’in güvenli olup olmadığına karar verebilen health check’ler, iş mantığını değil servis limitlerini zorlayan testler ve production trafiği ona bağlanmadan önce en az bir kez çalıştırılmış bir rollback yolu.

Seri Navigasyonu:

Geçiş Deseni Seçimi#

Serverless migrasyonlarında üç desen öne çıkar ve her biri farklı bir yerden çatlar.

Yaklaşım #1: Big Bang Migration#

Uygulama: Tüm CDK altyapısını tek bir bakım penceresinde deploy etmek.

Nerede çatlar: Büyük bir stack’in CloudFormation deployment süresini önceden kestirmek zordur; ilk taşan şey bakım penceresi olur. Başarısız bir API Gateway stage deployment’ı ya da yarım kalan bir DynamoDB import’u, servis zaten kapalıyken rollback yapmanıza yol açar; üstelik rollback yolu deployment yolu kadar uzundur.

Karar: Demo uygulamalar ve gerçek bir bakım penceresi olan iç araçlar için uygundur. Cross-service bağımlılıkları olan production sistemler için uygun değildir.

Yaklaşım #2: Strangler Fig Pattern#

Uygulama: Fonksiyonları gruplar hâlinde taşımak; eski stack’te iş kalmayana kadar trafiği eski ve yeni API arasında bölmek.

Nerede çatlar: İki sistem, geçiş boyunca authentication ve session state’ini paylaşmak zorundadır. Stack’ler arası çağrılar, tek stack’li sürümde olmayan bir ağ atlaması ekler. Geçiş haftalarla ölçülür ve bu haftaların her biri iki deployment pipeline’ını birlikte işletmek demektir.

Karar: Uygulama tek seferde taşınamayacak kadar büyükse doğru seçimdir; yeter ki bağımlılık haritası önceden çıkarılsın ve auth katmanı kopyalanmak yerine paylaşılsın.

Yaklaşım #3: Blue-Green Deployment#

Uygulama: Tam CDK stack’ini paralel olarak ayağa kaldırmak, production’a benzeyen trafikle doğrulamak, sonra DNS’i ya da API Gateway stage’ini çevirmek.

Nerede çatlar: Her stateless kaynağın ikinci bir kopyasının bedelini ödersiniz ve bu desen state hakkında hiçbir şey söylemez. Paylaşılan tablolar iki stack tarafından da okunabilir kalmalıdır; bu da geçiş süresince şema değişikliklerini kısıtlar.

Karar: Varsayılan seçenek. Rollback, bir CloudFormation operasyonu değil ikinci bir DNS ya da stage değişikliğidir; anahtarı baskı altında geri alınabilir kılan da budur.

Production’a Hazır Migration Stratejileri#

1. Blue-Green Deployment Stack’i#

Blue-green stack’i kendi health check’ini, kendi alarmlarını ve anahtarı çeviren fonksiyonu birlikte taşır:

// lib/stacks/production-blue-green-stack.ts
import { Stack, StackProps, Tags, CfnOutput, Duration, TreatMissingData } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { RestApi, MethodLoggingLevel, LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
import { Alarm, ComparisonOperator } from 'aws-cdk-lib/aws-cloudwatch';
import { LambdaAction } from 'aws-cdk-lib/aws-cloudwatch-actions';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export interface BlueGreenStackProps extends StackProps {
  stage: string;
  environment: 'blue' | 'green';
  monitoringConfig: {
    errorThreshold: number;
    latencyThreshold: number;
    rollbackFunction: NodejsFunction;
  };
}

export class ProductionBlueGreenStack extends Stack {
  public readonly api: RestApi;
  public readonly healthCheckEndpoint: string;
  public readonly switchOverFunction: NodejsFunction;

  constructor(scope: Construct, id: string, props: BlueGreenStackProps) {
    super(scope, id, props);

    // Tam CDK infrastructure oluştur
    this.api = new RestApi(this, 'Api', {
      restApiName: `my-service-${props.stage}-${props.environment}`,
      description: `Production API - ${props.environment.toUpperCase()} environment`,
      deployOptions: {
        stageName: props.environment,
        // Migration sırasında güvenlik için agresif throttling
        throttlingRateLimit: props.environment === 'green' ? 500 : 1000,
        throttlingBurstLimit: props.environment === 'green' ? 1000 : 2000,
        // Migration sırasında gelişmiş monitoring
        metricsEnabled: true,
        loggingLevel: MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        tracingEnabled: true,
      },
    });

    // Tüm Lambda fonksiyonlarını deploy et ve route'ları bağla.
    // createLambdaFunctions ve setupApiRoutes bu sınıfın private helper'ları,
    // kısalık için buraya alınmadı.
    const functions = this.createLambdaFunctions(props);
    this.setupApiRoutes(functions);

    // Monitoring için health check endpoint oluştur
    const healthCheckFn = new NodejsFunction(this, 'HealthCheckFunction', {
      entry: 'src/health/health-check.ts',
      handler: 'handler',
      environment: {
        ENVIRONMENT: props.environment,
        API_VERSION: process.env.API_VERSION || 'v1',
        DEPLOYMENT_TIME: new Date().toISOString(),
      },
    });

    const healthResource = this.api.root.addResource('health');
    healthResource.addMethod('GET', new LambdaIntegration(healthCheckFn));

    this.healthCheckEndpoint = `${this.api.url}health`;

    // Production monitoring alarm'ları oluştur
    this.createProductionAlarms(props);

    // Traffic switching fonksiyonu
    this.switchOverFunction = this.createSwitchOverFunction(props);

    // Tanımlama için tüm kaynakları tagla
    Tags.of(this).add('Environment', props.environment);
    Tags.of(this).add('MigrationPhase', 'cdk-migration');
    Tags.of(this).add('DeploymentTime', new Date().toISOString());
    Tags.of(this).add('Version', process.env.COMMIT_SHA || 'latest');

    // Kritik bilgileri export et
    new CfnOutput(this, 'ApiEndpoint', {
      value: this.api.url,
      exportName: `${this.stackName}-api-endpoint`,
      description: `API endpoint for ${props.environment} environment`,
    });

    new CfnOutput(this, 'HealthCheckUrl', {
      value: this.healthCheckEndpoint,
      exportName: `${this.stackName}-health-check`,
      description: 'Health check endpoint for monitoring',
    });
  }

  private createProductionAlarms(props: BlueGreenStackProps) {
    // Error rate alarm - rollback tetikler
    const errorAlarm = new Alarm(this, 'HighErrorRateAlarm', {
      metric: this.api.metricServerError({
        period: Duration.minutes(2),
        statistic: 'Sum',
      }),
      threshold: props.monitoringConfig.errorThreshold,
      evaluationPeriods: 2,
      comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
      alarmDescription: `High error rate detected in ${props.environment} environment`,
      treatMissingData: TreatMissingData.NOT_BREACHING,
    });

    // Latency alarm - araştırma tetikler
    const latencyAlarm = new Alarm(this, 'HighLatencyAlarm', {
      metric: this.api.metricLatency({
        period: Duration.minutes(5),
        statistic: 'Average',
      }),
      threshold: props.monitoringConfig.latencyThreshold,
      evaluationPeriods: 3,
      alarmDescription: `High latency detected in ${props.environment} environment`,
    });

    // Alarm'ları otomatik rollback'e bağla
    errorAlarm.addAlarmAction(
      new LambdaAction(props.monitoringConfig.rollbackFunction)
    );

    // External monitoring için alarm ARN'lerini export et
    new CfnOutput(this, 'ErrorAlarmArn', {
      value: errorAlarm.alarmArn,
      exportName: `${this.stackName}-error-alarm`,
    });
  }

  private createSwitchOverFunction(props: BlueGreenStackProps) {
    return new NodejsFunction(this, 'TrafficSwitchFunction', {
      entry: 'src/deployment/traffic-switch.ts',
      handler: 'handler',
      timeout: Duration.minutes(5),
      environment: {
        CURRENT_ENVIRONMENT: props.environment,
        TARGET_ENVIRONMENT: props.environment === 'blue' ? 'green' : 'blue',
        HOSTED_ZONE_ID: process.env.HOSTED_ZONE_ID!,
        DOMAIN_NAME: process.env.API_DOMAIN!,
        SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL!,
      },
      initialPolicy: [
        new PolicyStatement({
          actions: ['route53:ChangeResourceRecordSets', 'route53:GetChange'],
          resources: ['*'],
        }),
      ],
    });
  }
}

// src/health/health-check.ts - Kapsamlı health validation
import { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';

const dynamoDB = new DynamoDBClient({});

export const handler: APIGatewayProxyHandler = async () => {
  const startTime = Date.now();
  const checks = [];

  try {
    // Database connectivity check
    const tableCheck = await dynamoDB.send(new DescribeTableCommand({
      TableName: process.env.USERS_TABLE!,
    }));
    checks.push({
      name: 'database',
      status: tableCheck.Table?.TableStatus === 'ACTIVE' ? 'healthy' : 'unhealthy',
      responseTime: Date.now() - startTime,
    });

    // Memory usage check
    const memoryUsed = process.memoryUsage();
    checks.push({
      name: 'memory',
      status: memoryUsed.heapUsed < 100 * 1024 * 1024 ? 'healthy' : 'warning', // 100MB threshold
      details: {
        heapUsed: Math.round(memoryUsed.heapUsed / 1024 / 1024) + 'MB',
        heapTotal: Math.round(memoryUsed.heapTotal / 1024 / 1024) + 'MB',
      },
    });

    const overallStatus = checks.every(check => check.status === 'healthy') ? 'healthy' : 'degraded';

    return {
      statusCode: overallStatus === 'healthy' ? 200 : 503,
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'no-cache',
      },
      body: JSON.stringify({
        status: overallStatus,
        environment: process.env.ENVIRONMENT,
        version: process.env.API_VERSION,
        deploymentTime: process.env.DEPLOYMENT_TIME,
        timestamp: new Date().toISOString(),
        responseTime: Date.now() - startTime,
        checks,
      }),
    };
  } catch (error) {
    return {
      statusCode: 503,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        status: 'unhealthy',
        error: (error as Error).message,
        timestamp: new Date().toISOString(),
      }),
    };
  }
};

2. Strangler Fig Pattern#

Ne zaman kullanılır: Sıfır kesinti gerektiren büyük uygulamalar.

// lib/constructs/migration/traffic-splitter.ts
import { Construct } from 'constructs';
import { RestApi, Deployment, Stage } from 'aws-cdk-lib/aws-apigateway';
import { Alarm } from 'aws-cdk-lib/aws-cloudwatch';

export class TrafficSplitter extends Construct {
  constructor(scope: Construct, id: string, props: {
    legacyApiId: string;
    newApi: RestApi;
    trafficPercentageToNew: number;
  }) {
    super(scope, id);

    // Canary deployment oluştur
    const deployment = new Deployment(this, 'CanaryDeployment', {
      api: props.newApi,
      description: `Canary deployment ${new Date().toISOString()}`,
    });

    const stage = new Stage(this, 'CanaryStage', {
      deployment,
      stageName: 'canary',
      canarySettings: {
        percentTraffic: props.trafficPercentageToNew,
        useStageCache: false,
      },
    });

    // Monitoring için CloudWatch alarm'ları.
    // Metriği tüm API'ye değil canary stage'ine bağla.
    new Alarm(this, 'CanaryErrorAlarm', {
      metric: stage.metricServerError(),
      threshold: 5,
      evaluationPeriods: 2,
    });
  }
}

3. DNS Seviyesinde Traffic Switching#

Ne zaman kullanılır: Anahtarın CloudFormation dışında dönmesi gerektiğinde. Rollback o zaman tek bir kayıt değişikliğidir; araya stack güncellemesi girmez.

// lib/stacks/blue-green-stack.ts
export class BlueGreenStack extends Stack {
  constructor(scope: Construct, id: string, props: {
    stage: string;
    version: 'blue' | 'green';
  }) {
    super(scope, id);

    const api = new RestApi(this, 'Api', {
      restApiName: `my-service-${props.stage}-${props.version}`,
      deployOptions: {
        stageName: props.version,
      },
    });

    // Kolay tanımlama için kaynakları tagla
    Tags.of(this).add('Deployment', props.version);
    Tags.of(this).add('Version', process.env.COMMIT_SHA || 'latest');

    // API endpoint'ini export et
    new CfnOutput(this, 'ApiEndpoint', {
      value: api.url,
      exportName: `${this.stackName}-endpoint`,
    });
  }
}

// deployment-scripts/blue-green-switch.ts
import { Route53Client, ChangeResourceRecordSetsCommand } from '@aws-sdk/client-route53';

export async function switchTraffic(targetVersion: 'blue' | 'green') {
  const route53 = new Route53Client({});

  await route53.send(new ChangeResourceRecordSetsCommand({
    HostedZoneId: process.env.HOSTED_ZONE_ID,
    ChangeBatch: {
      Changes: [{
        Action: 'UPSERT',
        ResourceRecordSet: {
          Name: 'api.example.com',
          Type: 'CNAME',
          TTL: 60,
          ResourceRecords: [{
            Value: `api-${targetVersion}.execute-api.region.amazonaws.com`,
          }],
        },
      }],
    },
  }));
}

Production İçin Etkili Test Stratejisi#

Her unit ve integration testini geçen bir suite, geçiş anında ortaya çıkan hataları yine de kaçırabilir. Bu hatalar genellikle mantıkla değil limitlerle ilgilidir.

Standart Testlerin Kaçırdıkları#

Alışıldık suite’in kapsadığı: Unit testler, integration testler ve bir load test; hepsi yeşil.

Kapsamadığı:

  • Construct’lar biriktikçe CloudFormation template’inin stack başına boyut limitini aşması
  • API Gateway integration timeout’unun Lambda timeout ayarlarıyla çakışması
  • Yoğun trafik altında DynamoDB throttling
  • JWT validation maliyetinin istek hacmiyle birlikte büyümesi

Production Odaklı Test Stratejisi#

Sentezlenen template üzerine yazılan assertion’lar, limit ve yapılandırma sorunlarını deploy öncesinde yakalar:

// test/infrastructure/api-stack.test.ts
import { Template, Match } from 'aws-cdk-lib/assertions';
import { App } from 'aws-cdk-lib';
import { ApiStack } from '../../lib/stacks/api-stack';

describe('ApiStack', () => {
  let template: Template;

  beforeAll(() => {
    const app = new App();
    const stack = new ApiStack(app, 'TestStack', {
      config: testConfig,
    });
    template = Template.fromStack(stack);
  });

  test('Lambda functions have correct runtime', () => {
    template.allResourcesProperties('AWS::Lambda::Function', {
      Runtime: 'nodejs20.x',
    });
  });

  test('API Gateway has throttling enabled', () => {
    template.hasResourceProperties('AWS::ApiGateway::Stage', {
      ThrottlingRateLimit: Match.anyValue(),
      ThrottlingBurstLimit: Match.anyValue(),
    });
  });

  test('DynamoDB tables have point-in-time recovery', () => {
    template.allResourcesProperties('AWS::DynamoDB::Table', {
      PointInTimeRecoverySpecification: {
        PointInTimeRecoveryEnabled: true,
      },
    });
  });
});

Integration Testing#

// test/integration/api.test.ts
import { CloudFormationClient, ListExportsCommand } from '@aws-sdk/client-cloudformation';
import axios from 'axios';

describe('API Integration Tests', () => {
  let apiEndpoint: string;
  let authToken: string;

  beforeAll(async () => {
    // Deploy edilmiş API endpoint'ini al
    const cf = new CloudFormationClient({});
    const exports = await cf.send(new ListExportsCommand({}));
    apiEndpoint = exports.Exports?.find(
      e => e.Name === 'ApiStack-endpoint'
    )?.Value!;

    // Auth token al
    authToken = await getTestAuthToken();
  });

  test('Health check endpoint', async () => {
    const response = await axios.get(`${apiEndpoint}/health`);
    expect(response.status).toBe(200);
    expect(response.data).toEqual({ status: 'healthy' });
  });

  test('Create and retrieve user', async () => {
    // User oluştur
    const createResponse = await axios.post(
      `${apiEndpoint}/users`,
      { name: 'Test User', email: 'test@example.com' },
      { headers: { Authorization: `Bearer ${authToken}` } }
    );
    expect(createResponse.status).toBe(201);

    // User'ı getir
    const userId = createResponse.data.userId;
    const getResponse = await axios.get(
      `${apiEndpoint}/users/${userId}`,
      { headers: { Authorization: `Bearer ${authToken}` } }
    );
    expect(getResponse.data.name).toBe('Test User');
  });
});

Load Testing#

// test/load/k6-script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const errorRate = new Rate('errors');

export const options = {
  stages: [
    { duration: '2m', target: 100 },  // Ramp up
    { duration: '5m', target: 100 },  // Sustain
    { duration: '2m', target: 200 },  // Spike
    { duration: '5m', target: 200 },  // Sustain spike
    { duration: '2m', target: 0 },  // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // Request'lerin %95'i 500ms altında
    errors: ['rate<0.01'],  // Error oranı %1 altında
  },
};

export default function() {
  const response = http.get(`${__ENV.API_URL}/users`);

  const success = check(response, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  errorRate.add(!success);
  sleep(1);
}

Rollback Prosedürleri#

Otomatik Rollback#

// lib/constructs/deployment/safe-deployment.ts
import { Construct } from 'constructs';
import { Alarm, TreatMissingData } from 'aws-cdk-lib/aws-cloudwatch';
import { RestApi } from 'aws-cdk-lib/aws-apigateway';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { Topic } from 'aws-cdk-lib/aws-sns';
import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions';
import { LambdaSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';
import { CfnOutput } from 'aws-cdk-lib';

export class SafeDeployment extends Construct {
  constructor(scope: Construct, id: string, props: {
    api: RestApi;
    alarmThreshold: number;
    rollbackFunction: IFunction;
  }) {
    super(scope, id);

    // CloudWatch alarm oluştur
    const alarm = new Alarm(this, 'DeploymentAlarm', {
      metric: props.api.metricServerError(),
      threshold: props.alarmThreshold,
      evaluationPeriods: 2,
      treatMissingData: TreatMissingData.NOT_BREACHING,
    });

    // Bildirimler için SNS topic
    const topic = new Topic(this, 'RollbackTopic');
    alarm.addAlarmAction(new SnsAction(topic));

    // Otomatik rollback için Lambda
    topic.addSubscription(
      new LambdaSubscription(props.rollbackFunction)
    );

    // Manuel rollback komutu
    new CfnOutput(this, 'RollbackCommand', {
      value: `aws lambda invoke --function-name ${props.rollbackFunction.functionName} --payload '{"action":"rollback"}' response.json`,
    });
  }
}

// src/deployment/rollback-handler.ts
import { SNSEvent } from 'aws-lambda';
import { CodeDeployClient, StopDeploymentCommand } from '@aws-sdk/client-codedeploy';

// Yardımcı fonksiyonlar
async function switchTraffic(version: string): Promise<void> {
  // Traffic switching implementasyonu
}

async function notifySlack(message: { channel: string; message: string }): Promise<void> {
  // Slack bildirimi implementasyonu
}

export const handler = async (event: SNSEvent) => {
  console.log('Initiating rollback:', JSON.stringify(event, null, 2));

  const codedeploy = new CodeDeployClient({});

  // Mevcut deployment'ı durdur
  await codedeploy.send(new StopDeploymentCommand({
    deploymentId: process.env.CURRENT_DEPLOYMENT_ID,
    autoRollbackEnabled: true,
  }));

  // Traffic'i önceki versiyona döndür
  await switchTraffic('blue'); // Green'in başarısız olduğunu varsayarak

  // Ekibi bilgilendir
  await notifySlack({
    channel: '#alerts',
    message: 'Automatic rollback initiated due to high error rate',
  });
};

Performans Optimizasyonu#

Lambda Performans Tuning#

// lib/constructs/performance/optimized-function.ts
import { Construct } from 'constructs';
import { Duration, Stack } from 'aws-cdk-lib';
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Architecture, CfnFunction, CfnAlias } from 'aws-cdk-lib/aws-lambda';

// Temel fonksiyon arayüzü
interface ServerlessFunctionProps extends NodejsFunctionProps {
  config: {
    stage: string;
  };
}

export class OptimizedFunction extends NodejsFunction {
  constructor(scope: Construct, id: string, props: ServerlessFunctionProps & {
    enableProvisioning?: boolean;
    enableSnapStart?: boolean;
  }) {
    super(scope, id, {
      ...props,
      memorySize: props.memorySize || 1024,
      architecture: Architecture.ARM_64, // Daha iyi price/performance
      environment: {
        ...props.environment,
        NODE_OPTIONS: '--enable-source-maps --max-old-space-size=896',
        AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
      },
    });

    // Kritik fonksiyonlar için provisioned concurrency
    if (props.enableProvisioning && props.config.stage === 'prod') {
      const version = this.currentVersion;

      new CfnAlias(this, 'ProvisionedAlias', {
        functionName: this.functionName,
        functionVersion: version.version,
        name: 'provisioned',
        provisionedConcurrencyConfig: {
          provisionedConcurrentExecutions: 5,
        },
      });
    }

    // Java fonksiyonları için SnapStart
    if (props.enableSnapStart) {
      const cfnFunction = this.node.defaultChild as CfnFunction;
      cfnFunction.snapStart = {
        applyOn: 'PublishedVersions',
      };
    }
  }
}

API Gateway Optimizasyonu#

// lib/constructs/performance/cached-api.ts
import { Construct } from 'constructs';
import { Duration } from 'aws-cdk-lib';
import { RestApi, RestApiProps } from 'aws-cdk-lib/aws-apigateway';

export class CachedApi extends RestApi {
  constructor(scope: Construct, id: string, props: RestApiProps & {
    cacheConfig?: {
      ttlMinutes: number;
      encrypted: boolean;
      clusterSize: string;
    };
  }) {
    super(scope, id, {
      ...props,
      deployOptions: {
        ...props.deployOptions,
        cachingEnabled: true,
        cacheClusterEnabled: true,
        cacheClusterSize: props.cacheConfig?.clusterSize || '0.5',
        cacheDataEncrypted: props.cacheConfig?.encrypted ?? true,
        cacheTtl: Duration.minutes(props.cacheConfig?.ttlMinutes || 5),
        methodOptions: {
          '/*/*': {
            cachingEnabled: true,
            cacheKeyParameters: [
              'method.request.path.proxy',
              'method.request.querystring.page',
            ],
          },
        },
      },
    });
  }
}

Monitoring ve Gözlemlenebilirlik#

Kapsamlı Monitoring Stack#

// lib/stacks/monitoring-stack.ts
import { Stack, StackProps, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Dashboard, GraphWidget, Alarm } from 'aws-cdk-lib/aws-cloudwatch';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

// ApiStack için arayüz
interface ApiStack extends Stack {
  api: any; // aws-apigateway'den RestApi
  functions: NodejsFunction[];
}

export class MonitoringStack extends Stack {
  constructor(scope: Construct, id: string, props: {
    apiStack: ApiStack;
    stage: string;
  }) {
    super(scope, id);

    // Dashboard oluştur
    const dashboard = new Dashboard(this, 'ServiceDashboard', {
      dashboardName: `my-service-${props.stage}`,
    });

    // API metrikleri
    dashboard.addWidgets(
      new GraphWidget({
        title: 'API Requests',
        left: [props.apiStack.api.metricCount()],
        right: [props.apiStack.api.metricLatency()],
      }),
      new GraphWidget({
        title: 'API Errors',
        left: [
          props.apiStack.api.metric4XXError(),
          props.apiStack.api.metric5XXError(),
        ],
      })
    );

    // Lambda metrikleri
    const lambdaWidgets = props.apiStack.functions.map(fn =>
      new GraphWidget({
        title: `${fn.functionName} Performance`,
        left: [fn.metricInvocations()],
        right: [fn.metricDuration()],
      })
    );
    dashboard.addWidgets(...lambdaWidgets);

    // Alarmlar
    this.createAlarms(props.apiStack);
  }

  private createAlarms(apiStack: ApiStack) {
    // API Gateway alarmları
    new Alarm(this, 'HighErrorRate', {
      metric: apiStack.api.metric5XXError({
        period: Duration.minutes(5),
        statistic: 'Sum',
      }),
      threshold: 10,
      evaluationPeriods: 2,
    });

    // Lambda alarmları
    apiStack.functions.forEach(fn => {
      new Alarm(this, `${fn.node.id}Throttles`, {
        metric: fn.metricThrottles(),
        threshold: 5,
        evaluationPeriods: 2,
      });

      new Alarm(this, `${fn.node.id}Errors`, {
        metric: fn.metricErrors(),
        threshold: 10,
        evaluationPeriods: 2,
      });
    });
  }
}

Distributed Tracing#

// lib/constructs/observability/tracing.ts
import { Construct } from 'constructs';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Tracing } from 'aws-cdk-lib/aws-lambda';
import { OptimizedFunction } from '../performance/optimized-function';

// Temel fonksiyon arayüzü
interface ServerlessFunctionProps {
  environment?: Record<string, string>;
}

export class TracedFunction extends OptimizedFunction {
  constructor(scope: Construct, id: string, props: ServerlessFunctionProps) {
    super(scope, id, {
      ...props,
      tracing: Tracing.ACTIVE,
      environment: {
        ...props.environment,
        // _X_AMZN_TRACE_ID Lambda tarafından rezerve edilmiştir ve her
        // invocation'da enjekte edilir; burada set etmek deploy anında reddedilir.
        AWS_XRAY_CONTEXT_MISSING: 'LOG_ERROR',
        AWS_XRAY_LOG_LEVEL: 'error',
      },
    });

    // X-Ray izinleri ekle
    this.addToRolePolicy(new PolicyStatement({
      actions: [
        'xray:PutTraceSegments',
        'xray:PutTelemetryRecords',
      ],
      resources: ['*'],
    }));
  }
}

// src/libs/tracing.ts
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({
  serviceName: process.env.SERVICE_NAME || 'my-service',
});

export function traceMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = async function(...args: any[]) {
    const segment = tracer.getSegment();
    const subsegment = segment?.addNewSubsegment(propertyKey);

    try {
      const result = await originalMethod.apply(this, args);
      subsegment?.close();
      return result;
    } catch (error) {
      subsegment?.addError(error as Error);
      subsegment?.close();
      throw error;
    }
  };

  return descriptor;
}

Migration Checklist’i#

Pre-Migration#

  • Mevcut kaynakları envantere al

    • Tüm Lambda fonksiyonlarını belgele
    • API Gateway endpoint’lerini listele
    • DynamoDB tablolarını ve indexlerini mapla
    • Custom kaynakları tanımla
    • Tüm environment variable’ları ve secret’ları not et
  • Dependency’leri değerlendir

    • Kullanımdaki Serverless plugin’lerini gözden geçir
    • Custom CloudFormation kaynaklarını kontrol et
    • External servis entegrasyonlarını tanımla
    • IAM role’leri ve policy’leri belgele
  • Migration stratejisini planla

    • Migration pattern’ı seç (big bang, strangler fig, blue-green)
    • Rollback prosedürlerini tanımla
    • Başarı kriterlerini belirle
    • Gerekirse maintenance window’ları planla

Migration Sırasında#

  • CDK projesini kur

    • CDK ile repository’i initialize et
    • Environment’ları yapılandır
    • CI/CD pipeline’larını kur
    • Infrastructure testlerini implement et
  • Componentleri migrate et

    • Stateless kaynaklarla başla
    • Mevcut stateful kaynakları import et
    • Lambda fonksiyonlarını migrate et
    • API Gateway’i kur
    • Authentication’ı yapılandır
  • Testing

    • Unit testleri çalıştır
    • Integration testlerini yürüt
    • Load testing yap
    • Güvenlik konfigürasyonlarını validate et

Post-Migration#

  • Monitor et ve optimize et

    • Kapsamlı monitoring kur
    • Alert’leri yapılandır
    • Performans metriklerini gözden geçir
    • Cold start’ları optimize et
  • Dokümantasyon

    • Runbook’ları güncelle
    • Yeni deployment prosedürlerini belgele
    • Mimari diyagramlar oluştur
    • Ekibi CDK konusunda eğit
  • Temizlik

    • Eski Serverless Framework kaynaklarını kaldır
    • Kullanılmayan IAM role’leri sil
    • S3 deployment bucket’larını temizle
    • DNS kayıtlarını güncelle

Yaygın Tuzaklar ve Çözümler#

1. Kaynak Adlandırma Çakışmaları#

// Hardcoded isimlerden kaçın
// Kötü
const table = new Table(this, 'Table', {
  tableName: 'users-table', // Varsa çakışır
});

// İyi
const table = new Table(this, 'Table', {
  tableName: `${props.serviceName}-${props.stage}-users`,
});

2. State Management#

// Stateful ve stateless kaynakları ayır
import { App } from 'aws-cdk-lib';

const app = new App();

// Stateful kaynaklar ayrı stack'te
const dataStack = new DataStack(app, 'DataStack', {
  terminationProtection: true,
});

// Stateless kaynaklar serbestçe güncellenebilir
const apiStack = new ApiStack(app, 'ApiStack', {
  tables: dataStack.tables,
});

3. Environment Variable Migration#

// Serverless variable'larını CDK'ya mapla
import { Stack, Fn } from 'aws-cdk-lib';

const legacyMappings: Record<string, string> = {
  '${self:service}': props.serviceName,
  '${opt:stage}': props.stage,
  '${opt:region}': Stack.of(this).region,
  '${cf:OtherStack.Output}': Fn.importValue('OtherStack-Output'),
};

Serverless Framework’te Kalmanın Uygun Olduğu Durumlar#

Bazı senaryolarda Serverless Framework, CDK’dan daha uygun kalır:

  1. Minimal customization gerektiren basit CRUD uygulamaları
  2. Hızlı prototipleme gereken proof-of-concept projeler
  3. TypeScript deneyimi ve eğitim için zamanı olmayan ekipler
  4. CDK’da karşılığı olmayan ağır plugin bağımlılıkları taşıyan uygulamalar
  5. Yalnızca YAML’a izin veren altyapı politikaları olan organizasyonlar

Varsayılan Nerede Geçerli#

Production geçişinde varsayılan, alarma bağlı otomatik rollback’li blue-green’dir. Uygulama, bakım penceresinin bedelini dürüstçe karşılayabilecek kadar küçükse ya da auth katmanını tutarlı tutmanın tek yolu strangler fig geçişi olacak kadar büyükse bu varsayılanı değiştirin. Planın geri kalanı her iki durumda da ayakta kalır: stateful kaynakları yeniden oluşturmak yerine import edin, sentezlenen template üzerine assertion yazın ve production trafiği bağlanmadan önce rollback’i bir staging hesabında bir kez çalıştırın.

YAML yapılandırmasından TypeScript’e geçmek altyapıya derleme, test ve code review getirir. Bu kazanç ancak testler CI’da koşuyorsa ve rollback yolu en az bir kez denenmişse ortaya çıkar. Bu ikisi olmadan CDK, CloudFormation yazmanın daha pahalı bir yoludur.

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 6/6 yazı tamamlandı

İlgili yazılar