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ı.
AWS CDK Link Shortener Part 5: Ölçeklendirme ve Uzun Vadeli Bakım
Tek bölgede çalışan bir CDK link shortener, trafik coğrafyalara yayıldıkça gecikme ve erişilebilirlik sorunları yaşamaya başlar; DynamoDB tablosu ise 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. Bu yazıda DynamoDB Global Tables, multi-region CloudFront dağıtımı, kapasite planlaması ve sistemi uzun vadede ayakta tutan operasyonel kalıplar ele alınmaktadır.
1. Bölümde, link shortener altyapımızı inşa etmeye başladık. Ş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
Single-region kurulum günde 100K redirect için yeterliydi. Global pazarlarda 10M redirect’te, her milisaniye latency dönüşüm oranı sorununa dönüşür. Mimariyi şu şekilde evrimleştirmek gerekir:
// 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 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, 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 için regional deployment kalıbı:
// 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 Gerçeği: Bu sadece birden çok region’a deploy etmek değil. Data consistency, regional failover, maliyet etkileri ve operational karmaşıklığı dikkatli planlama gerektirir. DynamoDB Global Tables replication lag’ini anlamak, Route53 failover sürelerini test etmek ve cross-region maliyet etkilerini hesaplamak kritik adımlar. Her region ek trafik ve veri transfer maliyeti getirir; bunu kapasite planlamasına dahil et.
Veritabanı Ölçeklendirme Stratejileri: DynamoDB Auto-Scaling’in Ötesinde
Günde 10M+ istek alındığında, DynamoDB’nin auto-scaling’inin bile sınırları var. Hot partition’lar beklenmedik throttling’e yol açabilir; sharding stratejisi ve contributor insights bu tür sorunları erken tespit etmeye yardımcı olur. Production’da gerçekten çalışan kalıplar:
// 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,
});
}
}
Hot partition sorunları için sharding mantığı:
// 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 100 shard’da distributed query’leri debug etmek maliyetli. Basit çözümlerle başlayıp metrikler gerekli olduğunu kanıtladığında karmaşıklık eklemek daha sürdürülebilir. On-demand billing mode çoğu use case için yeterlidir; provisioned capacity sadece tahmin edilebilir trafik pattern’leri olan sistemlerde anlamlı. GSI’lar sorgu pattern’lerini optimize eder ama her index yazma maliyeti ve depolama ekler.
Felaket Kurtarma Planlaması
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. Etkili disaster recovery inşası:
// 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_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));
}
}
Kesinti kurtarma için backup otomasyonu:
// 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ç
Production sistemleri zamanla teknik borç biriktirir. Bu borcu üretimi bozmadan yönetmek sistematik 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 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_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 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_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)],
});
}
}
Teknik borçtan önde kalmak için maintenance otomasyonu:
// 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 sistemlerde teknoloji savaşın sadece yarısı. Diğer yarısı ölçeklenen takım süreçleri kurmak:
// 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'],
});
}
}
On-call yükünü azaltmak için runbook otomasyonu:
// 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
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 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;
}
Capacity Planning Gerçeği: İlk tahminler viral pazarlama kampanyalarını hesaba katmadığında %300 hata payıyla gerçekleşebilir. Business etkinlik takvimlerini teknik tahminlerle entegre etmek bu açığı kapatır. Pazarlama lansmanları ve engineering capacity planning ayrı konuşmalar olmamalı.
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
- İlk Günden Infrastructure as Code: CDK, ölçeklendirme ve felaket kurtarma sırasında manuel iş yükünü önemli ölçüde azaltır
- Optimizasyondan Önce Observability: Ölçemediğin şeyi geliştiremezsin
- Tasarımla Güvenlik: Güvenliği sonradan eklemek baştan inşa etmekten 10 kat daha zor
- Baştan Multi-Region: Global kullanıcılar mimarinin yetişmesini beklemez
Mimari Dengeler
- Sharding ile Başla: Hot partition’lar ölçekte kaçınılmaz - erken planla
- Operational Excellence’e Erken Yatırım: İyi runbook’lar altın değerinde
- İlk Günden Business Metrikleri: Teknik metrikler business hikayesini anlatmaz
- Takım Süreçleri Ölçekle Evrimleşir: 3 engineer için çalışan şey 30 ile bozulur
Ölçek için Maliyet Düşünceleri
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.
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 iyi bir seçim. Dakikada 1K’dan 100K redirect’e değişen trafik spike’larında bu tercih kendini kanıtlar. Aurora daha fazla capacity planning overhead gerektirir.
Lambda vs ECS/Fargate: Lambda’nın cold start’ları başta endişe kaynağı, ama provisioned concurrency bunu çözer. Container’ları yönetmemenin operational basitliği önemli bir avantaj. Serverless tercihinin temel getirisi budur.
CDK vs Terraform: CDK’nın TypeScript entegrasyonu infrastructure ve uygulama kodu arasındaki refactoring’i sorunsuz kılar. Type safety deployment’tan önce düzinelerce konfigürasyon hatasını yakalayabilir.
Multi-Region Active-Active vs Active-Passive: Active-active implementasyonu daha karmaşık olsa da failover testi gereksinimini ortadan kaldırır. Bir region down olduğunda trafik diğer region’lardan sorunsuz devam eder.
Ölçeğin İnsani Yönü
Teknik ölçeklendirme hikayenin sadece yarısı. Takım ölçeklendirmesinde göz önünde bulundurulması gereken konular:
- 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 mimariyi proaktif planlama seanslarından daha çok geliştirir
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
Sonraki Adımlar
Modern cloud servisleri ve infrastructure as code ile küçük takımların on yıl önce enterprise data center gerektiren sistemler inşa edebilmesi bu alanı demokratikleştirdi.
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.
Kaynaklar
- DynamoDB Global Tables - Küresel olarak dağıtılmış DynamoDB tabloları için çok bölgeli, çok aktif replikasyona ilişkin resmi belgeleme.
- Amazon Route 53 Geliştirici Kılavuzu - Çok bölgeli deployment’larda kullanılan gecikme tabanlı ve yük devretme yönlendirme politikaları referansı.
- AWS CDK Ortamları - Tek bir CDK uygulamasından birden fazla AWS hesabı ve bölgeyi hedefleme kılavuzu.
- AWS Lambda En İyi Uygulamaları - Bağımlılık yönetimi, performans optimizasyonu ve uzun vadeli bakım için resmi rehber.
- Amazon CloudWatch Alarmları - Yüksek ölçekli operasyonel izleme için metrik tabanlı ve bileşik alarm yapılandırma belgeleri.
- Amazon CloudFront Geliştirici Kılavuzu - Küresel CDN dağıtımı, kaynak yük devretmesi ve ölçekte önbellek optimizasyonu referansı.
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.
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.
Bu Serideki Tüm Yazılar
İlgili yazılar
CDK TypeScript Lambda için 9 bundler ve 3 cdk synth runner'ının ölçümlü karşılaştırması; her katman için varsayılan ve onu seçtiren kural.
CDK stack düzeni için yaşam döngüsü testi: bir kaynağın ömrü tek bir dağıtımdan uzunsa kendi uzun ömürlü stack'ine koyun, ona bilinen bir adla erişin.
Ölçekte karşılaşacağınız IAM boyut, ekleme ve kota limitleri; ve hepsinden uzak tutan kapsamlı politika, izin sınırı ve SCP yapısı.
AppSync subscription'ları yalnızca mutation ile tetiklenir. Downstream BFF olaylarını NONE veri kaynaklı bir mutation'a EventBridge ve CDK ile köprülemeyi inceliyorum.
AWS Bedrock + Knowledge Bases + OpenSearch Serverless üstüne CDK ile TypeScript kullanarak RAG agent kurmak: mimari, IAM bağlantısı, otomatik ingestion ve chat UI.