İçeriğe atla

2025-09-05

AWS CDK Link Shortener Bölüm 4: Production Deployment ve Optimizasyon

Multi-environment deployment stratejileri, ölçekte performans optimizasyonu, ve maliyet yönetimi. Production deneyimleri ve öğrenilen dersler ile doğru monitoring ve incident response pattern'ları.

Production Deployment ve Optimizasyon

Production optimizasyonu işleri hızlandırmanın ötesinde - herhangi bir yük koşulu altında tahmin edilebilir performans gerektirir. Trafik beklenmedik şekilde arttığında, staging’de mükemmel çalışan infrastructure production’da ölçeklendirme darboğazlarını ortaya çıkarabilir.

En yaygın gözden kaçırılan şey? Database’in peak yükler yerine steady-state trafik için provisionlanması. Normal operasyonlar için optimize edilmiş bir DynamoDB table, kampanyalar veya ürün lansmanları sırasında trafik 10x arttığında darboğaz haline gelebilir.

Bölüm 1-3’te temel, core fonksiyon ve güvenlik inşa ettik. Şimdi onu production deployment ve optimizasyon için kurşun geçirmez yapalım.

Multi-Environment Deployment: Dev ve Prod’un Ötesinde

Çoğu tutorial sana dev ve prod environment’larını gösterir. Gerçekte, en az dörte ihtiyacın var: dev, staging, pre-prod, ve production. İşte neden ve nasıl inşa ettiğin:

// bin/link-shortener.ts - Bizi lansman gününden geçiren app entry point'i
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { LinkShortenerStack } from '../lib/link-shortener-stack';
import { DatabaseStack } from '../lib/database-stack';
import { MonitoringStack } from '../lib/monitoring-stack';

const app = new cdk.App();

// Takımınla ölçeklenen environment konfigürasyonu
const environments = {
  dev: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: 'us-west-2', // Dev için daha ucuz region
    stage: 'dev',
    domain: 'dev-links.yourcompany.com',
    customDomain: false,
    monitoring: {
      detailedMetrics: false,
      logRetention: 7, // Günler
      alerting: false,
    },
    database: {
      billingMode: 'PAY_PER_REQUEST',
      pointInTimeRecovery: false,
      backupRetention: 7,
    },
    lambda: {
      reservedConcurrency: 10, // Dev maliyetlerini sınırla
      memorySize: 512,
      timeout: 30,
    }
  },
  
  staging: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: 'us-east-1',
    stage: 'staging',
    domain: 'staging-links.yourcompany.com',
    customDomain: true,
    monitoring: {
      detailedMetrics: true,
      logRetention: 14,
      alerting: true,
    },
    database: {
      billingMode: 'PAY_PER_REQUEST',
      pointInTimeRecovery: true,
      backupRetention: 14,
    },
    lambda: {
      reservedConcurrency: 50,
      memorySize: 1024,
      timeout: 30,
    }
  },

  'pre-prod': {
    account: process.env.CDK_PREPROD_ACCOUNT,
    region: 'us-east-1',
    stage: 'pre-prod',
    domain: 'pp-links.yourcompany.com',
    customDomain: true,
    monitoring: {
      detailedMetrics: true,
      logRetension: 30,
      alerting: true,
    },
    database: {
      billingMode: 'PROVISIONED', // Production pattern'larını match et
      readCapacity: 100,
      writeCapacity: 50,
      pointInTimeRecovery: true,
      backupRetention: 30,
    },
    lambda: {
      reservedConcurrency: 200,
      memorySize: 1024,
      timeout: 30,
    }
  },

  production: {
    account: process.env.CDK_PROD_ACCOUNT,
    region: 'us-east-1',
    stage: 'prod',
    domain: 'go.yourcompany.com',
    customDomain: true,
    monitoring: {
      detailedMetrics: true,
      logRetention: 90,
      alerting: true,
      dashboard: true,
    },
    database: {
      billingMode: 'PROVISIONED',
      readCapacity: 500, // Conservative başla, auto-scale up
      writeCapacity: 200,
      pointInTimeRecovery: true,
      backupRetention: 90,
      globalTables: true, // Multi-region disaster recovery
    },
    lambda: {
      reservedConcurrency: 1000,
      memorySize: 1024,
      timeout: 30,
      provisionedConcurrency: 10, // Bazı function'ları warm tut
    }
  }
};

const stage = app.node.tryGetContext('stage') || 'dev';
const config = environments[stage as keyof typeof environments];

if (!config) {
  throw new Error(`Invalid stage: ${stage}. Available stages: ${Object.keys(environments).join(', ')}`);
}

// Dependency'lerle mantıklı sırada deploy et
const databaseStack = new DatabaseStack(app, `LinkShortener-Database-${stage}`, {
  env: { account: config.account, region: config.region },
  stage,
  config: config.database,
});

const appStack = new LinkShortenerStack(app, `LinkShortener-App-${stage}`, {
  env: { account: config.account, region: config.region },
  stage,
  config,
  database: databaseStack.database,
});

// Monitoring'i sadece staging+ environment'larda deploy et
if (stage !== 'dev') {
  new MonitoringStack(app, `LinkShortener-Monitoring-${stage}`, {
    env: { account: config.account, region: config.region },
    stage,
    config: config.monitoring,
    appStack,
  });
}

Neden dört environment? Her biri belirli bir amaca hizmet eder:

  • Dev: Deneyim için maliyet kontrolleri ile geliştirme izolasyonu
  • Staging: Production benzeri veriler ve konfigurasyonlarla integration testi
  • Pre-prod: Load testing ve final validasyon için production replikası
  • Production: Tam monitoring ve redundancy ile canlı environment

Performans Optimizasyonu: Lambda Cold Start’ları ve Daha Fazlası

