İçeriğe atla

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

Multi-region deployment, veritabanı ölçeklendirme, felaket kurtarma ve production sistemleri için uzun vadeli bakım kalıpları.

Ayhan Sipahi Ayhan Sipahi

Tek bölgede çalışan bir CDK link shortener, trafik coğrafyalara yayıldıkça gecikme ve erişilebilirlik sorunları göstermeye başlar. DynamoDB tablosu da tek hata noktasına dönüşür. Multi-region replikasyon ve bir disaster recovery planı olmadan, tek bir bölgesel kesinti redirect servisinin tamamını devre dışı bırakır. Tercih edilmesi gereken varsayılan kurulum şu: global bir CloudFront dağıtımının arkasında DynamoDB Global Tables ve bir bölge tamamen düştüğünde devreye giren Route 53 failover yönlendirmesi.

1. Bölümde, link shortener altyapısının temelini kurmuştuk. Bu bölüm onu global ölçeğe taşıyor ve çalışır durumda tutan operasyonel katmanı ekliyor: kapasite planlaması, yedekleme otomasyonu ve uzun ömürlü bir sistemin biriktirdiği teknik borcun bakımı.

Multi-Region Mimarisi#

Tek bölgeli kurulumlar orta seviye trafiği rahatça taşır, global ölçek ise farklı kalıplar ister. Trafik binlerden milyonlara çıkıp birden fazla bölgeye yayıldığında, latency kullanıcı deneyiminin belirleyici unsuru olur.

// 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 * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
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 - production'da kritik
      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 => {
      return new route53.HealthCheck(this, `HealthCheck-${region}`, {
        type: route53.HealthCheckType.HTTPS,
        resourcePath: '/health',
        fqdn: `${region}.${props.domainName}`,
        port: 443,
        requestInterval: cdk.Duration.seconds(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;
  }
}

Deployment adımı region listesi üzerinde döner; her biri için primary global stack’in yanına bir regional stack kurar.

// 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,
  });
});

Birden fazla bölgeye deploy etmek, replikasyonun yanında veri tutarlılığı, bölgesel failover, maliyet etkisi ve operasyonel karmaşıklık başlıklarını da gündeme getirir: DynamoDB Global Tables replication lag’ini ölçmek, Route 53 failover sürelerini test etmek ve bölgeler arası veri transfer maliyetini hesaplamak kurulumun asıl işidir.

Auto-Scaling’in Ötesinde Veritabanı Ölçeklendirmesi#

Yüksek trafikli uygulamalar, auto-scaling açık olsa bile DynamoDB’nin ölçeklendirme sınırlarına takılabilir. Hot partition’lar beklenmedik throttling’e yol açar; sharding stratejisi ve contributor insights bu sorunu erken görmeyi sağlar.

// 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_20_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,
    });
  }
}

Her yazma işlemi önce consistent hashing’den geçer; ortaya çıkan shard key hangi partition’a düşeceğini belirler.

// 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')}`;
}

Sharding operasyonel karmaşıklık ekler: yüz shard’a yayılmış distributed query’leri debug etmek ciddi araç desteği ister, bu yüzden basit, tek partition’lı tabloyu önce göndermek ve sharding’i ancak metrikler gerektirdiğinde eklemek mantıklı. Billing de aynı on-demand varsayımı izler, çünkü provisioned capacity sadece trafik öngörülebilir olduğunda karşılığını verir; GSI’lar da yazma maliyeti ve depolama ekler, bu yüzden sadece gerçekten ihtiyaç duyulan bir sorgu pattern’i için eklenmeli.

Felaket Kurtarma Otomasyonu#

Bölgesel kesintiler, disaster recovery planlarını gerçek koşullarda sınar. AWS us-east-1 gibi primary region’ların uzun süreli downtime yaşadığı senaryolarda, failover mekanizmaları ve yedekleme stratejileri değerini kanıtlar.

// 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';
import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';

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',
    });

    // Primary region health check'i
    const primaryHealthCheck = new route53.HealthCheck(this, 'PrimaryHealth', {
      type: route53.HealthCheckType.HTTPS,
      resourcePath: '/health',
      fqdn: 'us-east-1.api.example.com',
      port: 443,
      requestInterval: cdk.Duration.seconds(30),
      failureThreshold: 3,
      
      // Veri gelmediğinde sağlıksız say, üç bölgeden ölç
      insufficientDataHealthStatus:
        route53.InsufficientDataHealthStatusEnum.UNHEALTHY,
      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.Failover.PRIMARY,
      healthCheck: primaryHealthCheck,
    });

    // Secondary region health check'i
    const secondaryHealthCheck = new route53.HealthCheck(this, 'SecondaryHealth', {
      type: route53.HealthCheckType.HTTPS,
      resourcePath: '/health',
      fqdn: 'eu-west-1.api.example.com',
      port: 443,
      requestInterval: cdk.Duration.seconds(30),
      failureThreshold: 3,
    });

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

  // Cross-region backup otomasyonu
  private createBackupStrategy() {
    const backupFunction = new lambda.Function(this, 'BackupFunction', {
      runtime: lambda.Runtime.NODEJS_20_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));
  }
}

