AWS CDK Link Shortener Part 5: Ölçeklendirme ve Uzun Vadeli Bakım

Multi-region deployment, veritabanı ölçeklendirme stratejileri, felaket kurtarma kalıpları ve uzun vadeli bakım yaklaşımları. Production sistemleri ölçekte çalıştırmanın gerçek dersleri ve yıllar sonra önemli olan mimari kararlar.

AWS CDK Link Shortener Part 5: Ölçeklendirme ve Uzun Vadeli Bakım#

Link shortener'ımızı başlattıktan iki yıl sonra, üçlük business review sırasında telefon geldi. "APAC pazarlarına genişlenmemiz gerekiyor ve Avrupa kullanıcılarımız yavaş redirectler hakkında şikayet ediyor." Basit bir istek olarak başlayan şey, altı aylık global ölçeklendirme projesine dönüştü ve bana herhangi bir mimari kursundan daha fazla distributed sistem öğretti.

Asıl şok edici olan? "Mükemmel mimaride" single-region sistemimiz sadece uluslararası kullanıcılar için yavaş değildi—artık üç kıtadaki müşteri kazanımına bağımlı olan bir işletme için tek hata noktasıydı. Zor yoldan ölçeklendirmeyi öğrenme zamanı.

1-4. Bölümlerde, link shortener'ımızı production için inşa ettik, güvenliğini sağladık ve optimize ettik. Şimdi global ölçekte ölçeklendirelim ve onu yıllarca çalışır durumda tutacak operational excellence kalıplarını kuralım. Mimari kararların sonuçlarını gerçekten göstermeye başladığı yer burası.

Multi-Region Mimarisi: Basit Artık Yeterli Değilken#

Orijinal single-region setup'ımız günde 100K redirect için harika çalıştı. Global pazarlarda 10M redirect'te, her milisaniye latency conversion rate problemi haline geldi. Mimariyi şu şekilde evrimleştirdik:

TypeScript
// lib/global-link-shortener-stack.ts - Multi-region deployment kalıbı
import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import { Construct } from 'constructs';

export interface GlobalLinkShortenerProps {
  readonly primaryRegion: string;
  readonly replicationRegions: string[];
  readonly domainName: string;
  readonly certificateArn: string;
}

export class GlobalLinkShortenerStack extends cdk.Stack {
  public readonly globalTable: dynamodb.Table;
  public readonly distribution: cloudfront.Distribution;

  constructor(scope: Construct, id: string, props: GlobalLinkShortenerProps) {
    super(scope, id, { 
      env: { region: props.primaryRegion },
      crossRegionReferences: true 
    });

    // Cross-region replication ile global DynamoDB table
    this.globalTable = new dynamodb.Table(this, 'GlobalLinksTable', {
      tableName: 'global-links-table',
      partitionKey: { name: 'shortCode', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      
      // Global data için point-in-time recovery
      pointInTimeRecovery: true,
      
      // Multi-region active-active için global tables
      replicationRegions: props.replicationRegions,
      
      // Region'lar arası real-time analytics için stream
      stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
      
      // Deletion protection - bunu zor yoldan öğrendik
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      deletionProtection: true,
    });

    // Analytics queries için global secondary index
    this.globalTable.addGlobalSecondaryIndex({
      indexName: 'domain-timestamp-index',
      partitionKey: { name: 'domain', type: dynamodb.AttributeType.STRING },
      sortKey: { name: 'createdAt', type: dynamodb.AttributeType.STRING },
    });

    // Her region için Route 53 health check'leri
    const healthChecks = props.replicationRegions.map((region, index) => {
      return new route53.CfnHealthCheck(this, `HealthCheck-${region}`, {
        type: 'HTTPS',
        resourcePath: '/health',
        fullyQualifiedDomainName: `${region}.${props.domainName}`,
        port: 443,
        requestInterval: 30,
        failureThreshold: 3,
      });
    });

    // Regional origin'ler ile global CloudFront distribution
    this.distribution = new cloudfront.Distribution(this, 'GlobalDistribution', {
      comment: 'Global Link Shortener Distribution',
      
      // Global edge location'lar için price class
      priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      
      // Custom domain konfigürasyonu
      domainNames: [props.domainName],
      certificate: acm.Certificate.fromCertificateArn(
        this, 'Certificate', props.certificateArn
      ),
      
      // Health check failover ile regional origin'ler
      additionalBehaviors: this.createRegionalBehaviors(props.replicationRegions),
      
      // Redirect response'ları için cache policy
      defaultBehavior: {
        origin: new origins.HttpOrigin(`${props.primaryRegion}.${props.domainName}`),
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
        originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN,
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        
        // Geo-routing optimizasyonu için edge Lambda
        edgeLambdas: [{
          functionVersion: this.createEdgeFunction(),
          eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
        }],
      },
    });
  }

  private createRegionalBehaviors(regions: string[]) {
    const behaviors: Record<string, cloudfront.BehaviorOptions> = {};
    
    regions.forEach(region => {
      behaviors[`/${region}/*`] = {
        origin: new origins.HttpOrigin(`${region}.api.example.com`),
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      };
    });
    
    return behaviors;
  }
}

Uluslararası performansımızı kurtaran regional deployment kalıbı:

TypeScript
// bin/global-deployment.ts - Regional deployment orkestrasyon
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { GlobalLinkShortenerStack } from '../lib/global-link-shortener-stack';
import { RegionalLinkShortenerStack } from '../lib/regional-link-shortener-stack';

const app = new cdk.App();

// Configuration driven deployment
const regions = [
  { name: 'us-east-1', isPrimary: true, weight: 40 },
  { name: 'eu-west-1', isPrimary: false, weight: 35 },
  { name: 'ap-southeast-1', isPrimary: false, weight: 25 },
];

const domainName = app.node.tryGetContext('domainName') || 'links.example.com';

// Primary global kaynakları deploy et
const globalStack = new GlobalLinkShortenerStack(app, 'GlobalLinkShortener', {
  primaryRegion: 'us-east-1',
  replicationRegions: regions.filter(r => !r.isPrimary).map(r => r.name),
  domainName,
  certificateArn: app.node.tryGetContext('certificateArn'),
});

// Regional stack'leri deploy et
regions.forEach(region => {
  new RegionalLinkShortenerStack(app, `LinkShortener-${region.name}`, {
    env: { region: region.name },
    globalTable: globalStack.globalTable,
    isPrimaryRegion: region.isPrimary,
    trafficWeight: region.weight,
    domainName,
    
    // Global kaynaklar için cross-stack reference'lar
    crossRegionReferences: true,
  });
});

Multi-Region Hakkında Acı Gerçek: Bu sadece birden çok region'a deploy etmek değil. Data consistency, regional failover, maliyet etkileri ve operational karmaşıklığı hakkında düşünmen gerekiyor. İlk denememiz 3 ay sürdü çünkü operational overhead'ı hafife aldık.

Veritabanı Ölçeklendirme Stratejileri: DynamoDB Auto-Scaling'in Ötesinde#

Günde 10M+ istek aldığında, DynamoDB'nin auto-scaling'inin bile sınırları var. Production'da gerçekten çalışan kalıplar burada:

TypeScript
// lib/database-scaling-stack.ts - Gelişmiş DynamoDB ölçeklendirme kalıpları
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as elasticache from 'aws-cdk-lib/aws-elasticache';
import * as lambda from 'aws-cdk-lib/aws-lambda';

export class ScalableDatabaseStack extends cdk.Stack {
  