50 milyon redirect’i process ettikten sonra, gerçekten ibre’yi hareket ettiren optimizasyonlar bunlar:

1. Önemli Lambda Konfigürasyonu

// lib/constructs/optimized-lambda.ts
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';

export interface OptimizedLambdaProps {
  entry: string;
  stage: string;
  reservedConcurrency?: number;
  provisionedConcurrency?: number;
  memorySize?: number;
}

export class OptimizedLambda extends Construct {
  public readonly function: nodejs.NodejsFunction;

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

    this.function = new nodejs.NodejsFunction(this, 'Function', {
      entry: props.entry,
      handler: 'handler',
      runtime: lambda.Runtime.NODEJS_20_X,
      
      // Memory konfigürasyonu CPU'yu etkiler - çoğu workload için sweet spot
      memorySize: props.memorySize || 1024,
      
      // Hızlı fail için yeterince agresif timeout
      timeout: cdk.Duration.seconds(30),
      
      // Optimizasyon için environment variable'lar
      environment: {
        NODE_OPTIONS: '--enable-source-maps',
        AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', // TCP bağlantılarını yeniden kullan
        POWERTOOLS_SERVICE_NAME: 'link-shortener',
        POWERTOOLS_METRICS_NAMESPACE: 'LinkShortener',
      },

      // Bundle optimizasyonu
      bundling: {
        minify: true,
        sourceMap: true,
        target: 'es2022',
        format: nodejs.OutputFormat.ESM,
        banner: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);',
        externalModules: [
          '@aws-sdk/*', // AWS SDK'yı bundle'lama
        ],
        esbuildArgs: {
          '--tree-shaking': 'true',
          '--platform': 'node',
          '--target': 'node20',
        },
      },

      // Bir function'ın tüm kapasiteyi yemesini engellemek için reserved concurrency
      reservedConcurrency: props.reservedConcurrency,

      // VPC konfigürasyonu sadece ihtiyacın varsa (cold start'lara 1-2s ekler)
      // vpc: props.stage === 'prod' ? vpc : undefined,
    });

    // Production kritik path'ler için provisioned concurrency
    if (props.provisionedConcurrency && props.stage === 'prod') {
      const version = this.function.currentVersion;
      
      new lambda.Alias(this, 'ProductionAlias', {
        aliasName: 'prod',
        version,
        provisionedConcurrencyConfig: {
          provisionedConcurrentExecutions: props.provisionedConcurrency,
        },
      });
    }

    // Performans insight'ları için X-Ray tracing
    this.function.addEnvironment('_X_AMZN_TRACE_ID', '${_X_AMZN_TRACE_ID}');
  }
}

2. Gerçekten Çalışan Connection Pooling

Bulduğumuz bir numaralı performans katili? Her invocation’da yeni DynamoDB bağlantıları yaratmak. İşte production connection manager’ımız:

// src/utils/dynamodb-connection.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

// Global connection pool - Lambda invocation'ları arasında yaşar
let dynamoClient: DynamoDBDocumentClient | null = null;

export function getDynamoClient(): DynamoDBDocumentClient {
  if (!dynamoClient) {
    const client = new DynamoDBClient({
      region: process.env.AWS_REGION,
      
      // Connection pooling konfigürasyonu
      maxAttempts: 3,
      requestHandler: {
        // Lambda runtime için optimize
        connectionTimeout: 1000, // 1s timeout
        requestTimeout: 5000,  // 5s toplam request timeout
        
        // Connection pooling
        httpsAgent: {
          maxSockets: 10,  // Default 50'den düşürüldü
          keepAlive: true,
          keepAliveMsecs: 30000,
        },
      },
      
      // Credential'ların client-side caching'i
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        sessionToken: process.env.AWS_SESSION_TOKEN,
      },
    });

    dynamoClient = DynamoDBDocumentClient.from(client, {
      marshallOptions: {
        convertEmptyValues: false,
        removeUndefinedValues: true,
        convertClassInstanceToMap: false,
      },
      unmarshallOptions: {
        wrapNumbers: false,
      },
    });

    // Debugging için connection yaratımını logla
    console.log('DynamoDB connection pool initialized');
  }

  return dynamoClient;
}

// Performans monitoring wrapper
export async function withPerformanceLogging<T>(
  operation: string,
  fn: () => Promise<T>
): Promise<T> {
  const start = Date.now();
  
  try {
    const result = await fn();
    const duration = Date.now() - start;
    
    console.log(JSON.stringify({
      operation,
      duration,
      success: true,
      timestamp: new Date().toISOString(),
    }));
    
    return result;
  } catch (error) {
    const duration = Date.now() - start;
    
    console.error(JSON.stringify({
      operation,
      duration,
      success: false,
      error: error instanceof Error ? error.message : String(error),
      timestamp: new Date().toISOString(),
    }));
    
    throw error;
  }
}

3. Production-Optimize Edilmiş Redirect Handler

// src/handlers/redirect-optimized.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { getDynamoClient, withPerformanceLogging } from '../utils/dynamodb-connection';
import { GetCommand } from '@aws-sdk/lib-dynamodb';

