AWS CDK Link Kısaltıcı Bölüm 2: Temel Fonksiyonlar & API Geliştirme
Yönlendirme motoru, analytics toplama ve API Gateway yapılandırması: günlük milyonlarca yönlendirme için performans optimizasyonları ve debugging stratejileri.
Bir link kısaltıcı aslında bir redirect motorudur. Kısa kod araması ve HTTP 301 yanıtı, kritik gecikme bütçesindeki tek işlemlerdir; her ikisinin de yüksek eşzamanlılıkta bile kullanıcının anlık algıladığı eşiğin (yaklaşık 200ms) altında kalması gerekir. Bu sıcak yolun etrafındaki iş mantığı (analitik, hız sınırlama, link süresinin dolması, özel slug’lar) redirect’i engellememelidir; redirect handler’a eklenen her özelliğin bedeli doğrudan gecikme olarak ödenir.
Bu serinin Bölüm 1’i temeli kurdu (DynamoDB tablosu, API Gateway, temel Lambda). Sıra çekirdek işlevsellikte: DynamoDB cache’li redirect Lambda’sı, kısa kod oluşturma ve yönetme API’si, yan kanal üzerinden analitik olay yayını ve upstream servisler bozulduğunda redirect’i hızlı tutan hata yönetimi desenleri.
Redirect Motoru: Sıcak Yol#
Kullanıcı tarafında ürünün tamamı redirect handler’dır: tek arama, tek yanıt. Aşağıdaki implementasyon handler’ı tek bir DynamoDB okumasında tutar, geri kalan her şeyi yan kanala devreder:
// lambda/redirect.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';
import { unmarshall } from '@aws-sdk/util-dynamodb';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import { trackAnalytics } from './analytics';
const dynamodb = new DynamoDBClient({
region: process.env.AWS_REGION,
// Handler dışında oluşturulur: sıcak çağrılar aynı soketi yeniden kullanır
maxAttempts: 3,
requestHandler: new NodeHttpHandler({
connectionTimeout: 1000,
requestTimeout: 2000,
})
});
export interface AnalyticsEvent {
shortCode: string;
timestamp: number;
userAgent?: string;
referer?: string;
ip?: string;
country?: string;
}
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
const startTime = Date.now();
const shortCode = event.pathParameters?.shortCode;
if (!shortCode) {
return createErrorResponse(400, 'Short code gerekli');
}
try {
// DynamoDB'den URL'i al
const result = await dynamodb.send(new GetItemCommand({
TableName: process.env.LINKS_TABLE_NAME!,
Key: { shortCode: { S: shortCode } },
ProjectionExpression: 'originalUrl, expiresAt, clickCount',
}));
if (!result.Item) {
// Analytics için 404'leri takip et
await trackAnalytics({
shortCode,
timestamp: Date.now(),
userAgent: event.headers['User-Agent'],
referer: event.headers['Referer'],
ip: event.requestContext.identity?.sourceIp,
}, 'NOT_FOUND');
return createErrorResponse(404, 'Link bulunamadı');
}
const item = unmarshall(result.Item);
// Expiration kontrolü
if (item.expiresAt && Date.now() > item.expiresAt) {
return createErrorResponse(410, 'Link süresi dolmuş');
}
// Analytics'i asenkron takip et (redirect'i bloklamasın)
trackAnalytics({
shortCode,
timestamp: Date.now(),
userAgent: event.headers['User-Agent'],
referer: event.headers['Referer'],
ip: event.requestContext.identity?.sourceIp,
}, 'SUCCESS').catch(error => {
console.error('Analytics tracking başarısız:', error);
// Analytics başarısız olursa redirect'i başarısız etme
});
// CloudWatch Insights'ın responseTime üzerinden toplayabilmesi için yapısal log
const responseTime = Date.now() - startTime;
console.log(JSON.stringify({ event: 'RedirectProcessed', shortCode, responseTime }));
return {
statusCode: 301,
headers: {
Location: item.originalUrl,
'Cache-Control': 'public, max-age=300', // 5 dakika
'X-Response-Time': `${responseTime}ms`,
},
body: '',
};
} catch (error) {
console.error(JSON.stringify({
event: 'RedirectError',
shortCode,
message: error instanceof Error ? error.message : String(error),
}));
return createErrorResponse(500, 'Internal server hatası');
}
};
function createErrorResponse(statusCode: number, message: string): APIGatewayProxyResult {
return {
statusCode,
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' },
body: `<!DOCTYPE html><html><head><title>Link Error</title></head><body><h1>${statusCode === 404 ? 'Link Bulunamadı' : 'Hata'}</h1><p>${message}</p></body></html>`,
};
}
Analytics: Business Intelligence Katmanı#
Bir kısaltıcıyı redirect’in ötesinde kullanışlı kılan şey analytics’tir: hangi kodlar trafik alıyor, tıklar nereden geliyor ve ne zaman geliyor. Toplama ayrı bir tabloya yazar, böylece redirect onu beklemez:
// lambda/analytics.ts
import { DynamoDBClient, PutItemCommand, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall } from '@aws-sdk/util-dynamodb';
import crypto from 'crypto';
import type { AnalyticsEvent } from './redirect';
const dynamodb = new DynamoDBClient({ region: process.env.AWS_REGION });
function hashIP(ip: string): string {
return crypto.createHash('sha256').update(ip + process.env.IP_SALT).digest('hex').substring(0, 16);
}
async function getCountryFromIP(ip?: string): Promise<string> {
if (!ip) return 'unknown';
// Placeholder: buraya bir geolocation araması (MaxMind veya CDN'inizin eklediği
// ülke header'ı) koyun ve hatalarını burada ele alın.
return 'US';
}
export async function trackAnalytics(
event: AnalyticsEvent,
eventType: 'SUCCESS' | 'NOT_FOUND' = 'SUCCESS'
): Promise<void> {
const timestamp = Date.now();
const analyticsItem = {
shortCode: event.shortCode,
timestamp,
eventType,
userAgent: event.userAgent || 'unknown',
referer: event.referer || 'direct',
ip: hashIP(event.ip || ''), // Privacy-first yaklaşım
country: await getCountryFromIP(event.ip),
// Efficient query'ler için saatlik partition
hourPartition: `${event.shortCode}#${Math.floor(timestamp / (1000 * 60 * 60))}`,
};
// Analytics tablosunda sakla
await dynamodb.send(new PutItemCommand({
TableName: process.env.ANALYTICS_TABLE_NAME!,
Item: marshall(analyticsItem),
}));
// Ana kayıttaki tık sayısını güncelle (sadece başarılı tıklar için)
if (eventType === 'SUCCESS') {
await dynamodb.send(new UpdateItemCommand({
TableName: process.env.LINKS_TABLE_NAME!,
Key: { shortCode: { S: event.shortCode } },
UpdateExpression: 'ADD clickCount :inc SET lastClickAt = :timestamp',
ExpressionAttributeValues: {
':inc': { N: '1' },
':timestamp': { N: timestamp.toString() },
},
}));
}
}
API Gateway: Ön Kapı#
CDK tanımı üç route’u üç handler’a bağlar ve onları koruyan stage seviyesindeki throttle değerlerini ayarlar:
// lib/api-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
interface ApiStackProps extends cdk.StackProps {
redirectHandler: lambda.IFunction;
createHandler: lambda.IFunction;
analyticsHandler: lambda.IFunction;
}
export class ApiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: ApiStackProps) {
super(scope, id, props);
// Redirect ve yönetim route'ları için REST API
const api = new apigateway.RestApi(this, 'LinkShortenerApi', {
restApiName: 'Link Shortener Servisi',
description: 'Production link kısaltıcı API',
// 1 KB üzerindeki yanıtları sıkıştır. binaryMediaTypes'ı boş bırakın:
// '*/*' değeri oluşturma API'sine gelen JSON body'leri base64'e çevirir.
minimumCompressionSize: 1024,
// CORS konfigürasyonu
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: [
'Content-Type',
'X-Amz-Date',
'Authorization',
'X-Api-Key',
'X-Amz-Security-Token',
],
maxAge: cdk.Duration.hours(1),
},
// Stage seviyesinde metrik ve throttling
deployOptions: {
metricsEnabled: true,
loggingLevel: apigateway.MethodLoggingLevel.INFO,
dataTraceEnabled: false, // Prod'da kapalı: tüm request payload'ını loglar
throttlingBurstLimit: 2000,
throttlingRateLimit: 1000,
},
});
// Validator var olan bir API'ye bağlıdır, bu yüzden ondan sonra oluşturulur
const requestValidator = api.addRequestValidator('RequestValidator', {
validateRequestBody: true,
validateRequestParameters: true,
});
// Redirect route ekle: GET /{shortCode}
const redirectIntegration = new apigateway.LambdaIntegration(props.redirectHandler, {
proxy: true,
allowTestInvoke: false, // Performans için test invoke'u devre dışı bırak
});
api.root.addResource('{shortCode}').addMethod('GET', redirectIntegration, {
requestParameters: { 'method.request.path.shortCode': true },
});
// Link oluşturma API: POST /api/shorten
const apiResource = api.root.addResource('api');
const shortenResource = apiResource.addResource('shorten');
const createIntegration = new apigateway.LambdaIntegration(props.createHandler, {
proxy: true,
});
shortenResource.addMethod('POST', createIntegration, {
requestModels: {
'application/json': this.createRequestModel(api),
},
requestValidator,
});
// Analytics API: GET /api/analytics/{shortCode}
const analyticsResource = apiResource.addResource('analytics');
const analyticsCodeResource = analyticsResource.addResource('{shortCode}');
analyticsCodeResource.addMethod('GET', new apigateway.LambdaIntegration(props.analyticsHandler));
}
private createRequestModel(api: apigateway.RestApi): apigateway.Model {
return new apigateway.Model(this, 'ShortenRequestModel', {
restApi: api,
contentType: 'application/json',
schema: {
type: apigateway.JsonSchemaType.OBJECT,
properties: {
url: {
type: apigateway.JsonSchemaType.STRING,
pattern: '^https?://.+',
minLength: 10,
maxLength: 2048,
},
customCode: {
type: apigateway.JsonSchemaType.STRING,
pattern: '^[a-zA-Z0-9-_]{3,20}$',
},
expiresIn: {
type: apigateway.JsonSchemaType.NUMBER,
minimum: 3600, // 1 saat minimum
maximum: 31536000, // 1 yıl maksimum
},
},
required: ['url'],
additionalProperties: false,
},
});
}
}
Performansı Belirleyen Desenler#
Yukarıdaki koddaki üç karar, redirect gecikme bütçesinin büyük kısmını taşır.
1. Çağrılar Arası Bağlantı Yeniden Kullanımı#
DynamoDB client’ı handler’ın dışında oluşturulur; böylece aynı örnek ve HTTP agent’ı, sıcak execution environment’ın çağrıları arasında yaşamaya devam eder. Yeni bir environment’tan gelen ilk istek yine DNS çözümlemesi ve TLS handshake bedelini öder, sonraki her istek açık bağlantıyı yeniden kullanır. Client’ı handler’ın içine taşımak bu kazanımı çöpe atar ve her redirect’e bir handshake ekler.
2. Analytics’i Kritik Yolun Dışına Almak#
Redirect, DynamoDB yanıt verir vermez döner; analytics yazımı arkada çalışmaya bırakılır. Fire-and-forget deseninin gizlediği bir kural var: Lambda, yanıt yazıldıktan sonra execution environment’ı dondurur. Yani await etmediğiniz bir promise, o environment tekrar çağrılana kadar askıda kalabilir; environment önce geri alınırsa tamamen kaybolur. Arada bir tık kaybetmek kabul edilebilir değilse ya yazımı await edin ya da olayı bir SQS kuyruğuna bırakıp ayrı bir consumer’a kaydettirin.
3. Projection’lar ve Okuma Maliyeti#
ProjectionExpression, yanıtı redirect’in ihtiyaç duyduğu üç attribute ile sınırlar: originalUrl, expiresAt, clickCount. Bu, hat üzerindeki payload’ı küçültür; okuma kapasitesini değil. DynamoDB tüketilen okuma kapasitesini projection uygulanmadan önceki tam item boyutundan hesaplar, dolayısıyla geniş item’ları okumak pahalı kalmaya devam eder. Analytics sorguları bu okumayı genişletmek yerine ayrı bir GSI kullanır.
Production Hata Ayıklama#
CloudWatch Insights Sorguları#
Aşağıdaki iki sorgu, handler’ın logladığı JSON alanlarını okur; Insights yapısal log satırlarında bu alanları kendiliğinden keşfeder.
fields @timestamp, responseTime
| filter event = "RedirectProcessed"
| stats avg(responseTime) as avgMs, pct(responseTime, 95) as p95Ms by bin(5m)
fields @timestamp, shortCode
| filter event = "RedirectError"
| stats count(*) as errorCount by shortCode
| sort errorCount desc
| limit 20
Lambda Performans İzleme#
// lambda/redirect.ts, modül seviyesi: her execution environment'ta bir kez ayarlanır
let isColdStart = true;
// ...handler içinde, 301 dönmeden hemen önce:
console.log(JSON.stringify({
event: 'RedirectProcessed',
coldStart: isColdStart,
responseTime: Date.now() - startTime,
shortCode,
}));
isColdStart = false;
Redirect Motoru Testleri#
Handler doğrudan DynamoDB ile konuştuğu için bu testler aws-sdk-client-mock’a ya da abc123 ve expired kayıtlarıyla doldurulmuş yerel bir tabloya ihtiyaç duyar:
// tests/redirect.test.ts
import { APIGatewayProxyEvent } from 'aws-lambda';
import { handler } from '../lambda/redirect';
function createAPIGatewayEvent(shortCode: string): APIGatewayProxyEvent {
return {
pathParameters: { shortCode },
headers: {},
requestContext: { identity: {} },
} as unknown as APIGatewayProxyEvent;
}
describe('Redirect Handler', () => {
beforeEach(() => {
process.env.LINKS_TABLE_NAME = 'test-links';
process.env.ANALYTICS_TABLE_NAME = 'test-analytics';
});
test('should redirect to original URL', async () => {
const result = await handler(createAPIGatewayEvent('abc123'));
expect(result.statusCode).toBe(301);
expect(result.headers?.Location).toBe('https://example.com');
expect(result.headers?.['Cache-Control']).toBe('public, max-age=300');
});
test('should handle expired links gracefully', async () => {
const result = await handler(createAPIGatewayEvent('expired'));
expect(result.statusCode).toBe(410);
expect(result.body).toContain('expired');
});
});
Sonraki Adımlar#
Yukarıdaki tek okumalı redirect, sıcak yol partition key üzerinde bir GetItem olarak kaldığı sürece geçerlidir. Orijinal URL’e göre arama, kullanıcı başına link listesi veya redirect üzerinde ikinci bir tablo okuması gecikme bütçesini bozar; bunların yeri oluşturma API’sinin arkası ya da yalnızca analytics’in sorguladığı bir GSI’dır.
Bölüm 3, servisi spam vektörü olmaktan koruyan katmanları ekler: input validation, rate limiting, WAF kuralları ve sertifikalarıyla birlikte custom domain kurulumu.
Kaynaklar#
- Tutorial: Create a CRUD HTTP API with Lambda and DynamoDB (yeni sekmede açılır) - DynamoDB sorgulamasıyla API Gateway ve Lambda yönlendirme işleyicisi kurulumunu anlatan resmi kılavuz.
- Best practices for querying and scanning data in DynamoDB (yeni sekmede açılır) - Verimli kısa kod sorguları için Scan yerine Query kullanımına ve tam tablo okumalarından kaçınmaya ilişkin DynamoDB rehberi.
- Best practices for using secondary indexes in DynamoDB (yeni sekmede açılır) - Birincil yönlendirme yolu yanı sıra analitik erişim desenlerini desteklemek için Global Secondary Index kullanımı.
- Configuring provisioned concurrency for a function (yeni sekmede açılır) - Kritik yönlendirme yolundaki soğuk başlatmaları ortadan kaldırmak için Lambda provisioned concurrency resmi belgesi.
- Visualize Lambda function invocations using AWS X-Ray (yeni sekmede açılır) - Yönlendirme gecikmesini ölçmek ve darboğazları belirlemek için Lambda X-Ray izleme araçları.
- Using CloudWatch metrics with Lambda (yeni sekmede açılır) - Kullanılabilir Lambda metrikleri ve hata oranları ile yönlendirme gecikme eşikleri için alarm oluşturma.
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
AWS CDK, DynamoDB ve Lambda ile production-grade link kısaltıcı kurulumu. Mimari kararlar, proje yapısı ve ölçekte ayakta kalan şema seçimleri.
Custom domain, toplu işlem ve URL expiration ile production link shortener servisleri için defense-in-depth güvenlik önlemlerinin implementasyonu.
Bir Lambda filosu Middy'nin statik middleware modelini ne zaman aşar, projeye özel bir motor istek başına konfigürasyonu nasıl çözer, bakımı neye mal olur
Lambda API'si ile OpenAPI sözleşmesini senkron tutmak için spec'i Zod şemalarından üretin ve bu üretimi CDK dağıtımına bağlayın.
İç servis katmanı kurmadan önce kurup kurmayacağınıza karar verin. Katmanın çağrı başına maliyeti, VPC Lattice'in kazandığı hacim ve direct invoke'un hâlâ kazandığı an.