  // Hot partition tespiti ve azaltma
  private createShardedTable() {
    const table = new dynamodb.Table(this, 'ShardedLinksTable', {
      partitionKey: { name: 'shardKey', type: dynamodb.AttributeType.STRING },
      sortKey: { name: 'shortCode', type: dynamodb.AttributeType.STRING },
      
      // Öngörülemeyen trafik için on-demand ölçeklendirme
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      
      // Hot partition tespiti için contributor insights
      contributorInsightsEnabled: true,
    });

    // Write sharding mantığı ekle
    const shardingFunction = new lambda.Function(this, 'ShardingFunction', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'sharding.handler',
      code: lambda.Code.fromAsset('functions'),
      environment: {
        SHARD_COUNT: '100', // Load'u shard'lara dağıt
        TABLE_NAME: table.tableName,
      },
    });

    return table;
  }

  // Hot link caching için Redis cluster
  private createCacheCluster() {
    const cacheSubnetGroup = new elasticache.CfnSubnetGroup(
      this, 'CacheSubnetGroup', {
        description: 'Subnet group for Redis cluster',
        subnetIds: this.vpc.privateSubnets.map(subnet => subnet.subnetId),
      }
    );

    return new elasticache.CfnCacheCluster(this, 'RedisCluster', {
      engine: 'redis',
      engineVersion: '7.0',
      cacheNodeType: 'cache.r6g.large',
      numCacheNodes: 1,
      
      // High availability için multi-AZ
      azMode: 'cross-az',
      preferredAvailabilityZones: ['us-east-1a', 'us-east-1b'],
      
      // Subnet ve güvenlik konfigürasyonu
      cacheSubnetGroupName: cacheSubnetGroup.ref,
      vpcSecurityGroupIds: [this.cacheSecurityGroup.securityGroupId],
      
      // Backup ve maintenance
      snapshotRetentionLimit: 5,
      snapshotWindow: '03:00-05:00',
      preferredMaintenanceWindow: 'sun:05:00-sun:07:00',
    });
  }

  // Analytics için read replica kalıbı
  private createAnalyticsReadReplicas() {
    // Redirect'leri etkilememek için analytics'te ayrı table
    return new dynamodb.Table(this, 'AnalyticsTable', {
      partitionKey: { name: 'date', type: dynamodb.AttributeType.STRING },
      sortKey: { name: 'linkId', type: dynamodb.AttributeType.STRING },
      
      // Analytics query'ler için time-based partitioning
      timeToLiveAttribute: 'ttl',
      
      // Real-time aggregation için stream processing
      stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
    });
  }
}

Hot partition problemlerimizi çözen sharding mantığı:

TypeScript
// functions/sharding.ts - Hot partition azaltma
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { createHash } from 'crypto';

interface LinkData {
  shortCode: string;
  targetUrl: string;
  domain: string;
  createdAt: string;
}

export const handler = async (event: any) => {
  const { shortCode, targetUrl, domain } = event as LinkData;
  
  // Load'u dağıtmak için shard key üretimi
  const shardKey = generateShardKey(shortCode, domain);
  
  const client = new DynamoDBClient({});
  
  // Sharded partition'a yaz
  const command = new PutItemCommand({
    TableName: process.env.TABLE_NAME,
    Item: {
      shardKey: { S: shardKey },
      shortCode: { S: shortCode },
      targetUrl: { S: targetUrl },
      domain: { S: domain },
      createdAt: { S: new Date().toISOString() },
      
      // Eski link'lerin otomatik temizlenmesi için TTL
      ttl: { N: Math.floor(Date.now() / 1000) + (365 * 24 * 60 * 60) },
    },
    
    // Overwrite'ları engellemek için conditional write
    ConditionExpression: 'attribute_not_exists(shortCode)',
  });

  try {
    await client.send(command);
    return { statusCode: 201, body: JSON.stringify({ shortCode, shardKey }) };
  } catch (error) {
    console.error('Sharding write failed:', error);
    throw new Error('Failed to create sharded link');
  }
};