// Cold start tracking'i handler'ın dışında declare et
let isColdStart = true;

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  const startTime = Date.now();
  const coldStart = isColdStart;
  isColdStart = false;

  // Path'den short code'u extract et
  const shortCode = event.pathParameters?.proxy || event.pathParameters?.shortCode;
  
  if (!shortCode) {
    return createErrorResponse(404, 'Short code not found');
  }

  try {
    const dynamodb = getDynamoClient();
    
    // Projection ile optimize edilmiş DynamoDB query
    const result = await withPerformanceLogging(
      'GetShortUrl',
      () => dynamodb.send(new GetCommand({
        TableName: process.env.URLS_TABLE_NAME!,
        Key: { shortCode },
        ProjectionExpression: 'originalUrl, expiresAt, clickCount',
        ConsistentRead: false, // Eventually consistent redirect'ler için yeterli
      }))
    );

    if (!result.Item) {
      // Analytics için 404 logla ama block etme
      logAnalyticsAsync('404', shortCode, event).catch(console.error);
      return createErrorResponse(404, 'Link not found');
    }

    const { originalUrl, expiresAt } = result.Item;
    
    // Expiration kontrolü
    if (expiresAt && Date.now() > expiresAt) {
      logAnalyticsAsync('EXPIRED', shortCode, event).catch(console.error);
      return createErrorResponse(410, 'Link has expired');
    }

    // Click count'u asynchronous olarak update et (fire-and-forget)
    updateClickCountAsync(shortCode).catch(console.error);
    
    // Başarılı redirect'i logla
    logAnalyticsAsync('SUCCESS', shortCode, event).catch(console.error);

    const responseTime = Date.now() - startTime;

    // Monitoring için structured logging
    console.log(JSON.stringify({
      event: 'redirect_success',
      shortCode,
      responseTime,
      coldStart,
      userAgent: event.headers['User-Agent']?.substring(0, 100),
      referer: event.headers['Referer']?.substring(0, 100),
      timestamp: new Date().toISOString(),
    }));

    return {
      statusCode: 301, // Caching için permanent redirect
      headers: {
        Location: originalUrl,
        'Cache-Control': 'public, max-age=300, s-maxage=3600', // 5dk browser, 1sa CDN
        'X-Response-Time': responseTime.toString(),
        'X-Cold-Start': coldStart.toString(),
      },
      body: '',
    };

  } catch (error) {
    const responseTime = Date.now() - startTime;
    
    console.error(JSON.stringify({
      event: 'redirect_error',
      shortCode,
      error: error instanceof Error ? error.message : String(error),
      responseTime,
      coldStart,
      timestamp: new Date().toISOString(),
    }));

    return createErrorResponse(500, 'Internal server error');
  }
};

async function updateClickCountAsync(shortCode: string): Promise<void> {
  try {
    const dynamodb = getDynamoClient();
    
    await dynamodb.send(new UpdateCommand({
      TableName: process.env.URLS_TABLE_NAME!,
      Key: { shortCode },
      UpdateExpression: 'ADD clickCount :inc SET lastClickAt = :timestamp',
      ExpressionAttributeValues: {
        ':inc': 1,
        ':timestamp': Date.now(),
      },
    }));
  } catch (error) {
    // Analytics update başarısız olursa redirect'i fail etme
    console.error('Failed to update click count:', error);
  }
}

async function logAnalyticsAsync(
  eventType: string,
  shortCode: string,
  event: APIGatewayProxyEvent
): Promise<void> {
  // Async analytics logging için implementation
  // Bu tipik olarak ayrı bir analytics table'a veya queue'ya yazar
}

function createErrorResponse(statusCode: number, message: string): APIGatewayProxyResult {
  return {
    statusCode,
    headers: {
      'Content-Type': 'text/html',
      'Cache-Control': 'no-cache',
    },
    body: `
      <!DOCTYPE html>
      <html>
        <head><title>Error</title></head>
        <body style="font-family: Arial, sans-serif; text-align: center; margin-top: 100px;">
          <h1>${statusCode}</h1>
          <p>${message}</p>
        </body>
      </html>
    `,
  };
}

Maliyet Optimizasyonu: Pahalı Hatalardan Dersler

Trafik kalıpları beklenmedik şekilde değiştiğinde maliyet optimizasyonu kritik hale gelir. Farklı AWS servislerinin nasıl ölçeklendiğini ve faturalandığını anlamak, yüksek trafik dönemlerinde bütçe sürprizlerini önlemeye yardımcı olur:

1. DynamoDB Optimizasyon Stratejisi

// lib/database-stack-optimized.ts
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as applicationautoscaling from 'aws-cdk-lib/aws-applicationautoscaling';
import { Construct } from 'constructs';

export class OptimizedDatabaseStack extends Construct {
  public readonly linksTable: dynamodb.Table;

  constructor(scope: Construct, id: string, props: {
    stage: string;
    expectedReadsPerSecond: number;
    expectedWritesPerSecond: number;
  }) {
    super(scope, id);

    this.linksTable = new dynamodb.Table(this, 'LinksTable', {
      partitionKey: {
        name: 'shortCode',
        type: dynamodb.AttributeType.STRING,
      },
      
      // On-demand ile başla, pattern'ları anladığın zaman provisioned'a geç
      billingMode: props.stage === 'prod' 
        ? dynamodb.BillingMode.PROVISIONED 
        : dynamodb.BillingMode.PAY_PER_REQUEST,
      
      // Production için provisioned capacity
      ...(props.stage === 'prod' && {
        readCapacity: Math.max(5, Math.ceil(props.expectedReadsPerSecond * 1.2)),
        writeCapacity: Math.max(5, Math.ceil(props.expectedWritesPerSecond * 1.2)),
      }),

      pointInTimeRecovery: props.stage === 'prod',
      deletionProtection: props.stage === 'prod',
      
      // Compliance için encryption
      encryption: dynamodb.TableEncryption.AWS_MANAGED,
      
      stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES, // Analytics için
    });

    // Production için auto-scaling
    if (props.stage === 'prod') {
      this.setupAutoScaling();
    }

    // Analytics query'leri için Global Secondary Index
    this.linksTable.addGlobalSecondaryIndex({
      indexName: 'UserIndex',
      partitionKey: {
        name: 'userId',
        type: dynamodb.AttributeType.STRING,
      },
      sortKey: {
        name: 'createdAt',
        type: dynamodb.AttributeType.NUMBER,
      },
      projectionType: dynamodb.ProjectionType.KEYS_ONLY, // Maliyetleri minimize et
      
      // Ana table ile aynı billing mode
      ...(props.stage === 'prod' && {
        readCapacity: Math.max(5, Math.ceil(props.expectedReadsPerSecond * 0.1)),
        writeCapacity: Math.max(5, Math.ceil(props.expectedWritesPerSecond * 1.0)),
      }),
    });
  }

