AWS CDK Link Kısaltıcı Bölüm 1: Proje Kurulumu & Temel Altyapı
AWS CDK, DynamoDB ve Lambda ile production-grade link kısaltıcı kurulumu. Gerçek mimari kararlar, ilk kurulum ve büyük ölçekte URL kısaltıcıları inşa etmenin dersleri.
Seri Navigasyonu
Bu, production-grade link kısaltıcı oluşturma üzerine 5 bölümlük bir serinin 1. Bölümü:
- Bölüm 1: Proje Kurulumu & Temel Altyapı (Buradasınız)
- Bölüm 2: Temel İşlevsellik & API Geliştirme
- Bölüm 3: Gelişmiş Özellikler & Güvenlik
- Bölüm 4: Production Deployment & Optimizasyon
- Bölüm 5: Ölçeklendirme & Bakım
Giriş: Gerçek Dünya Ölçeği
AWS üzerinde link kısaltıcı yapmak, yönlendirme gecikmesi, URL doğrulama ve link bazlı analitik ölçekte zorunlu hale gelene kadar kolay görünür; ancak bu noktada SaaS çözümleri maliyet açısından sürdürülemez hale gelir. Aylık milyonlarca yönlendirmede her mimari karar (depolama motoru, caching katmanı, CDN konumlandırması) hem maliyeti hem de kuyruk gecikmesini doğrudan etkiler. Bu yazı, production-grade bir shortener’ın temelini oluşturan CDK proje kurulumunu, DynamoDB şemasını ve Lambda bağlantısını ele alıyor.
Link kısaltıcılar production’a çıkana kadar basit görünür. Sonra eğlenceli edge case’ler başlar: yönlendirme döngüleri, kötü niyetli URL’ler, büyük ölçekte analitik ve klasik hata modu; birinin büyük bir kampanya lansmanı sırasında yanlışlıkla ilk kısa linke dönen bir kısa link oluşturması. Bu tür senaryolar rate limiting ve URL doğrulama olmadan hızla sorun yaratır.
Bu yazı, AWS CDK ile tatilinizdeyken sizi uyandırmayacak production-grade bir link kısaltıcı yapmayı gösteriyor. DynamoDB single-table design, Lambda optimizasyonu ve CloudFront caching dahil gerçek mimari kararları kapsıyor; tümü yüksek trafik ortamlarında test edilmiş. Bölüm 1 altyapı kurulumuna, Bölüm 2 redirect motoruna odaklanıyor.
Yük Altında Tutarlı Kalan Mimari
Kod yazmadan önce bir hafta boyunca mimari çizmek yatırıma değer. Production yükü altında dayanıklılığını kanıtlamış bir mimari aşağıda:
Bu mimari saniyede 2.000 isteği terlemeden hallediyor. Önemli kararlar:
- CloudFront ile önbellekleme - Aynı yönlendirme için Lambda’yı neden 10.000 kez çalıştırasın?
- RDS yerine DynamoDB - Büyük ölçekte tahmin edilebilir performans, connection pooling baş ağrısı yok
- Ayrı Lambda fonksiyonları - İşler ters gittiğinde ölçekleme ve debug etmesi daha kolay
- Sıcak yollar için DAX - Çünkü o viral link veritabanınızı döver
CDK Proje Kurulumu
İlk ders: sadece cdk init çalıştırmayın. Beş dakika ayırıp proje yapınızı düzgün kurun. 2x ölçekte her şeyi refactor etmediğinizde kendinize teşekkür edeceksiniz. Ayrı stack’ler ve construct’lar ile modüler yapı, ileride environment-specific konfigürasyonlar için hayat kurtarır.
# TypeScript ile projeyi baştan oluştur
mkdir link-shortener && cd link-shortener
npx cdk init app --language typescript
# Gerçekten ihtiyacımız olan bağımlılıkları yükle
npm install @aws-cdk/aws-lambda-nodejs @aws-cdk/aws-dynamodb \
@aws-cdk/aws-apigatewayv2 @aws-cdk/aws-apigatewayv2-integrations \
@aws-cdk/aws-cloudfront @aws-cdk/aws-cloudfront-origins
# Akıl sağlığı için dev bağımlılıkları
npm install -D @types/aws-lambda esbuild prettier eslint \
@typescript-eslint/parser @typescript-eslint/eslint-plugin
Proje yapınız şöyle görünmeli:
link-shortener/
├── bin/
│ └── link-shortener.ts # CDK app giriş noktası
├── lib/
│ ├── stacks/
│ │ ├── api-stack.ts # API Gateway + Lambda
│ │ ├── database-stack.ts # DynamoDB tabloları
│ │ └── cdn-stack.ts # CloudFront dağıtımı
│ └── constructs/
│ ├── link-table.ts # DynamoDB construct
│ └── lambda-function.ts # Yeniden kullanılabilir Lambda construct
├── src/
│ ├── handlers/
│ │ ├── create.ts # Kısa link oluştur
│ │ ├── redirect.ts # Yönlendirmeleri yönet
│ │ └── analytics.ts # Tıklamaları takip et
│ └── utils/
│ ├── id-generator.ts # Kısa ID üretimi
│ └── url-validator.ts # URL doğrulama
├── test/
└── cdk.json
DynamoDB Şema Tasarımı
Çoğu tutorial id ve url içeren basit bir tablo gösterir. Bu production’da yaşamaz. Birkaç veritabanı migration’ından sonra gerçekten çalışan şema şudur:
// lib/constructs/link-table.ts
import { Table, AttributeType, BillingMode, StreamViewType } from 'aws-cdk-lib/aws-dynamodb';
import { RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class LinkTable extends Construct {
public readonly table: Table;
constructor(scope: Construct, id: string) {
super(scope, id);
this.table = new Table(this, 'LinksTable', {
partitionKey: {
name: 'PK',
type: AttributeType.STRING,
},
sortKey: {
name: 'SK',
type: AttributeType.STRING,
},
billingMode: BillingMode.PAY_PER_REQUEST, // Buradan başla, pattern'lerini öğrenince provisioned'a geç
pointInTimeRecovery: true, // Çünkü birisi önemli bir şeyi silecek
stream: StreamViewType.NEW_AND_OLD_IMAGES, // Analitik ve debugging için
removalPolicy: RemovalPolicy.RETAIN, // Production verisini asla yanlışlıkla silme
});
// Orijinal URL ile arama için GSI (tekilleştirme)
this.table.addGlobalSecondaryIndex({
indexName: 'GSI1',
partitionKey: {
name: 'GSI1PK',
type: AttributeType.STRING,
},
sortKey: {
name: 'GSI1SK',
type: AttributeType.STRING,
},
});
// Analitik sorguları için GSI
this.table.addGlobalSecondaryIndex({
indexName: 'GSI2',
partitionKey: {
name: 'GSI2PK',
type: AttributeType.STRING,
},
sortKey: {
name: 'CreatedAt',
type: AttributeType.NUMBER,
},
});
}
}
Neden bu şema? Gerçek veriyle açıklanalım:
// Tablodaki örnek kayıtlar
const linkRecord = {
PK: 'LINK#abc123', // Kısa kod
SK: 'METADATA', // Gelecekteki genişlemeye izin verir
GSI1PK: 'URL#https://example.com/very/long/url',
GSI1SK: 'LINK#abc123', // Tekilleştirme için
GSI2PK: 'USER#user123', // Kim oluşturdu
CreatedAt: 1706544000000, // Sıralama için timestamp
OriginalUrl: 'https://example.com/very/long/url',
ClickCount: 0,
ExpiresAt: 1738080000000, // TTL
Tags: ['campaign-2024', 'email'],
CustomSlug: 'summer-sale', // Opsiyonel özel slug
};
const clickRecord = {
PK: 'LINK#abc123',
SK: `CLICK#${Date.now()}#${uuid}`, // Benzersiz tıklama olayı
UserAgent: 'Mozilla/5.0...',
IPHash: 'hashed-ip', // Gizlilik uyumlu
Referer: 'https://twitter.com',
Timestamp: 1706544000000,
};
Bu tasarım şunları sağlar:
- Bir link için tüm veriyi tek istekle sorgula
- URL’leri verimli tekilleştir
- Analitik için bireysel tıklamaları takip et
- Çakışma olmadan özel slug’ları destekle
- TTL ile linkleri otomatik expire et
Create Handler Implementasyonu
İşte milyonlarca link işleyen create handler:
// src/handlers/create.ts
import { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
import { generateShortId } from '../utils/id-generator';
import { validateUrl } from '../utils/url-validator';
const client = new DynamoDBClient({});
const ddb = DynamoDBDocumentClient.from(client, {
marshallOptions: { removeUndefinedValues: true },
});
const TABLE_NAME = process.env.TABLE_NAME!;
const DOMAIN = process.env.SHORT_DOMAIN!;
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const startTime = Date.now();
try {
const body = JSON.parse(event.body || '{}');
const { url, customSlug, expiresInDays = 365, tags = [] } = body;
// URL'yi doğrula (production'da sık karşılaşılan bir sorun kaynağı)
const validation = await validateUrl(url);
if (!validation.isValid) {
return {
statusCode: 400,
body: JSON.stringify({
error: validation.error,
details: validation.details
}),
};
}
// Mevcut kısa link kontrolü (tekilleştirme)
const existing = await ddb.send(new QueryCommand({
TableName: TABLE_NAME,
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :pk',
ExpressionAttributeValues: {
':pk': `URL#${url}`,
},
Limit: 1,
}));
if (existing.Items?.length) {
const existingLink = existing.Items[0];
console.log(`Tekilleştirme bulundu: ${existingLink.PK}`);
return {
statusCode: 200,
body: JSON.stringify({
shortUrl: `${DOMAIN}/${existingLink.PK.replace('LINK#', '')}`,
isNew: false,
processingTime: Date.now() - startTime,
}),
};
}
// Çakışma tespiti ile kısa ID üret
let shortId = customSlug || generateShortId();
let attempts = 0;
const maxAttempts = 5;
while (attempts < maxAttempts) {
try {
await ddb.send(new PutCommand({
TableName: TABLE_NAME,
Item: {
PK: `LINK#${shortId}`,
SK: 'METADATA',
GSI1PK: `URL#${url}`,
GSI1SK: `LINK#${shortId}`,
GSI2PK: event.requestContext?.authorizer?.userId || 'ANONYMOUS',
CreatedAt: Date.now(),
OriginalUrl: url,
ClickCount: 0,
ExpiresAt: Date.now() + (expiresInDays * 24 * 60 * 60 * 1000),
Tags: tags,
CreatedBy: event.requestContext?.authorizer?.userId,
SourceIP: event.requestContext?.http?.sourceIp,
},
ConditionExpression: 'attribute_not_exists(PK)',
}));
break; // Başarılı!
} catch (error: any) {
if (error.name === 'ConditionalCheckFailedException') {
if (customSlug) {
return {
statusCode: 409,
body: JSON.stringify({
error: 'Özel slug zaten mevcut',
suggestion: generateShortId(),
}),
};
}
shortId = generateShortId(); // Başka ID dene
attempts++;
} else {
throw error;
}
}
}
return {
statusCode: 201,
body: JSON.stringify({
shortUrl: `${DOMAIN}/${shortId}`,
shortId,
expiresAt: new Date(Date.now() + (expiresInDays * 24 * 60 * 60 * 1000)).toISOString(),
processingTime: Date.now() - startTime,
}),
};
} catch (error) {
console.error('Kısa link oluşturma hatası:', error);
return {
statusCode: 500,
body: JSON.stringify({
error: 'Sunucu hatası',
requestId: event.requestContext?.requestId,
}),
};
}
};
ID Üreteci Tasarımı
nanoid, shortid ve bir sürü başka kütüphaneyi denedikten sonra, production’da gerçekten çalışan:
// src/utils/id-generator.ts
import { randomBytes } from 'crypto';
// Destek ekibi karıştırdıktan sonra belirsiz karakterleri kaldırdım (0, O, l, I)
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const ID_LENGTH = 7; // 3.5 trilyon kombinasyon verir
export function generateShortId(length: number = ID_LENGTH): string {
const bytes = randomBytes(length);
let id = '';
for (let i = 0; i < length; i++) {
id += ALPHABET[bytes[i] % ALPHABET.length];
}
return id;
}
// Özel slug'lar için - production deneyiminden gelen doğrulama kuralları
export function validateCustomSlug(slug: string): { valid: boolean; reason?: string } {
if (slug.length < 3) {
return { valid: false, reason: 'Çok kısa (min 3 karakter)' };
}
if (slug.length > 50) {
return { valid: false, reason: 'Çok uzun (max 50 karakter)' };
}
// Sadece alfanümerik ve tire, alfanümerik ile başlayıp bitmeli
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$/.test(slug)) {
return { valid: false, reason: 'Geçersiz karakter veya format' };
}
// Sorun yaratan rezerve kelimeler
const reserved = ['api', 'admin', 'dashboard', 'login', 'logout', 'static', 'health'];
if (reserved.includes(slug.toLowerCase())) {
return { valid: false, reason: 'Rezerve kelime' };
}
return { valid: true };
}
Lokal Geliştirme Ortamı
İlk günden lokal geliştirmeyi düzgün kurun. Her console.log değişikliğinde AWS’ye deploy etmek hem yavaş hem pahalıdır:
// local-dev.ts
import express from 'express';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { handler as createHandler } from './src/handlers/create';
import { handler as redirectHandler } from './src/handlers/redirect';
const app = express();
app.use(express.json());
// AWS servislerini lokal mockla
process.env.TABLE_NAME = 'local-links';
process.env.SHORT_DOMAIN = 'http://localhost:3000';
process.env.AWS_REGION = 'us-east-1';
// Lambda handler'ları Express için sarma
const lambdaToExpress = (handler: any) => async (req: any, res: any) => {
const event = {
body: JSON.stringify(req.body),
pathParameters: req.params,
queryStringParameters: req.query,
requestContext: {
http: {
sourceIp: req.ip,
},
requestId: Math.random().toString(36),
},
};
const result = await handler(event);
res.status(result.statusCode).json(JSON.parse(result.body));
};
app.post('/create', lambdaToExpress(createHandler));
app.get('/:id', lambdaToExpress(redirectHandler));
app.listen(3000, () => {
console.log('Lokal dev sunucusu http://localhost:3000 üzerinde çalışıyor');
console.log('DynamoDB Local port 8000 üzerinde gerekli');
});
DynamoDB’yi lokal çalıştır:
docker run -p 8000:8000 amazon/dynamodb-local \
-jar DynamoDBLocal.jar -sharedDb -inMemory
Deploy Script Yapılandırması
// package.json scripts
{
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk",
"local": "tsx watch local-dev.ts",
"deploy:dev": "cdk deploy --all --context environment=dev",
"deploy:prod": "cdk deploy --all --context environment=prod --require-approval never",
"destroy:dev": "cdk destroy --all --context environment=dev",
"synth": "cdk synth --quiet",
"diff": "cdk diff --all"
}
}
Production’dan Performans Rakamları
6 ay çalıştırdıktan sonra, işte gerçek rakamlar:
- Create endpoint: p50: 45ms, p99: 120ms
- Redirect endpoint (cold start): p50: 15ms, p99: 80ms
- Redirect endpoint (warm): p50: 8ms, p99: 25ms
- DynamoDB maliyeti: 5-10M yönlendirme için ~$6.25/ay (25M okuma birimi @ milyon başına $0.25)
- Lambda maliyeti: $12/ay (çoğu yönlendirme CloudFront’tan)
- CloudFront maliyeti: $85/ay (her kuruşuna değer)
Üretim Deneyiminden Çıkarımlar
-
On-demand DynamoDB ile başla: Erişim pattern’leri başta bilinmez. 3 ay sonra provisioned’a geçmek genellikle %60 tasarruf sağlar.
-
Her şeyi logla, hiçbir şeyi saklama: Başta her tıklamayı loglamak öğretici bir CloudWatch faturası üretir. %1 örnekleme yapıp gerisi için metrik kullanmak daha iyi bir yaklaşımdır.
-
Agresif önbellekle: Bir saatte 500,000 tıklama alan viral bir link, CloudFront’un büyük bir Lambda faturasını önlediği yerdir.
-
URL’leri düzgün doğrula: Birisi
javascript:alert('xss')için kısa link oluşturmaya çalışacak. Birisi yönlendirme döngüleri oluşturacak. Birisi servisi phishing için kullanacak. Bunları planlayın. -
İlk günden rate limiting: Olmadan, bir script bir ürün lansmanı sırasında 10 dakikada 100,000 link oluşturabilir.
Serinin Devamı
Temel işlevselliği uygulamaya hazır mısınız? Bölüm 2: Temel İşlevsellik & API Geliştirme’de:
- Akıllı önbellekleme stratejileriyle redirect handler oluşturacağız
- Cebinizi yakmayacak analitik uygulayacağız
- Rate limiting ve kötüye kullanım önleme ekleyeceğiz
- İşler bozulduğunda gerçekten haber veren monitoring kuracağız
Seriye Genel Bakış
- Bölüm 3: Özel domainler, QR kodları ve toplu işlemler dahil gelişmiş özellikler
- Bölüm 4: Blue-green deployment’lar ve sıfır kesinti migration’ları ile production deployment
- Bölüm 5: Ölçeklendirme stratejileri ve uzun vadeli bakım pattern’leri
Bu serinin tüm kodu GitHub’da, migration script’leri ve performans testleri dahil.
Link kısaltıcılar trafik ve kötüye kullanım yüzeyi ortaya çıkana kadar basit görünür. Baştan ölçek için tasarlayın, bugün çalışanı deploy edin ve her URL’yi doğrulayın.
Kaynaklar
- Tutorial: Create a CRUD HTTP API with Lambda and DynamoDB - Lambda ve DynamoDB destekli sunucusuz HTTP API kurulumunu anlatan resmi API Gateway eğitimi; link kısaltıcının temel deseni.
- Deploying Lambda functions with AWS CDK - TypeScript ile Lambda fonksiyonlarını tanımlayan ve dağıtan resmi CDK eğitimi.
- Tutorial: Create a serverless Hello World application - AWS CDK v2 - API Gateway REST API ve Lambda fonksiyonunu birleştiren uçtan uca CDK örneği.
- Best practices for designing and using partition keys effectively in DynamoDB - Kısa kod tablosu tasarımı için kritik olan DynamoDB bölüm anahtarı düzeni rehberi.
- Best practices for developing and deploying cloud infrastructure with the AWS CDK - Yapı yeniden kullanımı, ortam yapılandırması ve durum bilgili kaynak yönetimine ilişkin CDK en iyi uygulamaları.
- AWS CDK API Reference (v2) - aws-lambda, aws-apigateway ve aws-dynamodb dahil tüm CDK yapıları için eksiksiz API referansı.
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 Lambda, API Gateway, DynamoDB ve Step Functions için hızlı geri bildirim ve production güvenilirliği sağlayan kapsamlı bir test stratejisi oluşturmayı öğrenin.
Yönlendirme motoru, analytics toplama ve API Gateway yapılandırması: günlük milyonlarca yönlendirme için performans optimizasyonları ve debugging stratejileri.
Agent geliştirmek için TypeScript SDK karşılaştırması: Vercel AI SDK, OpenAI Agents SDK ve AWS Bedrock entegrasyonu, kod örnekleri ve karar frameworkleri ile.
Gelişmiş Amazon Cognito teknik rehberi: özel auth akışları, federation, multi-tenancy, migration stratejileri ve CDK ile production-grade güvenlik.
AWS AppSync ile ölçeklenebilir real-time API'ler: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.