function generateShardKey(shortCode: string, domain: string): string {
  const shardCount = parseInt(process.env.SHARD_COUNT || '10');
  
  // Eşit dağılım için consistent hashing
  const hash = createHash('md5')
    .update(`${shortCode}-${domain}`)
    .digest('hex');
  
  const shardIndex = parseInt(hash.substring(0, 8), 16) % shardCount;
  return `shard-${shardIndex.toString().padStart(3, '0')}`;
}

Ölçeklendirme Gerçeklik Kontrolü: Sharding teoride zarif görünüyor, ama sabah 3'te 100 shard'da distributed query'leri debug etmek eğlenceli değil. Basit çözümlerle başlayıp, metrikler gerekli olduğunu kanıtladığında karmaşıklık eklemeyi öğrendik.

Felaket Kurtarma: En Kötü Gün İçin Planlama#

Global deployment'ımıza altı ay sonra, AWS us-east-1'de büyük bir kesinti yaşadı. Primary region'ımız 4 saat down kaldı. Gerçek disaster recovery hakkında öğrendiklerimiz:

TypeScript
// lib/disaster-recovery-stack.ts - Multi-region failover otomasyonu
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';

export class DisasterRecoveryStack extends cdk.Stack {
  
  // Route 53 health check'lerini kullanan otomatik failover
  private createFailoverRouting() {
    const hostedZone = route53.HostedZone.fromLookup(this, 'Zone', {
      domainName: 'example.com',
    });

    // Health check ile primary region record
    const primaryHealthCheck = new route53.CfnHealthCheck(this, 'PrimaryHealth', {
      type: 'HTTPS',
      resourcePath: '/health',
      fullyQualifiedDomainName: 'us-east-1.api.example.com',
      port: 443,
      requestInterval: 30,
      failureThreshold: 3,
      
      // CloudWatch alarm entegrasyonu
      insufficientDataHealthStatus: 'Failure',
      measureLatency: true,
      regions: ['us-east-1', 'us-west-1', 'eu-west-1'],
    });

    // Failover routing ile primary record
    new route53.ARecord(this, 'PrimaryRecord', {
      zone: hostedZone,
      recordName: 'api',
      target: route53.RecordTarget.fromIpAddresses('1.2.3.4'),
      setIdentifier: 'primary',
      failover: route53.FailoverRoutingPolicy.PRIMARY,
      healthCheckId: primaryHealthCheck.attrHealthCheckId,
    });

    // Secondary region record
    const secondaryHealthCheck = new route53.CfnHealthCheck(this, 'SecondaryHealth', {
      type: 'HTTPS',
      resourcePath: '/health',
      fullyQualifiedDomainName: 'eu-west-1.api.example.com',
      port: 443,
      requestInterval: 30,
      failureThreshold: 3,
    });

    new route53.ARecord(this, 'SecondaryRecord', {
      zone: hostedZone,
      recordName: 'api',
      target: route53.RecordTarget.fromIpAddresses('5.6.7.8'),
      setIdentifier: 'secondary',
      failover: route53.FailoverRoutingPolicy.SECONDARY,
      healthCheckId: secondaryHealthCheck.attrHealthCheckId,
    });
  }

  // Cross-region backup otomasyonu
  private createBackupStrategy() {
    const backupFunction = new lambda.Function(this, 'BackupFunction', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'backup.handler',
      code: lambda.Code.fromAsset('functions'),
      timeout: cdk.Duration.minutes(15),
      
      environment: {
        PRIMARY_TABLE: 'links-table-us-east-1',
        BACKUP_BUCKET: 'links-backup-bucket',
        CROSS_REGION_BUCKET: 'links-backup-eu-west-1',
      },
    });

    // Günlük backup'ları planla
    new events.Rule(this, 'BackupSchedule', {
      schedule: events.Schedule.cron({ 
        hour: '2', 
        minute: '0' 
      }),
      targets: [new targets.LambdaFunction(backupFunction)],
    });

    // Point-in-time recovery monitoring
    const recoveryAlarm = new cloudwatch.Alarm(this, 'RecoveryAlarm', {
      metric: backupFunction.metricErrors(),
      threshold: 1,
      evaluationPeriods: 1,
    });

    // Backup hataları için SNS notification
    const alertTopic = new sns.Topic(this, 'BackupAlerts');
    recoveryAlarm.addAlarmAction(new cloudwatchActions.SnsAction(alertTopic));
  }
}

Kesinti sırasında bizi kurtaran backup otomasyonu:

TypeScript
// functions/backup.ts - Otomatik disaster recovery backup
import { DynamoDBClient, ScanCommand } from '@aws-sdk/client-dynamodb';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { gzip } from 'zlib';
import { promisify } from 'util';

const gzipAsync = promisify(gzip);