  private setupAutoScaling(): void {
    // Read capacity auto-scaling
    const readScaling = this.linksTable.autoScaleReadCapacity({
      minCapacity: 5,
      maxCapacity: 1000, // Makul tavan
    });

    readScaling.scaleOnUtilization({
      targetUtilizationPercent: 70, // Conservative target
      scaleInCooldown: cdk.Duration.minutes(5),
      scaleOutCooldown: cdk.Duration.minutes(1),
    });

    // Write capacity auto-scaling
    const writeScaling = this.linksTable.autoScaleWriteCapacity({
      minCapacity: 5,
      maxCapacity: 500,
    });

    writeScaling.scaleOnUtilization({
      targetUtilizationPercent: 70,
      scaleInCooldown: cdk.Duration.minutes(5),
      scaleOutCooldown: cdk.Duration.minutes(1),
    });
  }
}

2. Maksimum Maliyet Verimliliği için CloudFront Konfigürasyonu

// lib/cdn-stack-optimized.ts
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import { Construct } from 'constructs';

export class OptimizedCDNStack extends Construct {
  public readonly distribution: cloudfront.Distribution;

  constructor(scope: Construct, id: string, props: {
    apiGateway: apigateway.RestApi;
    stage: string;
  }) {
    super(scope, id);

    this.distribution = new cloudfront.Distribution(this, 'Distribution', {
      defaultBehavior: {
        origin: new origins.RestApiOrigin(props.apiGateway),
        
        // Redirect'ler için optimize edilmiş caching policy
        cachePolicy: new cloudfront.CachePolicy(this, 'RedirectCachePolicy', {
          cachePolicyName: `link-shortener-${props.stage}`,
          defaultTtl: cdk.Duration.minutes(5),
          maxTtl: cdk.Duration.hours(24),
          minTtl: cdk.Duration.minutes(1),
          
          // Sadece path'e göre cache (query string'leri ve header'ları ignore et)
          queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
          headerBehavior: cloudfront.CacheHeaderBehavior.none(),
          cookieBehavior: cloudfront.CacheCookieBehavior.none(),
        }),

        // Compression bandwidth maliyetlerini azaltır
        compress: true,
        
        // Redirect'ler için sadece GET request'lerine izin ver
        allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
        cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD,

        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      },

      // API endpoint'leri için ek behavior (caching yok)
      additionalBehaviors: {
        '/api/*': {
          origin: new origins.RestApiOrigin(props.apiGateway),
          cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
          viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
          allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
        },
      },

      // Kritik olmayan uygulamalar için en ucuz price class'ı kullan
      priceClass: props.stage === 'prod' 
        ? cloudfront.PriceClass.PRICE_CLASS_100  // ABD, Kanada, Avrupa
        : cloudfront.PriceClass.PRICE_CLASS_100,

      // Error handling
      errorResponses: [
        {
          httpStatus: 404,
          responseHttpStatus: 404,
          responsePagePath: '/404.html',
          ttl: cdk.Duration.minutes(5), // Origin'i döve döve yemesini engellemek için 404'leri cache'le
        },
        {
          httpStatus: 500,
          responseHttpStatus: 500,
          responsePagePath: '/500.html',
          ttl: cdk.Duration.minutes(1), // Server error'lar için kısa cache
        },
      ],

      // Analytics için logging'i enable et (ek maliyet ama insight'lar için gerekli)
      ...(props.stage === 'prod' && {
        enableLogging: true,
        logBucket: s3.Bucket.fromBucketName(this, 'LogsBucket', `cloudfront-logs-${props.stage}`),
        logFilePrefix: 'link-shortener/',
      }),
    });
  }
}

3. Maliyet Monitoring ve Alert’ler

// lib/cost-monitoring-stack.ts
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import { Construct } from 'constructs';