Backup job’u bir takvime göre çalışır; snapshot sıkıştırıldıktan sonra ikinci bir bölgeye kopyalanır.

// 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',
  }));
}

Route 53 health check’leri hatayı tespit edip failover’ı tetiklemek için tipik olarak 90-180 saniye harcar; bu gecikme kesinti boyunca kullanıcı deneyimine doğrudan yansır. Manuel override prosedürleri bu gecikme yaşanmadan önce hazır olmalı.

Uzun Vadeli Bakım ve Teknik Borç#

Production sistemleri, iş gereksinimleri değiştikçe zamanla teknik borç biriktirir. Bu borcu sistem kararlılığını bozmadan yönetmek sistematik bir yaklaşım gerektirir.

// 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 tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';

export class MaintenanceAutomationStack extends cdk.Stack {
  
  // Otomatik dependency update'leri
  private createDependencyUpdatePipeline() {
    const updateFunction = new lambda.Function(this, 'DependencyUpdater', {
      runtime: lambda.Runtime.NODEJS_20_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 cleanupChain = new tasks.LambdaInvoke(this, 'IdentifyExpiredLinks', {
      lambdaFunction: this.identifyExpiredLinksFunction,
    })
      .next(new tasks.LambdaInvoke(this, 'CreateBackupSnapshot', {
        lambdaFunction: this.createBackupFunction,
      }))
      .next(new tasks.LambdaInvoke(this, 'DeleteExpiredLinks', {
        lambdaFunction: this.deleteExpiredLinksFunction,
      }))
      .next(new tasks.LambdaInvoke(this, 'VerifyCleanup', {
        lambdaFunction: this.verifyCleanupFunction,
      }));

    const cleanupWorkflow = new stepfunctions.StateMachine(this, 'CleanupWorkflow', {
      definitionBody: stepfunctions.DefinitionBody.fromChainable(cleanupChain),
      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_20_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)],
    });
  }
}

Dependency güncellemeleri zamanlanmış bir job olarak çalışır, sadece patch ve minor sürümlere dokunur, ardından review için bir pull request açar.

// 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 ölçekte sorunun yalnızca bir kısmını teknoloji çözer. Geri kalanı takım süreci: dashboard’da ne göründüğü, kimin çağrıldığı ve bir incident’ın nasıl kalıcı düzeltmeye dönüştüğü.

// 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';
import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';
import * as iam from 'aws-cdk-lib/aws-iam';

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: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchReadOnlyAccess'),
      ],
    });
  }
}

Incident response’un buradan sonraki dalını severity belirler: P1’ler PagerDuty’ye taşınır, P3’ler bir kapasite gözden geçirmesi başlatır.

// 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);
  }
}

Otomasyon rutin sorunları kendi başına kapatır, ama karmaşık problemler yine de insan gözetimi ister. İyi tasarlanmış otomatik response’lar sık görülen senaryoları üstlenir.

Capacity Planning ve Büyüme Tahmini#

Trafik artışı tahminleri, sistem tasarımının kritik bir boyutu. Peak alışveriş dönemleri gibi büyük etkinliklerde sistemin hazır olup olmadığını belirlemek için capacity planning mimariye baştan entegre edilmelidir.

// 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 < 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;
}

İlk kapasite tahminleri, trafiği ciddi biçimde yukarı çeken pazarlama kampanyaları gibi iş etkinliklerini çoğu zaman hesaba katmaz; iş takvimini teknik tahminle birleştirmek bu isabetsizliği kapatır.

Seri Özeti: Öğrendiklerimiz#

Production sistemlerin ölçeklendirilmesi, belirli use case’lerin ötesinde geçerli mimari ve operasyonel dersler ortaya çıkarır. Link shortener ölçeklendirmesinden çıkan temel içgörüler:

Bu Yaklaşımın Güçlü Yönleri#

  1. İlk Günden Infrastructure as Code: CDK, ölçeklendirme ve felaket kurtarma sırasındaki manuel işin büyük kısmını ortadan kaldırır
  2. Observability ve Tasarımla Güvenlik: güvenliği sonradan eklemek, baştan kurmaktan çok daha maliyetlidir
  3. Kullanıcılar Globalse Multi-Region: replikasyon, trafiğin ölçülebilir bir kısmı primary region dışında olduğunda maliyetini karşılar

Mimari Dengeler#

  1. Shard Key’i Erken Tasarla: hot partition’lar ölçekte tahmin edilebilir hale gelir. Shard key’i ihtiyaç duymadan önce seçin; ilk deployment tek partition ile çıksa bile
  2. Operational Excellence’e Erken Yatırım: kesintiden önce yazılan tek bir runbook, sonradan yazılan birçok runbook’un yerini tutar
  3. İlk Günden Business Metrikleri: teknik metrikler tek başına işin business etkisini açıklamaz; business metriklerini en baştan toplayın
  4. Takım Süreçleri Ölçekle Evrimleşir: küçük bir takım için işleyen süreçler, on-call rotasyonu birden fazla zaman dilimine yayıldığında işlemez hale gelir

