2025-09-04
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 için İnşa Etmek
Geçen ay, çeyreklik planlama toplantısı sırasında pazarlama ekibi acil bir istek geldi: “Tüm kampanyalarımız için markalı kısa linkler gerekiyor. Gelecek haftaya kadar yapabilir misin?” Kolay cevap bir SaaS çözümü almak olurdu, ama ayda 5-10 milyon yönlendirme yapıyorsanız ve özel analitik istiyorsanız, kendi çözümünüzü yapmak mantıklı olmaya başlıyor.
Link kısaltıcıların işi şu - production’a çıkana kadar basit görünüyorlar. Sonra tüm eğlenceli edge case’leri keşfediyorsun: yönlendirme döngüleri, kötü niyetli URL’ler, büyük ölçekte analitik ve benim kişisel favorim - birisi büyük bir kampanya lansmanı sırasında yanlışlıkla başka bir kısa linke, o da ilkine geri dönen bir kısa link oluşturduğunda. Bu tür senaryolar rate limiting ve URL doğrulama olmadan hızla sorun yaratır.
AWS CDK ile tatilinizdeyken sizi uyandırmayacak production-grade bir link kısaltıcı yapmayı göstereyim. Bu seri 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. Rate limiting ve URL doğrulama kampanya lansmanları sırasında kritik hale gelir.
Kara Cuma’yı Atlatan Mimari
Kod yazmadan önce, bir hafta boyunca peçetelere mimari çizdim (gerçekten - kahve dükkanı peçeteleri sistem tasarımı için harika). İşte sonuç:
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 Projenizi Kurma (Doğru Şekilde)
İ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 Tasarımı: Yüksek Hacimli Production’dan Dersler
İşte çoğu tutorial’ın yanlış yaptığı yer - size id ve url içeren basit bir tablo gösteriyorlar. Şirin, ama production’da yaşamaz. Üç veritabanı migration’ından sonra (her biri bir öncekinden daha acı verici), işte gerçekten çalışan şema:
// 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 göstereyim:
// 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
Her Şeyi Halleten Lambda
İş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 (bunu zor yoldan öğrendim)
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,
}),
};
}
};
Sizi Yarı Yolda Bırakmayacak ID Üretici
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 - bu kuralları kızgın kullanıcılardan öğrendim
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 };
}
Can Sıkmayan Lokal Geliştirme
İlk günden lokal geliştirmeyi düzgün kurun. İnanın, her console.log değişikliğinde AWS’ye deploy etmek istemezsiniz:
// 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
Gününüzü Mahvetmeyecek Deploy Script’i
// 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 ~0.25)
- Lambda maliyeti: $12/ay (çoğu yönlendirme CloudFront’tan)
- CloudFront maliyeti: $85/ay (her kuruşuna değer)
Zor Yoldan Öğrenilenler
-
On-demand DynamoDB ile başla - Henüz erişim pattern’lerinizi bilmiyorsunuz. 3 ay sonra provisioned’a geçtik ve %60 tasarruf ettik.
-
Her şeyi logla, hiçbir şeyi saklama - Başta her tıklamayı logladık. CloudWatch faturası… öğretici oldu. Şimdi %1 örnekleme yapıyor, gerisi için metrik kullanıyoruz.
-
Agresif önbellekle - Bir saatte 500,000 tıklama alan o viral link? CloudFront bizi büyük bir Lambda faturasından kurtardı.
-
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 servisinizi phishing için kullanacak. Bunları planlayın. -
İlk günden rate limiting - Başta eklemedik. Sonra bir ürün lansmanı sırasında birisinin script’i 10 dakikada 100,000 link oluşturdu. Eğlenceli zamanlar.
Bu Seride Sonraki Adımlar
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
Tüm Serinin Hızlı Önizlemesi:
- 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.
Unutmayın: link kısaltıcılar ta ki olmayıncaya kadar basittir. Baştan ölçek için inşa edin, ama bugün çalışanı deploy edin. Ve her zaman, her zaman o URL’leri doğrulayın.
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
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 konfigürasyonu. Günlük milyonlarca yönlendirmeyi işlemenin gerçek performans optimizasyonları ve debugging stratejileri.
AI agent geliştirmek için TypeScript SDK'larının pratik karşılaştırması - Vercel AI SDK, OpenAI Agents SDK ve AWS Bedrock entegrasyonu. Kod örnekleri, karar frameworkleri ve production patternleri içeriyor.
Amazon Cognito'nun gelişmiş özellikleri üzerine kapsamlı teknik kılavuz: özel authentication akışları, federation pattern'leri, multi-tenancy mimarileri, migration stratejileri ve production-grade güvenlik implementasyonu.
AWS AppSync ile ölçeklenebilir real-time API'ler geliştirmek için kapsamlı bir rehber: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.