export class CostMonitoringStack extends Construct {
  constructor(scope: Construct, id: string, props: {
    stage: string;
    alertEmail: string;
    monthlyBudget: number;
  }) {
    super(scope, id);

    // Maliyet alert'leri için SNS topic
    const alertTopic = new sns.Topic(this, 'CostAlerts', {
      displayName: `Link Shortener Cost Alerts - ${props.stage}`,
    });

    alertTopic.addSubscription(
      new subscriptions.EmailSubscription(props.alertEmail)
    );

    // DynamoDB maliyet monitoring
    const dynamoReadAlarm = new cloudwatch.Alarm(this, 'DynamoReadUnitsHigh', {
      metric: new cloudwatch.Metric({
        namespace: 'AWS/DynamoDB',
        metricName: 'ConsumedReadCapacityUnits',
        dimensionsMap: {
          TableName: 'LinksTable', // Gerçek table adıyla replace et
        },
        statistic: 'Sum',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 1000, // Bütçene göre ayarla
      evaluationPeriods: 2,
      alarmDescription: 'DynamoDB read capacity kullanımı yüksek',
    });

    dynamoReadAlarm.addAlarmAction(
      new actions.SnsAction(alertTopic)
    );

    // Lambda invocation maliyet monitoring
    const lambdaInvocationsAlarm = new cloudwatch.Alarm(this, 'LambdaInvocationsHigh', {
      metric: new cloudwatch.Metric({
        namespace: 'AWS/Lambda',
        metricName: 'Invocations',
        dimensionsMap: {
          FunctionName: 'redirect-handler', // Gerçek function adıyla replace et
        },
        statistic: 'Sum',
        period: cdk.Duration.hours(1),
      }),
      threshold: 100000, // Saatte 100k invocation
      evaluationPeriods: 1,
      alarmDescription: 'Lambda invocation'lar alışılmadık derecede yüksek',
    });

    lambdaInvocationsAlarm.addAlarmAction(
      new actions.SnsAction(alertTopic)
    );

    // Maliyet dashboard'u yarat
    new cloudwatch.Dashboard(this, 'CostDashboard', {
      dashboardName: `LinkShortener-Costs-${props.stage}`,
      widgets: [
        [
          new cloudwatch.GraphWidget({
            title: 'DynamoDB Read Capacity Units',
            left: [dynamoReadAlarm.metric],
            width: 12,
          }),
        ],
        [
          new cloudwatch.GraphWidget({
            title: 'Lambda Invocations',
            left: [lambdaInvocationsAlarm.metric],
            width: 12,
          }),
        ],
        [
          new cloudwatch.GraphWidget({
            title: 'CloudFront Requests',
            left: [
              new cloudwatch.Metric({
                namespace: 'AWS/CloudFront',
                metricName: 'Requests',
                statistic: 'Sum',
                period: cdk.Duration.hours(1),
              }),
            ],
            width: 12,
          }),
        ],
      ],
    });
  }
}

Production Monitoring: “Çalışıyor”un Ötesinde

En büyük incident’ımız sırasında bizi kurtaran monitoring:

1. Önemli Custom Metric’ler

// src/utils/metrics.ts
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';

const cloudwatch = new CloudWatchClient({ region: process.env.AWS_REGION });

export class MetricsCollector {
  private namespace = 'LinkShortener/Production';
  private metrics: Array<{
    MetricName: string;
    Value: number;
    Unit: string;
    Timestamp: Date;
    Dimensions?: Array<{ Name: string; Value: string }>;
  }> = [];

  async recordRedirectSuccess(shortCode: string, responseTime: number, coldStart: boolean): Promise<void> {
    this.metrics.push(
      {
        MetricName: 'RedirectResponseTime',
        Value: responseTime,
        Unit: 'Milliseconds',
        Timestamp: new Date(),
        Dimensions: [
          { Name: 'ColdStart', Value: coldStart.toString() },
        ],
      },
      {
        MetricName: 'RedirectCount',
        Value: 1,
        Unit: 'Count',
        Timestamp: new Date(),
        Dimensions: [
          { Name: 'Status', Value: 'Success' },
        ],
      }
    );

    await this.flush();
  }

  async recordDatabaseLatency(operation: string, latency: number): Promise<void> {
    this.metrics.push({
      MetricName: 'DatabaseLatency',
      Value: latency,
      Unit: 'Milliseconds',
      Timestamp: new Date(),
      Dimensions: [
        { Name: 'Operation', Value: operation },
      ],
    });

    await this.flush();
  }

  async recordError(errorType: string, shortCode?: string): Promise<void> {
    this.metrics.push({
      MetricName: 'ErrorCount',
      Value: 1,
      Unit: 'Count',
      Timestamp: new Date(),
      Dimensions: [
        { Name: 'ErrorType', Value: errorType },
        ...(shortCode ? [{ Name: 'ShortCode', Value: shortCode }] : []),
      ],
    });

    await this.flush();
  }

  private async flush(): Promise<void> {
    if (this.metrics.length === 0) return;

    try {
      await cloudwatch.send(new PutMetricDataCommand({
        Namespace: this.namespace,
        MetricData: this.metrics,
      }));

      this.metrics = []; // Başarılı send'den sonra temizle
    } catch (error) {
      console.error('Failed to send metrics:', error);
      // Throw etme - metric başarısızlıkları ana fonksiyonu bozmamalı
    }
  }
}

// Singleton instance
export const metrics = new MetricsCollector();

2. Gerçekliği Simüle Eden Load Testing

// tests/load-test.ts - Lansman günü sorunumuzu yakalayacak test
import { performance } from 'perf_hooks';

interface LoadTestConfig {
  baseUrl: string;
  concurrentUsers: number;
  testDurationMs: number;
  rampUpMs: number;
  shortCodes: string[];
}

interface LoadTestResult {
  totalRequests: number;
  successfulRequests: number;
  failedRequests: number;
  averageResponseTime: number;
  p50ResponseTime: number;
  p95ResponseTime: number;
  p99ResponseTime: number;
  errorsPerSecond: number;
  requestsPerSecond: number;
}

export async function runLoadTest(config: LoadTestConfig): Promise<LoadTestResult> {
  const results: Array<{
    success: boolean;
    responseTime: number;
    timestamp: number;
    error?: string;
  }> = [];

  const startTime = performance.now();
  const endTime = startTime + config.testDurationMs;
  
  // Her concurrent kullanıcı için promise yarat
  const userPromises = Array.from({ length: config.concurrentUsers }, async (_, userIndex) => {
    // Ramp-up sırasında kullanıcı başlangıç zamanlarını stagger et
    const userStartDelay = (config.rampUpMs * userIndex) / config.concurrentUsers;
    await sleep(userStartDelay);
    
    while (performance.now() < endTime) {
      const requestStart = performance.now();
      
      try {
        // Random short code seçimi
        const shortCode = config.shortCodes[Math.floor(Math.random() * config.shortCodes.length)];
        const url = `${config.baseUrl}/${shortCode}`;
        
        const response = await fetch(url, {
          method: 'GET',
          redirect: 'manual', // Redirect'leri takip etme - sadece timing istiyoruz
        });
        
        const responseTime = performance.now() - requestStart;
        
        results.push({
          success: response.status >= 200 && response.status < 400,
          responseTime,
          timestamp: performance.now(),
        });
        
      } catch (error) {
        const responseTime = performance.now() - requestStart;
        
        results.push({
          success: false,
          responseTime,
          timestamp: performance.now(),
          error: error instanceof Error ? error.message : String(error),
        });
      }
      
      // Bir sonraki request'den önce bekle (istenen yük için ayarla)
      await sleep(100 + Math.random() * 200); // Kullanıcı başına request'ler arası 100-300ms
    }
  });

  // Tüm kullanıcıların tamamlanmasını bekle
  await Promise.all(userPromises);
  
  // İstatistikleri hesapla
  const successfulResults = results.filter(r => r.success);
  const responseTimes = successfulResults.map(r => r.responseTime);
  responseTimes.sort((a, b) => a - b);
  
  const totalDurationSec = (performance.now() - startTime) / 1000;
  
  return {
    totalRequests: results.length,
    successfulRequests: successfulResults.length,
    failedRequests: results.length - successfulResults.length,
    averageResponseTime: responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length,
    p50ResponseTime: responseTimes[Math.floor(responseTimes.length * 0.5)],
    p95ResponseTime: responseTimes[Math.floor(responseTimes.length * 0.95)],
    p99ResponseTime: responseTimes[Math.floor(responseTimes.length * 0.99)],
    errorsPerSecond: (results.length - successfulResults.length) / totalDurationSec,
    requestsPerSecond: results.length / totalDurationSec,
  };
}

async function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Örnek kullanım - her deployment'tan önce bunu çalıştır
async function validatePerformance() {
  console.log('Pre-deployment load test çalıştırılıyor...');
  
  const testConfig: LoadTestConfig = {
    baseUrl: 'https://staging-links.yourcompany.com',
    concurrentUsers: 50,
    testDurationMs: 60 * 1000, // 1 dakika
    rampUpMs: 10 * 1000,  // 10 saniye ramp-up
    shortCodes: ['test1', 'test2', 'test3', 'popular-link', 'campaign-2024'],
  };
  
  const results = await runLoadTest(testConfig);
  
  // Performans assertion'ları
  const maxAcceptableP95 = 500; // 500ms P95 response time
  const maxAcceptableErrorRate = 0.01; // %1 error rate
  
  if (results.p95ResponseTime > maxAcceptableP95) {
    throw new Error(`P95 response time çok yüksek: ${results.p95ResponseTime}ms > ${maxAcceptableP95}ms`);
  }
  
  const errorRate = results.failedRequests / results.totalRequests;
  if (errorRate > maxAcceptableErrorRate) {
    throw new Error(`Error rate çok yüksek: ${(errorRate * 100).toFixed(2)}% > ${(maxAcceptableErrorRate * 100)}%`);
  }
  
  console.log('Load test geçti:', results);
}

Blue-Green Deployment’lar: Korkmadan Deploy Et

Bizi huzurla uyutturan deployment stratejisi:

// deployment/blue-green-deploy.ts
import * as aws from '@aws-sdk/client-route53';
import * as lambda from '@aws-sdk/client-lambda';

interface DeploymentConfig {
  stage: 'blue' | 'green';
  domainName: string;
  hostedZoneId: string;
  healthCheckUrl: string;
}

export class BlueGreenDeployment {
  private route53 = new aws.Route53Client({});
  private lambdaClient = new lambda.LambdaClient({});

  async deployNewVersion(config: DeploymentConfig): Promise<void> {
    console.log(`${config.stage} deployment başlatılıyor...`);
    
    // Adım 1: Yeni infrastructure'ı deploy et
    await this.deployCDKStack(config.stage);
    
    // Adım 2: Yeni environment'ı warm up yap
    await this.warmUpEnvironment(config);
    
    // Adım 3: Health check'leri çalıştır
    await this.runHealthChecks(config.healthCheckUrl);
    
    // Adım 4: Trafiği kademeli olarak shift et
    await this.shiftTraffic(config, [10, 25, 50, 100]);
    
    console.log(`${config.stage} deployment başarıyla tamamlandı`);
  }

  private async deployCDKStack(stage: string): Promise<void> {
    // Bu tipik olarak CDK CLI veya AWS SDK kullanır
    console.log(`${stage} için CDK stack deploy ediliyor...`);
    
    // Örnek: CDK deploy komutunu exec et
    const { spawn } = await import('child_process');
    
    return new Promise((resolve, reject) => {
      const deploy = spawn('npx', ['cdk', 'deploy', '--all', '--context', `stage=${stage}`], {
        stdio: 'inherit',
      });
      
      deploy.on('close', (code) => {
        if (code === 0) {
          resolve();
        } else {
          reject(new Error(`CDK deploy ${code} kodu ile başarısız oldu`));
        }
      });
    });
  }

  private async warmUpEnvironment(config: DeploymentConfig): Promise<void> {
    console.log('Lambda function'ları warm up yapılıyor...');
    
    // Bu stage için tüm Lambda function'ları al
    const functions = await this.lambdaClient.send(new lambda.ListFunctionsCommand({
      Marker: undefined,
      MaxItems: 100,
    }));
    
    const stageFunctions = functions.Functions?.filter(fn => 
      fn.FunctionName?.includes(config.stage)
    ) || [];
    
    // Her function'ı warm up yap
    const warmUpPromises = stageFunctions.map(async (fn) => {
      if (!fn.FunctionName) return;
      
      try {
        await this.lambdaClient.send(new lambda.InvokeCommand({
          FunctionName: fn.FunctionName,
          Payload: JSON.stringify({
            source: 'warm-up',
            warmUp: true,
          }),
        }));
        
        console.log(`${fn.FunctionName} warm up edildi`);
      } catch (error) {
        console.warn(`[WARN] ${fn.FunctionName} warm up başarısız:`, error);
      }
    });
    
    await Promise.all(warmUpPromises);
  }

  private async runHealthChecks(healthCheckUrl: string): Promise<void> {
    console.log('Health check'ler çalıştırılıyor...');
    
    const checks = [
      { name: 'Basic redirect', path: '/test-redirect' },
      { name: 'API health', path: '/api/health' },
      { name: '404 handling', path: '/non-existent-link' },
    ];
    
    for (const check of checks) {
      const url = `${healthCheckUrl}${check.path}`;
      const response = await fetch(url);
      
      // Farklı endpoint'ler için farklı beklentiler
      const expectedStatus = check.path === '/non-existent-link' ? 404 : 200;
      
      if (response.status !== expectedStatus) {
        throw new Error(`${check.name} için health check başarısız: ${response.status}`);
      }
      
      console.log(`${check.name} health check geçti`);
    }
  }

  private async shiftTraffic(
    config: DeploymentConfig, 
    trafficPercentages: number[]
  ): Promise<void> {
    for (const percentage of trafficPercentages) {
      console.log(`${config.stage}'e %${percentage} trafik shift ediliyor...`);
      
      // Route53 weighted routing'i update et
      await this.updateRoute53WeightedRecord(config, percentage);
      
      // DNS propagation ve monitoring için bekle
      await this.sleep(120000); // 2 dakika
      
      // Trafik shift sırasında error rate'leri kontrol et
      await this.monitorErrorRates(config);
      
      console.log(`%${percentage} trafik başarıyla shift edildi`);
    }
  }

  private async updateRoute53WeightedRecord(
    config: DeploymentConfig, 
    weight: number
  ): Promise<void> {
    const oppositeWeight = 100 - weight;
    const oppositeStage = config.stage === 'blue' ? 'green' : 'blue';
    
    // Mevcut stage weight'ini update et
    await this.route53.send(new aws.ChangeResourceRecordSetsCommand({
      HostedZoneId: config.hostedZoneId,
      ChangeBatch: {
        Changes: [{
          Action: 'UPSERT',
          ResourceRecordSet: {
            Name: config.domainName,
            Type: 'CNAME',
            SetIdentifier: config.stage,
            Weight: weight,
            TTL: 60, // Hızlı değişiklikler için kısa TTL
            ResourceRecords: [{ 
              Value: `${config.stage}-api.example.com` 
            }],
          },
        }],
      },
    }));

    // Karşıt stage weight'ini update et
    await this.route53.send(new aws.ChangeResourceRecordSetsCommand({
      HostedZoneId: config.hostedZoneId,
      ChangeBatch: {
        Changes: [{
          Action: 'UPSERT',
          ResourceRecordSet: {
            Name: config.domainName,
            Type: 'CNAME',
            SetIdentifier: oppositeStage,
            Weight: oppositeWeight,
            TTL: 60,
            ResourceRecords: [{ 
              Value: `${oppositeStage}-api.example.com` 
            }],
          },
        }],
      },
    }));
  }

  private async monitorErrorRates(config: DeploymentConfig): Promise<void> {
    // Bu CloudWatch ile integrate olup error rate'leri kontrol eder
    // ve eşiği aşarsa otomatik rollback yapar
    
    console.log('Error rate'ler monitor ediliyor...');
    
    // Örnek: CloudWatch metric'lerini kontrol et
    // Eğer error rate > %1 ise rollback
    // Eğer response time P95 > 500ms ise rollback
    
    await this.sleep(30000); // 30 saniye monitor et
  }

  async rollback(config: DeploymentConfig): Promise<void> {
    console.log(`${config.stage} deployment rollback yapılıyor...`);
    
    // Tüm trafiği stable versiyona geri kaydır
    const stableStage = config.stage === 'blue' ? 'green' : 'blue';
    await this.updateRoute53WeightedRecord({
      ...config,
      stage: stableStage,
    }, 100);
    
    console.log('Rollback tamamlandı');
  }

  private async sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Production Optimizasyon Değerlendirmeleri

Production infrastructure işletmek ölçeklendirme ve maliyet yönetimi hakkında önemli pattern’lar ortaya çıkarır:

1. Conservative provisioning ile agresif monitoring Minimal kapasite ile başlayıp auto-scaling’e güven. Over-provisioning çoğu workload için güvenilirliği artırmadan maliyetleri artırır.

2. Cold start’un kullanıcı deneyimindeki etkisi 2-3 saniyelik cold start latency bile redirect performansını önemli ölçüde bozar. Kritik yollar için provisioned concurrency genellikle ek maliyeti haklar.

3. DynamoDB auto-scaling zamanlamaları Auto-scaling kapasite artışı için 5-10 dakika alır ama hızla aşağı ölçeklenir. Target utilization’u %90 yerine %70’te ayarlamak trafik spike’ları için tampon sağlar.

4. Teknik metric’ler üzerinde business metric’ler “Kampanya başına redirect’ler” ve “conversion oluşturan linkler” tracking’i ham “Lambda invocation’lardan” daha eyleme geçirilebilir görüşler sağlar. Business konteksti optimizasyon çabalarını öncelliklendirmede yardımcı olur.

5. Staging load testing etkinliği Kapsamlı load testing çoğu production sorununu yakalar, ama gerçek kullanıcı pattern’ları genellikle sentetik testlerden farklıdır. Teorik peak load’lar yerine gerçek trafik pattern’larını simüle etmeye odaklan.

Önemli Production Metric’ler

Her sabah gerçekten baktığımız dashboard’lar:

// lib/production-dashboard.ts
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';

export class ProductionDashboard extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new cloudwatch.Dashboard(this, 'LinkShortenerProduction', {
      dashboardName: 'LinkShortener-Production-Health',
      widgets: [
        // Satır 1: Business metric'ler
        [
          new cloudwatch.SingleValueWidget({
            title: 'Redirects (24sa)',
            metrics: [
              new cloudwatch.Metric({
                namespace: 'LinkShortener/Production',
                metricName: 'RedirectCount',
                statistic: 'Sum',
                period: cdk.Duration.hours(24),
              }),
            ],
            width: 6,
          }),
          
          new cloudwatch.SingleValueWidget({
            title: 'Success Rate (24sa)',
            metrics: [
              new cloudwatch.MathExpression({
                expression: '(successful / total) * 100',
                usingMetrics: {
                  successful: new cloudwatch.Metric({
                    namespace: 'LinkShortener/Production',
                    metricName: 'RedirectCount',
                    dimensionsMap: { Status: 'Success' },
                    statistic: 'Sum',
                  }),
                  total: new cloudwatch.Metric({
                    namespace: 'LinkShortener/Production',
                    metricName: 'RedirectCount',
                    statistic: 'Sum',
                  }),
                },
              }),
            ],
            width: 6,
          }),
        ],

        // Satır 2: Performans metric'leri
        [
          new cloudwatch.GraphWidget({
            title: 'Response Time Percentile'ları',
            left: [
              new cloudwatch.Metric({
                namespace: 'LinkShortener/Production',
                metricName: 'RedirectResponseTime',
                statistic: 'p50',
                period: cdk.Duration.minutes(5),
                label: 'P50',
              }),
              new cloudwatch.Metric({
                namespace: 'LinkShortener/Production', 
                metricName: 'RedirectResponseTime',
                statistic: 'p95',
                period: cdk.Duration.minutes(5),
                label: 'P95',
              }),
              new cloudwatch.Metric({
                namespace: 'LinkShortener/Production',
                metricName: 'RedirectResponseTime', 
                statistic: 'p99',
                period: cdk.Duration.minutes(5),
                label: 'P99',
              }),
            ],
            width: 12,
          }),
        ],

        // Satır 3: Infrastructure sağlığı
        [
          new cloudwatch.GraphWidget({
            title: 'DynamoDB Throttling',
            left: [
              new cloudwatch.Metric({
                namespace: 'AWS/DynamoDB',
                metricName: 'ReadThrottledRequests',
                dimensionsMap: { TableName: 'LinksTable' },
                statistic: 'Sum',
              }),
              new cloudwatch.Metric({
                namespace: 'AWS/DynamoDB',
                metricName: 'WriteThrottledRequests',
                dimensionsMap: { TableName: 'LinksTable' },
                statistic: 'Sum',
              }),
            ],
            width: 6,
          }),
          
          new cloudwatch.GraphWidget({
            title: 'Lambda Cold Start'ları',
            left: [
              new cloudwatch.Metric({
                namespace: 'LinkShortener/Production',
                metricName: 'RedirectCount',
                dimensionsMap: { ColdStart: 'true' },
                statistic: 'Sum',
                period: cdk.Duration.minutes(5),
              }),
            ],
            width: 6,
          }),
        ],
      ],
    });
  }
}

Sırada Ne Var?

Bölüm 5’te son cepheyle uğraşacağız: günde milyonlarca redirect’i handle etmek için ölçeklendirme, ölçekte maliyet optimizasyonu, ve küçük bir takımın yüksek trafik servisi yönetmesini sağlayan operasyonel uygulamalar.

Multi-region deployment’lar, database sharding stratejileri, ve kullanıcıların problemleri fark etmesinden önce seni uyaran monitoring kurulumu gibi ileri seviye konuları kapsayacağız.

Burada inşa ettiğimiz infrastructure oldukça iyi ölçekleniyor, ama binlerce request’i handle eden bir servisle milyonlarını handle eden arasındaki farkı yaratan belirli pattern’lar ve uygulamalar var. Şimdi bu boşluğu dolduralım.

AWS CDK Link Kısaltıcı: Sıfırdan Production'a

AWS CDK, Node.js Lambda ve DynamoDB ile production-grade bir link kısaltma servisi kurulumu hakkında 5 bölümlük kapsamlı seri. Gerçek production hikayeleri, performans optimizasyonu ve maliyet yönetimi dahil.

İlerleme 4 / 5 yazı

İlgili yazılar

AWS ile Edge Computing: CloudFront Functions vs Lambda@Edge

Global uygulamalar için AWS edge computing çözümlerini seçme ve uygulama üzerine pratik örnekler ve maliyet optimizasyonu stratejileri içeren kapsamlı teknik rehber.

awscloudfrontlambda+6
DynamoDB Rate Limiting: Single Table Design'da Ölçekte Stratejiler

Single Table Design uygulamalarında DynamoDB throttling'i önleme ve yönetme stratejileri. Partition key tasarımı, write sharding, kapasite modları, DAX caching, retry pattern'leri ve yüksek throughput sistemler için CloudWatch monitoring konularını kapsar.

dynamodbawsrate-limiting+5
AWS Maliyet Optimizasyonu Araç Seti - Production Workload'lar için Pratik Stratejiler

Native AWS servisleri, otomasyon ve kanıtlanmış implementation pattern'leri kullanarak AWS maliyetlerini %40-70 azaltmaya yönelik kapsamlı bir rehber.

awscost-optimizationfinops+5
LangChain Production'da: Çalışan Patternler ve İşe Yaramayan Anti-Patternler

LangChain uygulamalarını production'a taşırken öğrendiklerim. Başarısızlığa yol açan anti-patternler, başarıyı sağlayan patternler, çalışan kod örnekleri ve maliyet optimizasyon stratejileri.

langchainllmproduction+5
Veritabanı Query Profiling: Sistematik Optimizasyon Yolculuğu

Sistematik veritabanı profiling ve optimizasyonu ile yıllık altyapı maliyetlerini 100K dolar azalttığımız hikaye. PostgreSQL ve MongoDB performance deneyimleri.

database-optimizationpostgresqlmongodb+7