Ölçekte Maliyet Kalemleri#

Global altyapı maliyetleri hem trafik hacmi hem de coğrafi dağılımla ölçeklenir. Yüksek trafikli redirect servisleri için tipik bir maliyet kompozisyonu şöyle:

  • DynamoDB Global Tables: Çok bölgeli veriler için maliyetin önemli bir kısmı
  • Lambda: Verimli istek-başı faturalama ile orta düzey maliyet
  • CloudFront: Global içerik dağıtımı için nispeten düşük maliyet
  • Route 53: DNS ve sağlık kontrolleri için minimum maliyet
  • Monitoring & Alerting: Vazgeçilmez operasyonel yük
  • Data Transfer: Bölgeler arası replikasyon ölçülebilir maliyet ekler

Not: Maliyetler kullanım pattern’leri, bölgeler ve AWS fiyatlandırma değişikliklerine bağlı olarak ciddi biçimde değişir. Kendi gereksinimleriniz için güncel fiyatlandırmayı her zaman doğrulayın.

Engineering yatırımı tipik olarak kurulum, ölçeklendirme ve sürekli bakım için ayrılmış takım üyeleri gerektirir. İş değeri, redirect performansının kullanıcı deneyimi ve dönüşüm oranları açısından ne kadar kritik olduğuna bağlıdır.

Kritik Mimari Kararlar ve Uzun Vadeli Etkileri#

KararDenge
DynamoDB Global Tables - Aurora Global DatabaseDynamoDB, öngörülebilir performans ve değişken trafik kalıplarına oturan pay-per-request faturalama sunar; Aurora Global Database daha güçlü tutarlılık garantileri verir ama daha fazla kapasite planlaması ister.
Lambda - ECS/FargateLambda sunucu yönetimini tamamen ortadan kaldırır; bedeli cold start’lardır, provisioned concurrency da bu gecikmenin çoğunu örtüyor. Container servisleri runtime üzerinde daha fazla kontrol sağlar ama sunucuları kendiniz işletmek de bu işin bir parçasıdır.
CDK - TerraformCDK’nın TypeScript entegrasyonu infrastructure ve uygulama kodu arasında type safety sağlar, böylece konfigürasyon hataları geliştirme sırasında yakalanır. Terraform ise daha geniş provider desteği ve olgun bir ekosistem sunar.
Active-active - active-passiveActive-active kurulumlar kesinti anında daha iyi dayanır ama implementasyonu daha karmaşıktır. Active-passive kurmak daha kolaydır, yeter ki failover yolu önceden test edilip koordine edilsin.

Takımı Ölçeklendirmek#

Teknik ölçeklendirme, takım ölçeklendirmesini de beraberinde getirir. En çok fark yaratan organizasyonel kalıplar:

  • Dokümantasyon Gereksinimi: takım bileşimi değiştikçe sistem bilgisinin yazıya geçirilmesi ve güncel tutulması gerekir
  • On-Call Düzeni: global sistemler yapılandırılmış rotasyon ve net eskalasyon prosedürleri ister
  • Bilginin Dağıtılması: her kritik bileşeni birden fazla takım üyesi anlamalı
  • Olaylardan Öğrenmek: yapılandırılmış incident review’ları, proaktif planlamanın kaçırdığı mimari iyileştirmeleri ortaya çıkarır

Bu kalıplar, yüksek trafikli ve düşük gecikmeli her servis için geçerli:

  • Event-driven, bölgesel olarak yerel tasarım: event-driven mimari request-response kalıplarından daha iyi ölçeklenir, veriyi bölgede tutmak da kullanıcıya dokunan özelliklerde global consistency’yi geride bırakır
  • Büyümenin kaldıracı olarak otomasyon: operasyonel işin ne kadarının otomatikleştirildiği, büyümenin ek yük mü yaratacağını yoksa ek personel almadan mı emileceğini belirler
  • Yatırım olarak çerçevelenen maliyetler: infrastructure harcamasını sağladığı business sonuçlarına bağlamak, onu bir maliyet kaleminden bir yatırım gerekçesine dönüştürür

İleriye Bakış#

Modern cloud servisleri ve infrastructure as code, küçük takımların daha önce ciddi data center yatırımı gerektiren sistemleri kurabilmesini sağlıyor. Bu değişim, sistem tasarımının ve kapasite planlamasının nereden başladığını da değiştiriyor.

Bu varsayılan, trafik gerçekten bölgeler arasına yayıldığında ve bölgesel bir kesinti kullanıcıya yansıyacağında geçerli olur. Bu eşiğin altında replikasyon maliyeti, eventual consistency penceresi ve ikinci bir on-call yüzeyi ödemeye değmez; bu yüzden kurulum, gerçek kullanıcı latency ölçümleri aksini söyleyene kadar tek bölgede kalır ve point-in-time recovery açık tutulur. Aynı sınır, sadece link shortener’lar için değil, yüksek trafikli her redirect ve lookup servisi için geçerli.

Kaynaklar#

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

İlgili yazılar