2025-09-05
AWS CDK Link Shortener Bölüm 4: Production Deployment ve Optimizasyon
Multi-environment deployment stratejileri, ölçekte performans optimizasyonu, ve maliyet yönetimi. Production deneyimleri ve öğrenilen dersler ile doğru monitoring ve incident response pattern'ları.
Production Deployment ve Optimizasyon
Production optimizasyonu işleri hızlandırmanın ötesinde - herhangi bir yük koşulu altında tahmin edilebilir performans gerektirir. Trafik beklenmedik şekilde arttığında, staging’de mükemmel çalışan infrastructure production’da ölçeklendirme darboğazlarını ortaya çıkarabilir.
En yaygın gözden kaçırılan şey? Database’in peak yükler yerine steady-state trafik için provisionlanması. Normal operasyonlar için optimize edilmiş bir DynamoDB table, kampanyalar veya ürün lansmanları sırasında trafik 10x arttığında darboğaz haline gelebilir.
Bölüm 1-3’te temel, core fonksiyon ve güvenlik inşa ettik. Şimdi onu production deployment ve optimizasyon için kurşun geçirmez yapalım.
Multi-Environment Deployment: Dev ve Prod’un Ötesinde
Çoğu tutorial sana dev ve prod environment’larını gösterir. Gerçekte, en az dörte ihtiyacın var: dev, staging, pre-prod, ve production. İşte neden ve nasıl inşa ettiğin:
// bin/link-shortener.ts - Bizi lansman gününden geçiren app entry point'i
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { LinkShortenerStack } from '../lib/link-shortener-stack';
import { DatabaseStack } from '../lib/database-stack';
import { MonitoringStack } from '../lib/monitoring-stack';
const app = new cdk.App();
// Takımınla ölçeklenen environment konfigürasyonu
const environments = {
dev: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'us-west-2', // Dev için daha ucuz region
stage: 'dev',
domain: 'dev-links.yourcompany.com',
customDomain: false,
monitoring: {
detailedMetrics: false,
logRetention: 7, // Günler
alerting: false,
},
database: {
billingMode: 'PAY_PER_REQUEST',
pointInTimeRecovery: false,
backupRetention: 7,
},
lambda: {
reservedConcurrency: 10, // Dev maliyetlerini sınırla
memorySize: 512,
timeout: 30,
}
},
staging: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'us-east-1',
stage: 'staging',
domain: 'staging-links.yourcompany.com',
customDomain: true,
monitoring: {
detailedMetrics: true,
logRetention: 14,
alerting: true,
},
database: {
billingMode: 'PAY_PER_REQUEST',
pointInTimeRecovery: true,
backupRetention: 14,
},
lambda: {
reservedConcurrency: 50,
memorySize: 1024,
timeout: 30,
}
},
'pre-prod': {
account: process.env.CDK_PREPROD_ACCOUNT,
region: 'us-east-1',
stage: 'pre-prod',
domain: 'pp-links.yourcompany.com',
customDomain: true,
monitoring: {
detailedMetrics: true,
logRetension: 30,
alerting: true,
},
database: {
billingMode: 'PROVISIONED', // Production pattern'larını match et
readCapacity: 100,
writeCapacity: 50,
pointInTimeRecovery: true,
backupRetention: 30,
},
lambda: {
reservedConcurrency: 200,
memorySize: 1024,
timeout: 30,
}
},
production: {
account: process.env.CDK_PROD_ACCOUNT,
region: 'us-east-1',
stage: 'prod',
domain: 'go.yourcompany.com',
customDomain: true,
monitoring: {
detailedMetrics: true,
logRetention: 90,
alerting: true,
dashboard: true,
},
database: {
billingMode: 'PROVISIONED',
readCapacity: 500, // Conservative başla, auto-scale up
writeCapacity: 200,
pointInTimeRecovery: true,
backupRetention: 90,
globalTables: true, // Multi-region disaster recovery
},
lambda: {
reservedConcurrency: 1000,
memorySize: 1024,
timeout: 30,
provisionedConcurrency: 10, // Bazı function'ları warm tut
}
}
};
const stage = app.node.tryGetContext('stage') || 'dev';
const config = environments[stage as keyof typeof environments];
if (!config) {
throw new Error(`Invalid stage: ${stage}. Available stages: ${Object.keys(environments).join(', ')}`);
}
// Dependency'lerle mantıklı sırada deploy et
const databaseStack = new DatabaseStack(app, `LinkShortener-Database-${stage}`, {
env: { account: config.account, region: config.region },
stage,
config: config.database,
});
const appStack = new LinkShortenerStack(app, `LinkShortener-App-${stage}`, {
env: { account: config.account, region: config.region },
stage,
config,
database: databaseStack.database,
});
// Monitoring'i sadece staging+ environment'larda deploy et
if (stage !== 'dev') {
new MonitoringStack(app, `LinkShortener-Monitoring-${stage}`, {
env: { account: config.account, region: config.region },
stage,
config: config.monitoring,
appStack,
});
}
Neden dört environment? Her biri belirli bir amaca hizmet eder:
- Dev: Deneyim için maliyet kontrolleri ile geliştirme izolasyonu
- Staging: Production benzeri veriler ve konfigurasyonlarla integration testi
- Pre-prod: Load testing ve final validasyon için production replikası
- Production: Tam monitoring ve redundancy ile canlı environment
Performans Optimizasyonu: Lambda Cold Start’ları ve Daha Fazlası
50 milyon redirect’i process ettikten sonra, gerçekten ibre’yi hareket ettiren optimizasyonlar bunlar:
1. Önemli Lambda Konfigürasyonu
// lib/constructs/optimized-lambda.ts
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
export interface OptimizedLambdaProps {
entry: string;
stage: string;
reservedConcurrency?: number;
provisionedConcurrency?: number;
memorySize?: number;
}
export class OptimizedLambda extends Construct {
public readonly function: nodejs.NodejsFunction;
constructor(scope: Construct, id: string, props: OptimizedLambdaProps) {
super(scope, id);
this.function = new nodejs.NodejsFunction(this, 'Function', {
entry: props.entry,
handler: 'handler',
runtime: lambda.Runtime.NODEJS_20_X,
// Memory konfigürasyonu CPU'yu etkiler - çoğu workload için sweet spot
memorySize: props.memorySize || 1024,
// Hızlı fail için yeterince agresif timeout
timeout: cdk.Duration.seconds(30),
// Optimizasyon için environment variable'lar
environment: {
NODE_OPTIONS: '--enable-source-maps',
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', // TCP bağlantılarını yeniden kullan
POWERTOOLS_SERVICE_NAME: 'link-shortener',
POWERTOOLS_METRICS_NAMESPACE: 'LinkShortener',
},
// Bundle optimizasyonu
bundling: {
minify: true,
sourceMap: true,
target: 'es2022',
format: nodejs.OutputFormat.ESM,
banner: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);',
externalModules: [
'@aws-sdk/*', // AWS SDK'yı bundle'lama
],
esbuildArgs: {
'--tree-shaking': 'true',
'--platform': 'node',
'--target': 'node20',
},
},
// Bir function'ın tüm kapasiteyi yemesini engellemek için reserved concurrency
reservedConcurrency: props.reservedConcurrency,
// VPC konfigürasyonu sadece ihtiyacın varsa (cold start'lara 1-2s ekler)
// vpc: props.stage === 'prod' ? vpc : undefined,
});
// Production kritik path'ler için provisioned concurrency
if (props.provisionedConcurrency && props.stage === 'prod') {
const version = this.function.currentVersion;
new lambda.Alias(this, 'ProductionAlias', {
aliasName: 'prod',
version,
provisionedConcurrencyConfig: {
provisionedConcurrentExecutions: props.provisionedConcurrency,
},
});
}
// Performans insight'ları için X-Ray tracing
this.function.addEnvironment('_X_AMZN_TRACE_ID', '${_X_AMZN_TRACE_ID}');
}
}
2. Gerçekten Çalışan Connection Pooling
Bulduğumuz bir numaralı performans katili? Her invocation’da yeni DynamoDB bağlantıları yaratmak. İşte production connection manager’ımız:
// src/utils/dynamodb-connection.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
// Global connection pool - Lambda invocation'ları arasında yaşar
let dynamoClient: DynamoDBDocumentClient | null = null;
export function getDynamoClient(): DynamoDBDocumentClient {
if (!dynamoClient) {
const client = new DynamoDBClient({
region: process.env.AWS_REGION,
// Connection pooling konfigürasyonu
maxAttempts: 3,
requestHandler: {
// Lambda runtime için optimize
connectionTimeout: 1000, // 1s timeout
requestTimeout: 5000, // 5s toplam request timeout
// Connection pooling
httpsAgent: {
maxSockets: 10, // Default 50'den düşürüldü
keepAlive: true,
keepAliveMsecs: 30000,
},
},
// Credential'ların client-side caching'i
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
sessionToken: process.env.AWS_SESSION_TOKEN,
},
});
dynamoClient = DynamoDBDocumentClient.from(client, {
marshallOptions: {
convertEmptyValues: false,
removeUndefinedValues: true,
convertClassInstanceToMap: false,
},
unmarshallOptions: {
wrapNumbers: false,
},
});
// Debugging için connection yaratımını logla
console.log('DynamoDB connection pool initialized');
}
return dynamoClient;
}
// Performans monitoring wrapper
export async function withPerformanceLogging<T>(
operation: string,
fn: () => Promise<T>
): Promise<T> {
const start = Date.now();
try {
const result = await fn();
const duration = Date.now() - start;
console.log(JSON.stringify({
operation,
duration,
success: true,
timestamp: new Date().toISOString(),
}));
return result;
} catch (error) {
const duration = Date.now() - start;
console.error(JSON.stringify({
operation,
duration,
success: false,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
}));
throw error;
}
}
3. Production-Optimize Edilmiş Redirect Handler
// src/handlers/redirect-optimized.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { getDynamoClient, withPerformanceLogging } from '../utils/dynamodb-connection';
import { GetCommand } from '@aws-sdk/lib-dynamodb';
// Cold start tracking'i handler'ın dışında declare et
let isColdStart = true;
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
const startTime = Date.now();
const coldStart = isColdStart;
isColdStart = false;
// Path'den short code'u extract et
const shortCode = event.pathParameters?.proxy || event.pathParameters?.shortCode;
if (!shortCode) {
return createErrorResponse(404, 'Short code not found');
}
try {
const dynamodb = getDynamoClient();
// Projection ile optimize edilmiş DynamoDB query
const result = await withPerformanceLogging(
'GetShortUrl',
() => dynamodb.send(new GetCommand({
TableName: process.env.URLS_TABLE_NAME!,
Key: { shortCode },
ProjectionExpression: 'originalUrl, expiresAt, clickCount',
ConsistentRead: false, // Eventually consistent redirect'ler için yeterli
}))
);
if (!result.Item) {
// Analytics için 404 logla ama block etme
logAnalyticsAsync('404', shortCode, event).catch(console.error);
return createErrorResponse(404, 'Link not found');
}
const { originalUrl, expiresAt } = result.Item;
// Expiration kontrolü
if (expiresAt && Date.now() > expiresAt) {
logAnalyticsAsync('EXPIRED', shortCode, event).catch(console.error);
return createErrorResponse(410, 'Link has expired');
}
// Click count'u asynchronous olarak update et (fire-and-forget)
updateClickCountAsync(shortCode).catch(console.error);
// Başarılı redirect'i logla
logAnalyticsAsync('SUCCESS', shortCode, event).catch(console.error);
const responseTime = Date.now() - startTime;
// Monitoring için structured logging
console.log(JSON.stringify({
event: 'redirect_success',
shortCode,
responseTime,
coldStart,
userAgent: event.headers['User-Agent']?.substring(0, 100),
referer: event.headers['Referer']?.substring(0, 100),
timestamp: new Date().toISOString(),
}));
return {
statusCode: 301, // Caching için permanent redirect
headers: {
Location: originalUrl,
'Cache-Control': 'public, max-age=300, s-maxage=3600', // 5dk browser, 1sa CDN
'X-Response-Time': responseTime.toString(),
'X-Cold-Start': coldStart.toString(),
},
body: '',
};
} catch (error) {
const responseTime = Date.now() - startTime;
console.error(JSON.stringify({
event: 'redirect_error',
shortCode,
error: error instanceof Error ? error.message : String(error),
responseTime,
coldStart,
timestamp: new Date().toISOString(),
}));
return createErrorResponse(500, 'Internal server error');
}
};
async function updateClickCountAsync(shortCode: string): Promise<void> {
try {
const dynamodb = getDynamoClient();
await dynamodb.send(new UpdateCommand({
TableName: process.env.URLS_TABLE_NAME!,
Key: { shortCode },
UpdateExpression: 'ADD clickCount :inc SET lastClickAt = :timestamp',
ExpressionAttributeValues: {
':inc': 1,
':timestamp': Date.now(),
},
}));
} catch (error) {
// Analytics update başarısız olursa redirect'i fail etme
console.error('Failed to update click count:', error);
}
}
async function logAnalyticsAsync(
eventType: string,
shortCode: string,
event: APIGatewayProxyEvent
): Promise<void> {
// Async analytics logging için implementation
// Bu tipik olarak ayrı bir analytics table'a veya queue'ya yazar
}
function createErrorResponse(statusCode: number, message: string): APIGatewayProxyResult {
return {
statusCode,
headers: {
'Content-Type': 'text/html',
'Cache-Control': 'no-cache',
},
body: `
<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body style="font-family: Arial, sans-serif; text-align: center; margin-top: 100px;">
<h1>${statusCode}</h1>
<p>${message}</p>
</body>
</html>
`,
};
}
Maliyet Optimizasyonu: Pahalı Hatalardan Dersler
Trafik kalıpları beklenmedik şekilde değiştiğinde maliyet optimizasyonu kritik hale gelir. Farklı AWS servislerinin nasıl ölçeklendiğini ve faturalandığını anlamak, yüksek trafik dönemlerinde bütçe sürprizlerini önlemeye yardımcı olur:
1. DynamoDB Optimizasyon Stratejisi
// lib/database-stack-optimized.ts
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as applicationautoscaling from 'aws-cdk-lib/aws-applicationautoscaling';
import { Construct } from 'constructs';
export class OptimizedDatabaseStack extends Construct {
public readonly linksTable: dynamodb.Table;
constructor(scope: Construct, id: string, props: {
stage: string;
expectedReadsPerSecond: number;
expectedWritesPerSecond: number;
}) {
super(scope, id);
this.linksTable = new dynamodb.Table(this, 'LinksTable', {
partitionKey: {
name: 'shortCode',
type: dynamodb.AttributeType.STRING,
},
// On-demand ile başla, pattern'ları anladığın zaman provisioned'a geç
billingMode: props.stage === 'prod'
? dynamodb.BillingMode.PROVISIONED
: dynamodb.BillingMode.PAY_PER_REQUEST,
// Production için provisioned capacity
...(props.stage === 'prod' && {
readCapacity: Math.max(5, Math.ceil(props.expectedReadsPerSecond * 1.2)),
writeCapacity: Math.max(5, Math.ceil(props.expectedWritesPerSecond * 1.2)),
}),
pointInTimeRecovery: props.stage === 'prod',
deletionProtection: props.stage === 'prod',
// Compliance için encryption
encryption: dynamodb.TableEncryption.AWS_MANAGED,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES, // Analytics için
});
// Production için auto-scaling
if (props.stage === 'prod') {
this.setupAutoScaling();
}
// Analytics query'leri için Global Secondary Index
this.linksTable.addGlobalSecondaryIndex({
indexName: 'UserIndex',
partitionKey: {
name: 'userId',
type: dynamodb.AttributeType.STRING,
},
sortKey: {
name: 'createdAt',
type: dynamodb.AttributeType.NUMBER,
},
projectionType: dynamodb.ProjectionType.KEYS_ONLY, // Maliyetleri minimize et
// Ana table ile aynı billing mode
...(props.stage === 'prod' && {
readCapacity: Math.max(5, Math.ceil(props.expectedReadsPerSecond * 0.1)),
writeCapacity: Math.max(5, Math.ceil(props.expectedWritesPerSecond * 1.0)),
}),
});
}
private setupAutoScaling(): void {
// Read capacity auto-scaling
const readScaling = this.linksTable.autoScaleReadCapacity({
minCapacity: 5,
maxCapacity: 1000, // Makul tavan
});
readScaling.scaleOnUtilization({
targetUtilizationPercent: 70, // Conservative target
scaleInCooldown: cdk.Duration.minutes(5),
scaleOutCooldown: cdk.Duration.minutes(1),
});
// Write capacity auto-scaling
const writeScaling = this.linksTable.autoScaleWriteCapacity({
minCapacity: 5,
maxCapacity: 500,
});
writeScaling.scaleOnUtilization({
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.minutes(5),
scaleOutCooldown: cdk.Duration.minutes(1),
});
}
}
2. Maksimum Maliyet Verimliliği için CloudFront Konfigürasyonu
// lib/cdn-stack-optimized.ts
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import { Construct } from 'constructs';
export class OptimizedCDNStack extends Construct {
public readonly distribution: cloudfront.Distribution;
constructor(scope: Construct, id: string, props: {
apiGateway: apigateway.RestApi;
stage: string;
}) {
super(scope, id);
this.distribution = new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: {
origin: new origins.RestApiOrigin(props.apiGateway),
// Redirect'ler için optimize edilmiş caching policy
cachePolicy: new cloudfront.CachePolicy(this, 'RedirectCachePolicy', {
cachePolicyName: `link-shortener-${props.stage}`,
defaultTtl: cdk.Duration.minutes(5),
maxTtl: cdk.Duration.hours(24),
minTtl: cdk.Duration.minutes(1),
// Sadece path'e göre cache (query string'leri ve header'ları ignore et)
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
}),
// Compression bandwidth maliyetlerini azaltır
compress: true,
// Redirect'ler için sadece GET request'lerine izin ver
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
// API endpoint'leri için ek behavior (caching yok)
additionalBehaviors: {
'/api/*': {
origin: new origins.RestApiOrigin(props.apiGateway),
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
},
},
// Kritik olmayan uygulamalar için en ucuz price class'ı kullan
priceClass: props.stage === 'prod'
? cloudfront.PriceClass.PRICE_CLASS_100 // ABD, Kanada, Avrupa
: cloudfront.PriceClass.PRICE_CLASS_100,
// Error handling
errorResponses: [
{
httpStatus: 404,
responseHttpStatus: 404,
responsePagePath: '/404.html',
ttl: cdk.Duration.minutes(5), // Origin'i döve döve yemesini engellemek için 404'leri cache'le
},
{
httpStatus: 500,
responseHttpStatus: 500,
responsePagePath: '/500.html',
ttl: cdk.Duration.minutes(1), // Server error'lar için kısa cache
},
],
// Analytics için logging'i enable et (ek maliyet ama insight'lar için gerekli)
...(props.stage === 'prod' && {
enableLogging: true,
logBucket: s3.Bucket.fromBucketName(this, 'LogsBucket', `cloudfront-logs-${props.stage}`),
logFilePrefix: 'link-shortener/',
}),
});
}
}
3. Maliyet Monitoring ve Alert’ler
// lib/cost-monitoring-stack.ts
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import { Construct } from 'constructs';
export class CostMonitoringStack extends Construct {
constructor(scope: Construct, id: string, props: {
stage: string;
alertEmail: string;
monthlyBudget: number;
}) {
super(scope, id);
// Maliyet alert'leri için SNS topic
const alertTopic = new sns.Topic(this, 'CostAlerts', {
displayName: `Link Shortener Cost Alerts - ${props.stage}`,
});
alertTopic.addSubscription(
new subscriptions.EmailSubscription(props.alertEmail)
);
// DynamoDB maliyet monitoring
const dynamoReadAlarm = new cloudwatch.Alarm(this, 'DynamoReadUnitsHigh', {
metric: new cloudwatch.Metric({
namespace: 'AWS/DynamoDB',
metricName: 'ConsumedReadCapacityUnits',
dimensionsMap: {
TableName: 'LinksTable', // Gerçek table adıyla replace et
},
statistic: 'Sum',
period: cdk.Duration.minutes(5),
}),
threshold: 1000, // Bütçene göre ayarla
evaluationPeriods: 2,
alarmDescription: 'DynamoDB read capacity kullanımı yüksek',
});
dynamoReadAlarm.addAlarmAction(
new actions.SnsAction(alertTopic)
);
// Lambda invocation maliyet monitoring
const lambdaInvocationsAlarm = new cloudwatch.Alarm(this, 'LambdaInvocationsHigh', {
metric: new cloudwatch.Metric({
namespace: 'AWS/Lambda',
metricName: 'Invocations',
dimensionsMap: {
FunctionName: 'redirect-handler', // Gerçek function adıyla replace et
},
statistic: 'Sum',
period: cdk.Duration.hours(1),
}),
threshold: 100000, // Saatte 100k invocation
evaluationPeriods: 1,
alarmDescription: 'Lambda invocation'lar alışılmadık derecede yüksek',
});
lambdaInvocationsAlarm.addAlarmAction(
new actions.SnsAction(alertTopic)
);
// Maliyet dashboard'u yarat
new cloudwatch.Dashboard(this, 'CostDashboard', {
dashboardName: `LinkShortener-Costs-${props.stage}`,
widgets: [
[
new cloudwatch.GraphWidget({
title: 'DynamoDB Read Capacity Units',
left: [dynamoReadAlarm.metric],
width: 12,
}),
],
[
new cloudwatch.GraphWidget({
title: 'Lambda Invocations',
left: [lambdaInvocationsAlarm.metric],
width: 12,
}),
],
[
new cloudwatch.GraphWidget({
title: 'CloudFront Requests',
left: [
new cloudwatch.Metric({
namespace: 'AWS/CloudFront',
metricName: 'Requests',
statistic: 'Sum',
period: cdk.Duration.hours(1),
}),
],
width: 12,
}),
],
],
});
}
}
Production Monitoring: “Çalışıyor”un Ötesinde
En büyük incident’ımız sırasında bizi kurtaran monitoring:
1. Önemli Custom Metric’ler
// src/utils/metrics.ts
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';
const cloudwatch = new CloudWatchClient({ region: process.env.AWS_REGION });
export class MetricsCollector {
private namespace = 'LinkShortener/Production';
private metrics: Array<{
MetricName: string;
Value: number;
Unit: string;
Timestamp: Date;
Dimensions?: Array<{ Name: string; Value: string }>;
}> = [];
async recordRedirectSuccess(shortCode: string, responseTime: number, coldStart: boolean): Promise<void> {
this.metrics.push(
{
MetricName: 'RedirectResponseTime',
Value: responseTime,
Unit: 'Milliseconds',
Timestamp: new Date(),
Dimensions: [
{ Name: 'ColdStart', Value: coldStart.toString() },
],
},
{
MetricName: 'RedirectCount',
Value: 1,
Unit: 'Count',
Timestamp: new Date(),
Dimensions: [
{ Name: 'Status', Value: 'Success' },
],
}
);
await this.flush();
}
async recordDatabaseLatency(operation: string, latency: number): Promise<void> {
this.metrics.push({
MetricName: 'DatabaseLatency',
Value: latency,
Unit: 'Milliseconds',
Timestamp: new Date(),
Dimensions: [
{ Name: 'Operation', Value: operation },
],
});
await this.flush();
}
async recordError(errorType: string, shortCode?: string): Promise<void> {
this.metrics.push({
MetricName: 'ErrorCount',
Value: 1,
Unit: 'Count',
Timestamp: new Date(),
Dimensions: [
{ Name: 'ErrorType', Value: errorType },
...(shortCode ? [{ Name: 'ShortCode', Value: shortCode }] : []),
],
});
await this.flush();
}
private async flush(): Promise<void> {
if (this.metrics.length === 0) return;
try {
await cloudwatch.send(new PutMetricDataCommand({
Namespace: this.namespace,
MetricData: this.metrics,
}));
this.metrics = []; // Başarılı send'den sonra temizle
} catch (error) {
console.error('Failed to send metrics:', error);
// Throw etme - metric başarısızlıkları ana fonksiyonu bozmamalı
}
}
}
// Singleton instance
export const metrics = new MetricsCollector();
2. Gerçekliği Simüle Eden Load Testing
// tests/load-test.ts - Lansman günü sorunumuzu yakalayacak test
import { performance } from 'perf_hooks';
interface LoadTestConfig {
baseUrl: string;
concurrentUsers: number;
testDurationMs: number;
rampUpMs: number;
shortCodes: string[];
}
interface LoadTestResult {
totalRequests: number;
successfulRequests: number;
failedRequests: number;
averageResponseTime: number;
p50ResponseTime: number;
p95ResponseTime: number;
p99ResponseTime: number;
errorsPerSecond: number;
requestsPerSecond: number;
}
export async function runLoadTest(config: LoadTestConfig): Promise<LoadTestResult> {
const results: Array<{
success: boolean;
responseTime: number;
timestamp: number;
error?: string;
}> = [];
const startTime = performance.now();
const endTime = startTime + config.testDurationMs;
// Her concurrent kullanıcı için promise yarat
const userPromises = Array.from({ length: config.concurrentUsers }, async (_, userIndex) => {
// Ramp-up sırasında kullanıcı başlangıç zamanlarını stagger et
const userStartDelay = (config.rampUpMs * userIndex) / config.concurrentUsers;
await sleep(userStartDelay);
while (performance.now() < endTime) {
const requestStart = performance.now();
try {
// Random short code seçimi
const shortCode = config.shortCodes[Math.floor(Math.random() * config.shortCodes.length)];
const url = `${config.baseUrl}/${shortCode}`;
const response = await fetch(url, {
method: 'GET',
redirect: 'manual', // Redirect'leri takip etme - sadece timing istiyoruz
});
const responseTime = performance.now() - requestStart;
results.push({
success: response.status >= 200 && response.status < 400,
responseTime,
timestamp: performance.now(),
});
} catch (error) {
const responseTime = performance.now() - requestStart;
results.push({
success: false,
responseTime,
timestamp: performance.now(),
error: error instanceof Error ? error.message : String(error),
});
}
// Bir sonraki request'den önce bekle (istenen yük için ayarla)
await sleep(100 + Math.random() * 200); // Kullanıcı başına request'ler arası 100-300ms
}
});
// Tüm kullanıcıların tamamlanmasını bekle
await Promise.all(userPromises);
// İstatistikleri hesapla
const successfulResults = results.filter(r => r.success);
const responseTimes = successfulResults.map(r => r.responseTime);
responseTimes.sort((a, b) => a - b);
const totalDurationSec = (performance.now() - startTime) / 1000;
return {
totalRequests: results.length,
successfulRequests: successfulResults.length,
failedRequests: results.length - successfulResults.length,
averageResponseTime: responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length,
p50ResponseTime: responseTimes[Math.floor(responseTimes.length * 0.5)],
p95ResponseTime: responseTimes[Math.floor(responseTimes.length * 0.95)],
p99ResponseTime: responseTimes[Math.floor(responseTimes.length * 0.99)],
errorsPerSecond: (results.length - successfulResults.length) / totalDurationSec,
requestsPerSecond: results.length / totalDurationSec,
};
}
async function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Örnek kullanım - her deployment'tan önce bunu çalıştır
async function validatePerformance() {
console.log('Pre-deployment load test çalıştırılıyor...');
const testConfig: LoadTestConfig = {
baseUrl: 'https://staging-links.yourcompany.com',
concurrentUsers: 50,
testDurationMs: 60 * 1000, // 1 dakika
rampUpMs: 10 * 1000, // 10 saniye ramp-up
shortCodes: ['test1', 'test2', 'test3', 'popular-link', 'campaign-2024'],
};
const results = await runLoadTest(testConfig);
// Performans assertion'ları
const maxAcceptableP95 = 500; // 500ms P95 response time
const maxAcceptableErrorRate = 0.01; // %1 error rate
if (results.p95ResponseTime > maxAcceptableP95) {
throw new Error(`P95 response time çok yüksek: ${results.p95ResponseTime}ms > ${maxAcceptableP95}ms`);
}
const errorRate = results.failedRequests / results.totalRequests;
if (errorRate > maxAcceptableErrorRate) {
throw new Error(`Error rate çok yüksek: ${(errorRate * 100).toFixed(2)}% > ${(maxAcceptableErrorRate * 100)}%`);
}
console.log('Load test geçti:', results);
}
Blue-Green Deployment’lar: Korkmadan Deploy Et
Bizi huzurla uyutturan deployment stratejisi:
// deployment/blue-green-deploy.ts
import * as aws from '@aws-sdk/client-route53';
import * as lambda from '@aws-sdk/client-lambda';
interface DeploymentConfig {
stage: 'blue' | 'green';
domainName: string;
hostedZoneId: string;
healthCheckUrl: string;
}
export class BlueGreenDeployment {
private route53 = new aws.Route53Client({});
private lambdaClient = new lambda.LambdaClient({});
async deployNewVersion(config: DeploymentConfig): Promise<void> {
console.log(`${config.stage} deployment başlatılıyor...`);
// Adım 1: Yeni infrastructure'ı deploy et
await this.deployCDKStack(config.stage);
// Adım 2: Yeni environment'ı warm up yap
await this.warmUpEnvironment(config);
// Adım 3: Health check'leri çalıştır
await this.runHealthChecks(config.healthCheckUrl);
// Adım 4: Trafiği kademeli olarak shift et
await this.shiftTraffic(config, [10, 25, 50, 100]);
console.log(`${config.stage} deployment başarıyla tamamlandı`);
}
private async deployCDKStack(stage: string): Promise<void> {
// Bu tipik olarak CDK CLI veya AWS SDK kullanır
console.log(`${stage} için CDK stack deploy ediliyor...`);
// Örnek: CDK deploy komutunu exec et
const { spawn } = await import('child_process');
return new Promise((resolve, reject) => {
const deploy = spawn('npx', ['cdk', 'deploy', '--all', '--context', `stage=${stage}`], {
stdio: 'inherit',
});
deploy.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`CDK deploy ${code} kodu ile başarısız oldu`));
}
});
});
}
private async warmUpEnvironment(config: DeploymentConfig): Promise<void> {
console.log('Lambda function'ları warm up yapılıyor...');
// Bu stage için tüm Lambda function'ları al
const functions = await this.lambdaClient.send(new lambda.ListFunctionsCommand({
Marker: undefined,
MaxItems: 100,
}));
const stageFunctions = functions.Functions?.filter(fn =>
fn.FunctionName?.includes(config.stage)
) || [];
// Her function'ı warm up yap
const warmUpPromises = stageFunctions.map(async (fn) => {
if (!fn.FunctionName) return;
try {
await this.lambdaClient.send(new lambda.InvokeCommand({
FunctionName: fn.FunctionName,
Payload: JSON.stringify({
source: 'warm-up',
warmUp: true,
}),
}));
console.log(`${fn.FunctionName} warm up edildi`);
} catch (error) {
console.warn(`[WARN] ${fn.FunctionName} warm up başarısız:`, error);
}
});
await Promise.all(warmUpPromises);
}
private async runHealthChecks(healthCheckUrl: string): Promise<void> {
console.log('Health check'ler çalıştırılıyor...');
const checks = [
{ name: 'Basic redirect', path: '/test-redirect' },
{ name: 'API health', path: '/api/health' },
{ name: '404 handling', path: '/non-existent-link' },
];
for (const check of checks) {
const url = `${healthCheckUrl}${check.path}`;
const response = await fetch(url);
// Farklı endpoint'ler için farklı beklentiler
const expectedStatus = check.path === '/non-existent-link' ? 404 : 200;
if (response.status !== expectedStatus) {
throw new Error(`${check.name} için health check başarısız: ${response.status}`);
}
console.log(`${check.name} health check geçti`);
}
}
private async shiftTraffic(
config: DeploymentConfig,
trafficPercentages: number[]
): Promise<void> {
for (const percentage of trafficPercentages) {
console.log(`${config.stage}'e %${percentage} trafik shift ediliyor...`);
// Route53 weighted routing'i update et
await this.updateRoute53WeightedRecord(config, percentage);
// DNS propagation ve monitoring için bekle
await this.sleep(120000); // 2 dakika
// Trafik shift sırasında error rate'leri kontrol et
await this.monitorErrorRates(config);
console.log(`%${percentage} trafik başarıyla shift edildi`);
}
}
private async updateRoute53WeightedRecord(
config: DeploymentConfig,
weight: number
): Promise<void> {
const oppositeWeight = 100 - weight;
const oppositeStage = config.stage === 'blue' ? 'green' : 'blue';
// Mevcut stage weight'ini update et
await this.route53.send(new aws.ChangeResourceRecordSetsCommand({
HostedZoneId: config.hostedZoneId,
ChangeBatch: {
Changes: [{
Action: 'UPSERT',
ResourceRecordSet: {
Name: config.domainName,
Type: 'CNAME',
SetIdentifier: config.stage,
Weight: weight,
TTL: 60, // Hızlı değişiklikler için kısa TTL
ResourceRecords: [{
Value: `${config.stage}-api.example.com`
}],
},
}],
},
}));
// Karşıt stage weight'ini update et
await this.route53.send(new aws.ChangeResourceRecordSetsCommand({
HostedZoneId: config.hostedZoneId,
ChangeBatch: {
Changes: [{
Action: 'UPSERT',
ResourceRecordSet: {
Name: config.domainName,
Type: 'CNAME',
SetIdentifier: oppositeStage,
Weight: oppositeWeight,
TTL: 60,
ResourceRecords: [{
Value: `${oppositeStage}-api.example.com`
}],
},
}],
},
}));
}
private async monitorErrorRates(config: DeploymentConfig): Promise<void> {
// Bu CloudWatch ile integrate olup error rate'leri kontrol eder
// ve eşiği aşarsa otomatik rollback yapar
console.log('Error rate'ler monitor ediliyor...');
// Örnek: CloudWatch metric'lerini kontrol et
// Eğer error rate > %1 ise rollback
// Eğer response time P95 > 500ms ise rollback
await this.sleep(30000); // 30 saniye monitor et
}
async rollback(config: DeploymentConfig): Promise<void> {
console.log(`${config.stage} deployment rollback yapılıyor...`);
// Tüm trafiği stable versiyona geri kaydır
const stableStage = config.stage === 'blue' ? 'green' : 'blue';
await this.updateRoute53WeightedRecord({
...config,
stage: stableStage,
}, 100);
console.log('Rollback tamamlandı');
}
private async sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Production Optimizasyon Değerlendirmeleri
Production infrastructure işletmek ölçeklendirme ve maliyet yönetimi hakkında önemli pattern’lar ortaya çıkarır:
1. Conservative provisioning ile agresif monitoring Minimal kapasite ile başlayıp auto-scaling’e güven. Over-provisioning çoğu workload için güvenilirliği artırmadan maliyetleri artırır.
2. Cold start’un kullanıcı deneyimindeki etkisi 2-3 saniyelik cold start latency bile redirect performansını önemli ölçüde bozar. Kritik yollar için provisioned concurrency genellikle ek maliyeti haklar.
3. DynamoDB auto-scaling zamanlamaları Auto-scaling kapasite artışı için 5-10 dakika alır ama hızla aşağı ölçeklenir. Target utilization’u %90 yerine %70’te ayarlamak trafik spike’ları için tampon sağlar.
4. Teknik metric’ler üzerinde business metric’ler “Kampanya başına redirect’ler” ve “conversion oluşturan linkler” tracking’i ham “Lambda invocation’lardan” daha eyleme geçirilebilir görüşler sağlar. Business konteksti optimizasyon çabalarını öncelliklendirmede yardımcı olur.
5. Staging load testing etkinliği Kapsamlı load testing çoğu production sorununu yakalar, ama gerçek kullanıcı pattern’ları genellikle sentetik testlerden farklıdır. Teorik peak load’lar yerine gerçek trafik pattern’larını simüle etmeye odaklan.
Önemli Production Metric’ler
Her sabah gerçekten baktığımız dashboard’lar:
// lib/production-dashboard.ts
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
export class ProductionDashboard extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
new cloudwatch.Dashboard(this, 'LinkShortenerProduction', {
dashboardName: 'LinkShortener-Production-Health',
widgets: [
// Satır 1: Business metric'ler
[
new cloudwatch.SingleValueWidget({
title: 'Redirects (24sa)',
metrics: [
new cloudwatch.Metric({
namespace: 'LinkShortener/Production',
metricName: 'RedirectCount',
statistic: 'Sum',
period: cdk.Duration.hours(24),
}),
],
width: 6,
}),
new cloudwatch.SingleValueWidget({
title: 'Success Rate (24sa)',
metrics: [
new cloudwatch.MathExpression({
expression: '(successful / total) * 100',
usingMetrics: {
successful: new cloudwatch.Metric({
namespace: 'LinkShortener/Production',
metricName: 'RedirectCount',
dimensionsMap: { Status: 'Success' },
statistic: 'Sum',
}),
total: new cloudwatch.Metric({
namespace: 'LinkShortener/Production',
metricName: 'RedirectCount',
statistic: 'Sum',
}),
},
}),
],
width: 6,
}),
],
// Satır 2: Performans metric'leri
[
new cloudwatch.GraphWidget({
title: 'Response Time Percentile'ları',
left: [
new cloudwatch.Metric({
namespace: 'LinkShortener/Production',
metricName: 'RedirectResponseTime',
statistic: 'p50',
period: cdk.Duration.minutes(5),
label: 'P50',
}),
new cloudwatch.Metric({
namespace: 'LinkShortener/Production',
metricName: 'RedirectResponseTime',
statistic: 'p95',
period: cdk.Duration.minutes(5),
label: 'P95',
}),
new cloudwatch.Metric({
namespace: 'LinkShortener/Production',
metricName: 'RedirectResponseTime',
statistic: 'p99',
period: cdk.Duration.minutes(5),
label: 'P99',
}),
],
width: 12,
}),
],
// Satır 3: Infrastructure sağlığı
[
new cloudwatch.GraphWidget({
title: 'DynamoDB Throttling',
left: [
new cloudwatch.Metric({
namespace: 'AWS/DynamoDB',
metricName: 'ReadThrottledRequests',
dimensionsMap: { TableName: 'LinksTable' },
statistic: 'Sum',
}),
new cloudwatch.Metric({
namespace: 'AWS/DynamoDB',
metricName: 'WriteThrottledRequests',
dimensionsMap: { TableName: 'LinksTable' },
statistic: 'Sum',
}),
],
width: 6,
}),
new cloudwatch.GraphWidget({
title: 'Lambda Cold Start'ları',
left: [
new cloudwatch.Metric({
namespace: 'LinkShortener/Production',
metricName: 'RedirectCount',
dimensionsMap: { ColdStart: 'true' },
statistic: 'Sum',
period: cdk.Duration.minutes(5),
}),
],
width: 6,
}),
],
],
});
}
}
Sırada Ne Var?
Bölüm 5’te son cepheyle uğraşacağız: günde milyonlarca redirect’i handle etmek için ölçeklendirme, ölçekte maliyet optimizasyonu, ve küçük bir takımın yüksek trafik servisi yönetmesini sağlayan operasyonel uygulamalar.
Multi-region deployment’lar, database sharding stratejileri, ve kullanıcıların problemleri fark etmesinden önce seni uyaran monitoring kurulumu gibi ileri seviye konuları kapsayacağız.
Burada inşa ettiğimiz infrastructure oldukça iyi ölçekleniyor, ama binlerce request’i handle eden bir servisle milyonlarını handle eden arasındaki farkı yaratan belirli pattern’lar ve uygulamalar var. Şimdi bu boşluğu dolduralım.
AWS CDK Link Kısaltıcı: Sıfırdan Production'a
AWS CDK, Node.js Lambda ve DynamoDB ile production-grade bir link kısaltma servisi kurulumu hakkında 5 bölümlük kapsamlı seri. Gerçek production hikayeleri, performans optimizasyonu ve maliyet yönetimi dahil.
Serideki tüm yazılar
İlgili yazılar
Global uygulamalar için AWS edge computing çözümlerini seçme ve uygulama üzerine pratik örnekler ve maliyet optimizasyonu stratejileri içeren kapsamlı teknik rehber.
Single Table Design uygulamalarında DynamoDB throttling'i önleme ve yönetme stratejileri. Partition key tasarımı, write sharding, kapasite modları, DAX caching, retry pattern'leri ve yüksek throughput sistemler için CloudWatch monitoring konularını kapsar.
Native AWS servisleri, otomasyon ve kanıtlanmış implementation pattern'leri kullanarak AWS maliyetlerini %40-70 azaltmaya yönelik kapsamlı bir rehber.
LangChain uygulamalarını production'a taşırken öğrendiklerim. Başarısızlığa yol açan anti-patternler, başarıyı sağlayan patternler, çalışan kod örnekleri ve maliyet optimizasyon stratejileri.
Sistematik veritabanı profiling ve optimizasyonu ile yıllık altyapı maliyetlerini 100K dolar azalttığımız hikaye. PostgreSQL ve MongoDB performance deneyimleri.