export const handler = async (event: any) => {
  const dynamoClient = new DynamoDBClient({ region: 'us-east-1' });
  const s3Client = new S3Client({ region: 'us-east-1' });
  
  const timestamp = new Date().toISOString().split('T')[0];
  let lastEvaluatedKey;
  let backupData = [];

  try {
    // Bütün table'ın paginated scan'i
    do {
      const scanCommand = new ScanCommand({
        TableName: process.env.PRIMARY_TABLE,
        ExclusiveStartKey: lastEvaluatedKey,
        Limit: 1000, // Parçalar halinde işle
      });

      const result = await dynamoClient.send(scanCommand);
      if (result.Items) {
        backupData.push(...result.Items);
      }
      
      lastEvaluatedKey = result.LastEvaluatedKey;
      
      // Büyük table'lar için progress logging
      console.log(`${backupData.length} öğe backup'landı...`);
      
    } while (lastEvaluatedKey);

    // Backup'ı sıkıştır ve upload et
    const compressed = await gzipAsync(JSON.stringify(backupData));
    
    const uploadCommand = new PutObjectCommand({
      Bucket: process.env.BACKUP_BUCKET,
      Key: `daily-backups/${timestamp}/links-backup.json.gz`,
      Body: compressed,
      
      // Cross-region replication tag'leri
      Tagging: 'BackupType=Daily&Region=us-east-1&Replicate=true',
      
      // Hassas data için encryption
      ServerSideEncryption: 'AES256',
    });

    await s3Client.send(uploadCommand);
    
    // Gerçek disaster recovery için cross-region kopyalama
    await copyToSecondaryRegion(compressed, timestamp);
    
    return {
      statusCode: 200,
      body: JSON.stringify({
        itemsBackedUp: backupData.length,
        backupKey: `daily-backups/${timestamp}/links-backup.json.gz`,
        timestamp,
      }),
    };

  } catch (error) {
    console.error('Backup failed:', error);
    
    // Operations team'e alert gönder
    await sendAlert({
      subject: 'Link Shortener Backup Failed',
      message: `Backup failed at ${new Date().toISOString()}: ${error.message}`,
      severity: 'HIGH',
    });
    
    throw error;
  }
};

async function copyToSecondaryRegion(data: Buffer, timestamp: string) {
  const secondaryS3 = new S3Client({ region: 'eu-west-1' });
  
  return secondaryS3.send(new PutObjectCommand({
    Bucket: process.env.CROSS_REGION_BUCKET,
    Key: `daily-backups/${timestamp}/links-backup.json.gz`,
    Body: data,
    ServerSideEncryption: 'AES256',
  }));
}

DR Gerçeği: Route 53 health check'leri hataları tespit edip failover'ı tetiklemek için 90-180 saniye alıyor. İnternet zamanında bu bir sonsuzluk. Bunu planla ve manuel override prosedürlerini hazır bulundur.

Uzun Vadeli Bakım ve Teknik Borç#

İki yıl sonra, "hızlı MVP"miz önemli teknik borç biriktirmişti. Production'ı bozmadan nasıl yönettiğimiz:

TypeScript
// lib/maintenance-automation-stack.ts - Teknik borç yönetimi
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions';
import * as events from 'aws-cdk-lib/aws-events';

export class MaintenanceAutomationStack extends cdk.Stack {
  
  // Otomatik dependency update'leri
  private createDependencyUpdatePipeline() {
    const updateFunction = new lambda.Function(this, 'DependencyUpdater', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'maintenance.updateDependencies',
      code: lambda.Code.fromAsset('functions'),
      timeout: cdk.Duration.minutes(5),
      
      environment: {
        GITHUB_TOKEN: 'your-github-token',
        REPOSITORY: 'your-org/link-shortener',
        SLACK_WEBHOOK: process.env.SLACK_WEBHOOK || '',
      },
    });

    // Haftalık dependency kontrolü
    new events.Rule(this, 'WeeklyUpdates', {
      schedule: events.Schedule.cron({
        weekDay: '1', // Pazartesi
        hour: '9',
        minute: '0',
      }),
      targets: [new targets.LambdaFunction(updateFunction)],
    });
  }

  // Data temizleme otomasyonu
  private createDataCleanupPipeline() {
    // Güvenli data temizleme için Step Function
    const cleanupWorkflow = new stepfunctions.StateMachine(this, 'CleanupWorkflow', {
      definition: stepfunctions.Chain
        .start(new stepfunctions.Task(this, 'IdentifyExpiredLinks', {
          task: new tasks.LambdaInvoke(this.identifyExpiredLinksFunction),
        }))
        .next(new stepfunctions.Task(this, 'CreateBackupSnapshot', {
          task: new tasks.LambdaInvoke(this.createBackupFunction),
        }))
        .next(new stepfunctions.Task(this, 'DeleteExpiredLinks', {
          task: new tasks.LambdaInvoke(this.deleteExpiredLinksFunction),
        }))
        .next(new stepfunctions.Task(this, 'VerifyCleanup', {
          task: new tasks.LambdaInvoke(this.verifyCleanupFunction),
        })),
      timeout: cdk.Duration.hours(2),
    });

    // Aylık temizleme planı
    new events.Rule(this, 'MonthlyCleanup', {
      schedule: events.Schedule.cron({
        day: '1',
        hour: '3',
        minute: '0',
      }),
      targets: [new targets.SfnStateMachine(cleanupWorkflow)],
    });
  }

  // Güvenlik audit otomasyonu
  private createSecurityAuditPipeline() {
    const auditFunction = new lambda.Function(this, 'SecurityAuditor', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'security.auditSystem',
      code: lambda.Code.fromAsset('functions'),
      timeout: cdk.Duration.minutes(10),
      
      environment: {
        SECURITY_SCAN_BUCKET: 'security-audit-results',
        COMPLIANCE_WEBHOOK: process.env.COMPLIANCE_WEBHOOK || '',
      },
    });

    // Günlük güvenlik kontrolleri
    new events.Rule(this, 'DailySecurityAudit', {
      schedule: events.Schedule.rate(cdk.Duration.days(1)),
      targets: [new targets.LambdaFunction(auditFunction)],
    });
  }
}

Bizi teknik borçtan önde tutan maintenance otomasyonu:

TypeScript
// functions/maintenance.ts - Otomatik bakım görevleri
import { Octokit } from '@octokit/rest';
import { execSync } from 'child_process';
import { writeFileSync, readFileSync } from 'fs';

export const updateDependencies = async (event: any) => {
  const octokit = new Octokit({
    auth: process.env.GITHUB_TOKEN,
  });

  try {
    // Güncel olmayan package'ları kontrol et
    const outdated = execSync('npm outdated --json', { encoding: 'utf8' });
    const outdatedPackages = JSON.parse(outdated);
    
    if (Object.keys(outdatedPackages).length === 0) {
      console.log('Tüm bağımlılıklar güncel');
      return { statusCode: 200, body: 'Güncelleme gerekmedi' };
    }

    // Update'ler için feature branch oluştur
    const branchName = `dependency-updates-${new Date().toISOString().split('T')[0]}`;
    
    await octokit.rest.git.createRef({
      owner: 'your-org',
      repo: 'link-shortener',
      ref: `refs/heads/${branchName}`,
      sha: await getCurrentCommitSha(),
    });

    // package.json'ı sadece uyumlu versiyonlarla güncelle
    const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
    let updatedCount = 0;

    for (const [pkg, info] of Object.entries(outdatedPackages)) {
      const pkgInfo = info as any;
      
      // Stabilite için sadece patch ve minor versiyonları güncelle
      if (isCompatibleUpdate(pkgInfo.current, pkgInfo.latest)) {
        if (packageJson.dependencies[pkg]) {
          packageJson.dependencies[pkg] = `^${pkgInfo.latest}`;
          updatedCount++;
        }
        if (packageJson.devDependencies[pkg]) {
          packageJson.devDependencies[pkg] = `^${pkgInfo.latest}`;
          updatedCount++;
        }
      }
    }

    if (updatedCount > 0) {
      writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
      
      // Uyumluluğu sağlamak için test'leri çalıştır
      const testResult = execSync('npm test', { encoding: 'utf8' });
      
      // Pull request oluştur
      await octokit.rest.pulls.create({
        owner: 'your-org',
        repo: 'link-shortener',
        title: `Otomatik dependency güncellemeleri (${updatedCount} paket)`,
        head: branchName,
        base: 'main',
        body: createPRBody(outdatedPackages, updatedCount),
      });

      await notifySlack(`${updatedCount} dependency güncellemesi için PR oluşturuldu`);
    }

    return {
      statusCode: 200,
      body: JSON.stringify({ updatedPackages: updatedCount }),
    };

  } catch (error) {
    console.error('Dependency update failed:', error);
    await notifySlack(`❌ Dependency update başarısız: ${error.message}`);
    throw error;
  }
};

function isCompatibleUpdate(current: string, latest: string): boolean {
  const [currentMajor, currentMinor] = current.split('.').map(Number);
  const [latestMajor, latestMinor] = latest.split('.').map(Number);
  
  // Sadece aynı major versiyon update'lerini izin ver
  return currentMajor === latestMajor && latestMinor >= currentMinor;
}

async function notifySlack(message: string) {
  if (!process.env.SLACK_WEBHOOK) return;
  
  await fetch(process.env.SLACK_WEBHOOK, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text: message }),
  });
}

Takım Süreçleri ve Operational Excellence#

Global bir sistem çalıştırmanın bize öğrettiği teknolojinin savaşın sadece yarısı olduğu. Diğer yarısı ölçeklenen takım süreçleri kurmak:

TypeScript
// lib/operational-excellence-stack.ts - Observability ve alerting
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as chatbot from 'aws-cdk-lib/aws-chatbot';

export class OperationalExcellenceStack extends cdk.Stack {
  
  // Kapsamlı monitoring dashboard'u
  private createOperationalDashboard() {
    const dashboard = new cloudwatch.Dashboard(this, 'OperationalDashboard', {
      dashboardName: 'LinkShortener-Operations',
      
      widgets: [
        // SLA monitoring
        [
          new cloudwatch.GraphWidget({
            title: 'Response Time SLA (95. yüzdelik)',
            left: [
              new cloudwatch.Metric({
                namespace: 'AWS/Lambda',
                metricName: 'Duration',
                statistic: 'p95',
                dimensionsMap: {
                  FunctionName: 'redirect-function',
                },
              }),
            ],
            leftYAxis: { min: 0, max: 100 },
            
            // 50ms'de SLA çizgisi
            leftAnnotations: [{
              value: 50,
              label: 'SLA Eşiği',
              color: cloudwatch.Color.RED,
            }],
          }),
          
          new cloudwatch.SingleValueWidget({
            title: 'Mevcut Erişilebilirlik',
            metrics: [
              new cloudwatch.MathExpression({
                expression: '100 - (errors / requests * 100)',
                usingMetrics: {
                  errors: new cloudwatch.Metric({
                    namespace: 'AWS/Lambda',
                    metricName: 'Errors',
                    statistic: 'Sum',
                  }),
                  requests: new cloudwatch.Metric({
                    namespace: 'AWS/Lambda',
                    metricName: 'Invocations',
                    statistic: 'Sum',
                  }),
                },
              }),
            ],
          }),
        ],
        
        // Maliyet monitoring
        [
          new cloudwatch.GraphWidget({
            title: 'Günlük Maliyet Dağılımı',
            stacked: true,
            left: [
              new cloudwatch.Metric({
                namespace: 'AWS/Billing',
                metricName: 'EstimatedCharges',
                statistic: 'Maximum',
                dimensionsMap: {
                  Currency: 'USD',
                  ServiceName: 'AmazonDynamoDB',
                },
              }),
              new cloudwatch.Metric({
                namespace: 'AWS/Billing',
                metricName: 'EstimatedCharges',
                statistic: 'Maximum',
                dimensionsMap: {
                  Currency: 'USD',
                  ServiceName: 'AWSLambda',
                },
              }),
            ],
          }),
        ],
        
        // Business metrikler
        [
          new cloudwatch.GraphWidget({
            title: 'İş Etkisi Metrikleri',
            left: [
              new cloudwatch.Metric({
                namespace: 'LinkShortener/Business',
                metricName: 'LinksCreated',
                statistic: 'Sum',
              }),
              new cloudwatch.Metric({
                namespace: 'LinkShortener/Business',
                metricName: 'RedirectsServed',
                statistic: 'Sum',
              }),
            ],
          }),
        ],
      ],
    });

    return dashboard;
  }

  // Akıllı alerting sistemi
  private createIntelligentAlerting() {
    const criticalAlerts = new sns.Topic(this, 'CriticalAlerts');
    const warningAlerts = new sns.Topic(this, 'WarningAlerts');

    // P1: Servis down
    new cloudwatch.Alarm(this, 'ServiceDownAlarm', {
      alarmName: 'LinkShortener-ServiceDown-P1',
      metric: new cloudwatch.Metric({
        namespace: 'AWS/Lambda',
        metricName: 'Errors',
        statistic: 'Sum',
        dimensionsMap: { FunctionName: 'redirect-function' },
      }),
      threshold: 10,
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      treatMissingData: cloudwatch.TreatMissingData.BREACHING,
      
      alarmActions: [new cloudwatchActions.SnsAction(criticalAlerts)],
    });

    // P2: Performance degradation
    new cloudwatch.Alarm(this, 'PerformanceDegradationAlarm', {
      alarmName: 'LinkShortener-SlowResponse-P2',
      metric: new cloudwatch.Metric({
        namespace: 'AWS/Lambda',
        metricName: 'Duration',
        statistic: 'p95',
      }),
      threshold: 100, // 100ms P95
      evaluationPeriods: 3,
      datapointsToAlarm: 2,
      
      alarmActions: [new cloudwatchActions.SnsAction(warningAlerts)],
    });

    // P3: Capacity planning
    new cloudwatch.Alarm(this, 'CapacityPlanningAlarm', {
      alarmName: 'LinkShortener-HighLoad-P3',
      metric: new cloudwatch.Metric({
        namespace: 'AWS/DynamoDB',
        metricName: 'ConsumedReadCapacityUnits',
        statistic: 'Sum',
      }),
      threshold: 8000, // Provisioned kapasitenin %80'i
      evaluationPeriods: 5,
      datapointsToAlarm: 3,
      
      alarmActions: [new cloudwatchActions.SnsAction(warningAlerts)],
    });

    // Takım notification'ları için Slack entegrasyonu
    new chatbot.SlackChannelConfiguration(this, 'SlackNotifications', {
      slackChannelConfigurationName: 'linkshortener-alerts',
      slackWorkspaceId: 'YOUR_WORKSPACE_ID',
      slackChannelId: 'C01234567890',
      
      notificationTopics: [criticalAlerts, warningAlerts],
      guardrailPolicies: ['arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess'],
    });
  }
}

Hafta sonlarımızı kurtaran runbook otomasyonu:

TypeScript
// functions/incident-response.ts - Otomatik incident response
export const autoIncidentResponse = async (event: any) => {
  const alarmName = event.Records[0].Sns.Message.AlarmName;
  const severity = extractSeverity(alarmName);
  
  console.log(`${severity} incident işleniyor: ${alarmName}`);

  // Severity'ye göre otomatik düzeltme
  switch (severity) {
    case 'P1':
      await handleCriticalIncident(event);
      break;
    case 'P2':
      await handlePerformanceIssue(event);
      break;
    case 'P3':
      await handleCapacityWarning(event);
      break;
  }
};

async function handleCriticalIncident(event: any) {
  // 1. PagerDuty incident oluştur
  await createPagerDutyIncident({
    title: 'Link Shortener Service Down',
    severity: 'critical',
    service: 'link-shortener-prod',
  });

  // 2. Acil durum read replica'larını etkinleştir
  await enableEmergencyReadReplicas();

  // 3. Bakım sayfasına geç
  await updateMaintenancePage(true);

  // 4. Tanı verisi toplamayı başlat
  await collectDiagnosticData();
  
  // 5. Paydaşları bilgilendir
  await notifyStakeholders('CRITICAL: Link shortener kesinti yaşıyor');
}

async function handlePerformanceIssue(event: any) {
  // DynamoDB kapasitesini otomatik ölçeklendir
  await scaleDynamoDBCapacity(1.5); // %50 artış
  
  // Potansiyel yavaş query'leri kaldırmak için cache'i temizle
  await clearApplicationCache();
  
  // Performance metriklerini topla
  await collectPerformanceMetrics();
}

async function handleCapacityWarning(event: any) {
  // Capacity planning otomasyonu
  const projectedGrowth = await calculateGrowthTrend();
  
  if (projectedGrowth > 0.8) { // %80 büyüme trendi
    await scheduleCapacityReview();
    await notifyCapacityTeam(projectedGrowth);
  }
}

Operational Excellence Dersi: Otomasyon iyi yargıyı değiştirmez—onu kullanmak için zaman kazandırır. Otomatik response'larımız yaygın sorunların %80'ini hallediyor, insanları gerçekten karmaşık problemlere odaklanmaya bırakıyor.

Capacity Planning ve Büyüme Tahmini#

Engineering leader'larını gece uykusuz bırakan iş sorusu: "Sistemimiz Black Friday'i kaldırabilir mi?" Capacity planning'i mimarimize nasıl entegre ettiğimiz:

TypeScript
// functions/capacity-planning.ts - Büyüme tahmini ve capacity planning
import { CloudWatchClient, GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch';
import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';

interface CapacityProjection {
  currentCapacity: number;
  projectedDemand: number;
  recommendedCapacity: number;
  confidenceLevel: number;
  timeframe: string;
}

export const generateCapacityForecast = async (event: any): Promise<CapacityProjection> => {
  const cloudwatch = new CloudWatchClient({});
  const dynamodb = new DynamoDBClient({});

  // Tarihi trafik kalıplarını analiz et
  const historicalData = await getHistoricalMetrics(cloudwatch, 90); // 90 gün
  const seasonalPatterns = analyzeSeasonalTrends(historicalData);
  const growthTrend = calculateGrowthTrend(historicalData);

  // Mevcut capacity ayarlarını al
  const currentCapacity = await getCurrentCapacity(dynamodb);

  // Gelecekteki talebi tahmin et
  const projection = projectDemand({
    historicalData,
    seasonalPatterns,
    growthTrend,
    currentCapacity,
    timeframe: '30days',
  });

  // Uygulanabilir öneriler üret
  const recommendations = generateRecommendations(projection);

  // Capacity planning raporu oluştur
  await createCapacityReport({
    projection,
    recommendations,
    timestamp: new Date().toISOString(),
  });

  return projection;
};

async function getHistoricalMetrics(client: CloudWatchClient, days: number) {
  const endTime = new Date();
  const startTime = new Date(endTime.getTime() - days * 24 * 60 * 60 * 1000);

  const command = new GetMetricStatisticsCommand({
    Namespace: 'AWS/DynamoDB',
    MetricName: 'ConsumedReadCapacityUnits',
    Dimensions: [
      { Name: 'TableName', Value: 'links-table' },
    ],
    StartTime: startTime,
    EndTime: endTime,
    Period: 3600, // 1 saatlik periyotlar
    Statistics: ['Average', 'Maximum'],
  });

  const response = await client.send(command);
  return response.Datapoints || [];
}

function analyzeSeasonalTrends(data: any[]) {
  // Haftanın günü ve saate göre grupla
  const patterns = {
    hourly: new Array(24).fill(0),
    daily: new Array(7).fill(0),
    monthly: new Array(12).fill(0),
  };

  data.forEach(point => {
    const date = new Date(point.Timestamp);
    const hour = date.getHours();
    const day = date.getDay();
    const month = date.getMonth();

    patterns.hourly[hour] += point.Average;
    patterns.daily[day] += point.Average;
    patterns.monthly[month] += point.Average;
  });

  // Kalıpları normalize et
  return {
    peakHour: patterns.hourly.indexOf(Math.max(...patterns.hourly)),
    peakDay: patterns.daily.indexOf(Math.max(...patterns.daily)),
    peakMonth: patterns.monthly.indexOf(Math.max(...patterns.monthly)),
    variance: calculateVariance(patterns.hourly),
  };
}

function projectDemand(config: any): CapacityProjection {
  const {
    historicalData,
    seasonalPatterns,
    growthTrend,
    currentCapacity,
    timeframe,
  } = config;

  // Büyüme projeksiyonu için linear regression
  const baselineGrowth = growthTrend.slope * 30; // 30 günlük projeksiyon
  
  // Mevsimsel ayarlama
  const seasonalMultiplier = getSeasonalMultiplier(seasonalPatterns, timeframe);
  
  // İş etkinliği ayarlamaları (tatil satışları, pazarlama kampanyaları)
  const eventMultiplier = getBusinessEventMultiplier(timeframe);

  const projectedDemand = 
    currentCapacity.average * 
    (1 + baselineGrowth) * 
    seasonalMultiplier * 
    eventMultiplier;

  return {
    currentCapacity: currentCapacity.provisioned,
    projectedDemand: Math.ceil(projectedDemand),
    recommendedCapacity: Math.ceil(projectedDemand * 1.2), // %20 buffer
    confidenceLevel: calculateConfidence(growthTrend.r2),
    timeframe,
  };
}

function generateRecommendations(projection: CapacityProjection) {
  const recommendations = [];

  if (projection.projectedDemand > projection.currentCapacity * 0.8) {
    recommendations.push({
      type: 'SCALE_UP',
      urgency: 'HIGH',
      action: `DynamoDB kapasitesini ${projection.recommendedCapacity} RCU'ya artır`,
      estimatedCost: calculateCostIncrease(projection),
    });
  }

  if (projection.confidenceLevel &lt;0.7) {
    recommendations.push({
      type: 'MONITORING',
      urgency: 'MEDIUM',
      action: 'Projeksiyondaki düşük güven nedeniyle monitoring sıklığını artır',
      estimatedCost: 0,
    });
  }

  return recommendations;
}

Capacity Planning Gerçeği: İlk tahminimiz %300 hatalıydı çünkü viral pazarlama kampanyalarını hesaba katmadık. Artık business etkinlik takvimlerini teknik tahminlerimizle entegre ediyoruz. Pazarlama lansmanları ve engineering capacity planning artık ayrı konuşmalar değil.

Seri Özeti: Öğrendiklerimiz#

Beş bölüm ve binlerce satır CDK kodu sonrası, production-grade link shortener inşa etmenin bize gerçekten öğrettikleri:

Doğru Yaptığımız Şeyler#

  1. İlk Günden Infrastructure as Code: CDK ölçeklendirme ve felaketler sırasında bize sayısız saat kazandırdı
  2. Optimizasyondan Önce Observability: Ölçemediğin şeyi geliştiremezsin
  3. Tasarımla Güvenlik: Güvenliği sonradan eklemek baştan inşa etmekten 10 kat daha zor
  4. Baştan Multi-Region: Global kullanıcılar mimarinin yetişmesini beklemez

Farklı Yapacaklarımız#

  1. Sharding ile Başla: Hot partition'lar ölçekte kaçınılmaz—bunları planla
  2. Operational Excellence'e Erken Yatırım: İyi runbook'lar altın değerinde
  3. İlk Günden Business Metrikleri: Teknik metrikler business hikayesini anlatmaz
  4. Takım Süreçleri Ölçekle Evrimleşir: 3 engineer için çalışan şey 30 ile bozulur

Ölçeğin Gerçek Maliyeti#

50M redirect'i karşılayan son aylık AWS faturamız:

  • DynamoDB Global Tables: $1,200 (3 region, on-demand)
  • Lambda: $180 (cross-region invocation'lar dahil)
  • CloudFront: $45 (global distribution)
  • Route 53: $25 (health check'ler ve DNS)
  • Monitoring & Alerting: $80 (CloudWatch, X-Ray)
  • Data Transfer: $120 (cross-region replication)
  • Toplam: $1,650/ay enterprise-grade global altyapı için

Maliyet Gerçeklik Kontrolü: Bu redirect başına yaklaşık $0.000033. İnşa etmek ve maintain etmek için engineering zamanı? Setup, ölçeklendirme ve bakım boyunca yaklaşık 2.5 engineer full-time. Business değeri? Redirect'lerin kritik müşteri dokunma noktaları olduğunda ölçülemez.

Anahtar Mimari Kararlar ve Uzun Vadeli Etkileri#

DynamoDB Global Tables vs Aurora Global Database: Öngörülebilir performance ve pay-per-request faturalama için DynamoDB'yi seçtik. İki yıl sonra, dakikada 1K'dan 100K redirect'e değişen trafik spike'larıyla, bunu yaptığımız için memnunuz. Aurora daha fazla capacity planning overhead gerektirirdi.

Lambda vs ECS/Fargate: Lambda'nın cold start'ları başta endişeliydi, ama provisioned concurrency bunu çözdü. Container'ları yönetmemenin operational basitliği kazandı. Sunucu bakım sorunu yaşamadık çünkü sunucu yok.

CDK vs Terraform: CDK'nın Lambda function'larımızla TypeScript entegrasyonu infrastructure ve uygulama kodu arasındaki refactoring'i sorunsuz yaptı. Type safety deployment'tan önce düzinelerce konfigürasyon hatasını yakaladı.

Multi-Region Active-Active vs Active-Passive: Active-active implement etmesi daha karmaşıktı ama "failover testi" problemini ortadan kaldırdı. us-east-1 down olduğunda, trafik diğer region'lardan sorunsuz devam etti.

Ölçeğin İnsani Yönü#

Teknik ölçeklendirme hikayenin sadece yarısı. Takım ölçeklendirmesi hakkında öğrendiklerimiz:

  • Dokümantasyon Kritik Hale Gelir: Orijinal mimar ayrıldığında, tribal knowledge de gidiyor
  • On-Call Rotasyonu Yapı Gerektiriyor: Sisteminiz zaman dilimlerini kapsadığında burn out gerçek
  • Cross-Training Yatırımdır: Her component'i en az iki kişi anlamalı
  • Incident Review'lar Öğrenme Yaratır: Blameless postmortem'lar mimarimizi herhangi bir planlama seansından daha çok geliştirdi

Link Shortener'ların Ötesinde#

Bu kalıplar herhangi bir yüksek trafikli, düşük latency servise uygulanır:

  • Event-driven mimari request-response kalıplarından daha iyi ölçeklenir
  • Regional data locality kullanıcı yüzlü özellikler için global consistency'yi yener
  • Operational otomasyon iş ile kariyer arasındaki fark
  • Business alignment infrastructure maliyetlerini business yatırımlarına çevirir

İleriye Bakış#

Hafta sonu projesi olarak başlayan link shortener artık bazı Fortune 500 websitelerinden daha fazla trafiği karşılıyor. Modern cloud servisleri ve infrastructure as code ile küçük takımların sadece on yıl önce enterprise data center gerektiren sistemler inşa edebileceğinin hatırlatıcısı.

Asıl ders link shortener inşa etmek hakkında değil—işinle büyüyen, takımını destekleyen ve ölçeğin kaçınılmaz karmaşıklığından kurtulan sistemler inşa etmek hakkında. Link shortener, API gateway veya sonraki unicorn startup inşa ediyor olsan da, bu kalıplar sana iyi hizmet edecek.

Son Düşünce: Mimari trade-off'lar hakkında, ama operational excellence bu trade-off'ların sonuçlarını minimize etmek hakkında. Zarif şekilde başarısız olan, öngörülebilir şekilde ölçeklenen ve baskı altındaki insanlar tarafından maintain edilebilen sistemler inşa et. Gelecekteki benliğin sana teşekkür edecek.


Bu, sıfırdan production-scale link shortener'a 5 bölümlük yolculuğumuzu tamamlıyor. Tüm CDK construct'ları, Lambda function'ları ve deployment scriptleri ile tam kaynak kodu GitHub repository'de mevcut. Mutlu inşaatlar!

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.

İlerleme5/5 yazı tamamlandı
Loading...

Yorumlar (0)

Sohbete katıl

Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap

Henüz yorum yok

Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!

Related Posts