Cloudflare Workers ile Webhook İşleme Sistemi Kurma
Webhook işleme sistemleri, modern yazılım altyapısının vazgeçilmez bir parçası haline geldi. GitHub, Stripe, Slack gibi servisler sürekli olarak senin uygulamana veri gönderiyor ve bu verileri hızlı, güvenilir bir şekilde işlemen gerekiyor. Geleneksel yaklaşımda bir sunucu ayağa kaldırır, ölçeklendirme sorunlarıyla boğuşur, bakım yaparsın. Cloudflare Workers ile bu tablonun tamamen değiştiğini göreceksin. Edge’de çalışan, global olarak dağıtılmış, sıfıra yakın cold start süreli bir webhook işleme sistemi kurmak artık birkaç saatlik iş.
Cloudflare Workers Neden Webhook İşleme İçin İdeal?
Webhook trafiği doğası gereği düzensizdir. Saatlerce sessiz kalır, sonra birden yüzlerce istek gelir. Bu yüzden webhook işleme sistemlerinin esnek ölçeklenebilir olması şart. Klasik bir EC2 instance veya VPS kurduğunda ya kapasitenin altında çalışırsın ve para ödersin, ya da trafik patlamasında sistemin çöker.
Cloudflare Workers bu sorunu köklü biçimde çözer. Her istek Cloudflare’in 300’den fazla PoP (Point of Presence) noktasından birine, kullanıcıya en yakın olan yere yönlendirilir. V8 isolate mimarisi sayesinde cold start süresi genellikle 0-5ms arasında kalır. Karşılaştırma için AWS Lambda’nın cold start süresi 100-500ms arasında değişir. Bu fark, yüksek frekanslı webhook senaryolarında ciddi anlamda hissedilir.
Üstelik Cloudflare Workers’ın ücretsiz planı günde 100,000 istek içeriyor. Orta ölçekli bir uygulama için bu limit gayet yeterli. Paid plan ise ayda 10 milyon istek için sadece 5 dolar.
Geliştirme Ortamını Hazırlamak
Başlamadan önce ortamını hazırlaman gerekiyor. Wrangler, Cloudflare’in resmi CLI aracı ve Workers geliştirme sürecinin merkezinde yer alıyor.
# Node.js 18+ gerekli, önce versiyonu kontrol et
node --version
# Wrangler'ı global olarak kur
npm install -g wrangler
# Cloudflare hesabına giriş yap
wrangler login
# Yeni bir Workers projesi oluştur
wrangler init webhook-processor --type javascript
# Proje dizinine gir
cd webhook-processor
# Gerekli bağımlılıkları kur
npm install
Wrangler login komutu seni tarayıcıya yönlendirir ve Cloudflare hesabın ile authenticate eder. Bu adım tamamlandıktan sonra wrangler.toml dosyasını düzenlemeye başlayabilirsin.
# wrangler.toml temel yapılandırması
cat > wrangler.toml << 'EOF'
name = "webhook-processor"
main = "src/index.js"
compatibility_date = "2024-01-01"
[vars]
ENVIRONMENT = "production"
[[kv_namespaces]]
binding = "WEBHOOK_CACHE"
id = "buraya_kv_namespace_id_gelecek"
[[queues.producers]]
binding = "WEBHOOK_QUEUE"
queue = "webhook-processing-queue"
[[queues.consumers]]
queue = "webhook-processing-queue"
max_batch_size = 10
max_batch_timeout = 30
EOF
KV namespace ve Queue oluşturmak için aşağıdaki komutları çalıştır:
# KV namespace oluştur
wrangler kv:namespace create "WEBHOOK_CACHE"
# Çıktıdaki id'yi wrangler.toml'a yapıştır
# Queue oluştur (paid plan gerektirir)
wrangler queues create webhook-processing-queue
# Development için preview namespace da oluştur
wrangler kv:namespace create "WEBHOOK_CACHE" --preview
Temel Webhook Handler Yazmak
Şimdi asıl işi yapan kodu yazmaya başlayalım. Gerçek dünyada bir webhook handler’ın yapması gereken üç temel şey var: isteği doğrulamak, işlemek ve yanıt vermek.
// src/index.js - Ana worker dosyası
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// Routing mantığı
if (request.method === 'POST' && url.pathname === '/webhooks/github') {
return handleGitHubWebhook(request, env, ctx);
}
if (request.method === 'POST' && url.pathname === '/webhooks/stripe') {
return handleStripeWebhook(request, env, ctx);
}
if (request.method === 'GET' && url.pathname === '/health') {
return new Response(JSON.stringify({ status: 'ok', timestamp: Date.now() }), {
headers: { 'Content-Type': 'application/json' }
});
}
return new Response('Not Found', { status: 404 });
}
};
async function handleGitHubWebhook(request, env, ctx) {
// İmza doğrulaması - bu adımı asla atlama!
const signature = request.headers.get('X-Hub-Signature-256');
const body = await request.text();
const isValid = await verifyGitHubSignature(
body,
signature,
env.GITHUB_WEBHOOK_SECRET
);
if (!isValid) {
return new Response('Unauthorized', { status: 401 });
}
const payload = JSON.parse(body);
const eventType = request.headers.get('X-GitHub-Event');
// İşlemi arka plana at, hemen 200 dön
ctx.waitUntil(processGitHubEvent(eventType, payload, env));
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
Burada dikkat etmen gereken kritik bir nokta var: ctx.waitUntil(). Bu fonksiyon sayesinde webhook’a hemen 200 yanıtı dönebilir, asıl işlemi arka planda devam ettirebilirsin. GitHub veya Stripe gibi servisler webhook endpoint’inden hızlı yanıt bekler. Eğer 10 saniye içinde yanıt gelmezse retry mekanizması devreye girer ve aynı webhook birden fazla işlenir.
İmza Doğrulama Sistemi
Güvenlik bu işin kalbi. Her büyük webhook sağlayıcısı, sahte istekleri filtrelemen için imza mekanizması kullanır. Workers ortamında Web Crypto API’ı kullanarak bunu implement edebilirsin.
// src/utils/crypto.js
async function verifyGitHubSignature(body, signature, secret) {
if (!signature || !secret) return false;
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const bodyData = encoder.encode(body);
// HMAC-SHA256 key oluştur
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
// Body'yi imzala
const signatureBuffer = await crypto.subtle.sign('HMAC', key, bodyData);
// Hex string'e çevir
const hashArray = Array.from(new Uint8Array(signatureBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
const expectedSignature = `sha256=${hashHex}`;
// Timing-safe karşılaştırma - bu kritik!
return timingSafeEqual(signature, expectedSignature);
}
function timingSafeEqual(a, b) {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
// Stripe imza doğrulama - farklı mekanizma kullanır
async function verifyStripeSignature(body, signature, secret) {
const parts = signature.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).split('=')[1];
const receivedSig = parts.find(p => p.startsWith('v1=')).split('=')[1];
// 5 dakikadan eski istekleri reddet (replay attack koruması)
const webhookAge = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (webhookAge > 300) return false;
const signedPayload = `${timestamp}.${body}`;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(signedPayload));
const hashHex = Array.from(new Uint8Array(sig))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return timingSafeEqual(receivedSig, hashHex);
}
Idempotency ve Duplicate Koruması
Webhook sistemlerinin en sinir bozucu problemi duplicate işlemedir. Ağ sorunları, retry mekanizmaları veya sağlayıcı taraflı hatalar aynı webhook’un birden fazla gelmesine neden olabilir. KV store kullanarak bunu önleyebilirsin.
// src/utils/idempotency.js
async function processWithIdempotency(eventId, processor, env) {
const cacheKey = `processed:${eventId}`;
// Daha önce işlenmiş mi kontrol et
const cached = await env.WEBHOOK_CACHE.get(cacheKey);
if (cached) {
console.log(`Event ${eventId} already processed, skipping`);
return JSON.parse(cached);
}
// Önce "processing" durumunu kaydet (race condition koruması)
await env.WEBHOOK_CACHE.put(
`processing:${eventId}`,
'true',
{ expirationTtl: 300 } // 5 dakika
);
try {
const result = await processor();
// Başarıyla işlendi, 24 saat sakla
await env.WEBHOOK_CACHE.put(
cacheKey,
JSON.stringify({ success: true, processedAt: Date.now(), result }),
{ expirationTtl: 86400 }
);
// Processing flag'ini temizle
await env.WEBHOOK_CACHE.delete(`processing:${eventId}`);
return result;
} catch (error) {
// Hata durumunda processing flag'ini sil, retry için izin ver
await env.WEBHOOK_CACHE.delete(`processing:${eventId}`);
throw error;
}
}
Gerçek Dünya Senaryosu: GitHub Actions Tetikleyici
Pratikte sık karşılaşılan senaryo şu: GitHub’a push geldiğinde otomatik deploy tetiklemek istiyorsun ama bunu doğrudan GitHub Actions üzerinden değil, kendi sistemin üzerinden kontrol etmek istiyorsun. Workers bu iş için biçilmiş kaftan.
// src/handlers/github.js
async function processGitHubEvent(eventType, payload, env) {
const deliveryId = payload.delivery || `gh-${Date.now()}`;
await processWithIdempotency(deliveryId, async () => {
switch (eventType) {
case 'push':
return handlePushEvent(payload, env);
case 'pull_request':
return handlePullRequestEvent(payload, env);
case 'release':
return handleReleaseEvent(payload, env);
default:
console.log(`Unhandled event type: ${eventType}`);
}
}, env);
}
async function handlePushEvent(payload, env) {
const { ref, repository, commits, pusher } = payload;
// Sadece main branch push'larını işle
if (ref !== 'refs/heads/main') return;
// Commit mesajlarını analiz et
const hasSkipCI = commits.some(c =>
c.message.includes('[skip ci]') || c.message.includes('[ci skip]')
);
if (hasSkipCI) {
console.log('CI skip requested, aborting deployment');
return;
}
// Deploy API'ına çağrı yap
const deployResponse = await fetch(env.DEPLOY_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${env.DEPLOY_API_TOKEN}`
},
body: JSON.stringify({
repository: repository.full_name,
branch: 'main',
commitSha: commits[commits.length - 1].id,
triggeredBy: pusher.name,
timestamp: Date.now()
})
});
if (!deployResponse.ok) {
throw new Error(`Deploy trigger failed: ${deployResponse.status}`);
}
// Slack'e bildirim gönder
await sendSlackNotification(env.SLACK_WEBHOOK_URL, {
text: `Deployment triggered for ${repository.full_name}`,
attachments: [{
color: '#36a64f',
fields: [
{ title: 'Repository', value: repository.full_name, short: true },
{ title: 'Triggered by', value: pusher.name, short: true },
{ title: 'Commit', value: commits[commits.length - 1].message.substring(0, 100) }
]
}]
});
return { deployed: true };
}
async function sendSlackNotification(webhookUrl, payload) {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
console.error(`Slack notification failed: ${response.status}`);
}
}
Hata Yönetimi ve Retry Mekanizması
Production ortamında her şey planlandığı gibi gitmez. Downstream servisler zaman zaman kapanır, rate limit’e takılırsın, network hataları olur. Bunu da ele almanız gerekiyor.
// src/utils/retry.js
async function withRetry(fn, options = {}) {
const {
maxAttempts = 3,
initialDelay = 1000,
maxDelay = 30000,
backoffMultiplier = 2,
retryableErrors = ['NetworkError', 'TimeoutError']
} = options;
let lastError;
let delay = initialDelay;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
const isRetryable = retryableErrors.some(e => error.message.includes(e))
|| error.status >= 500;
if (!isRetryable || attempt === maxAttempts) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
// Exponential backoff + jitter
await sleep(delay + Math.random() * 1000);
delay = Math.min(delay * backoffMultiplier, maxDelay);
}
}
throw lastError;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Rate limit farkındalıklı fetch wrapper
async function rateLimitedFetch(url, options, env) {
const domain = new URL(url).hostname;
const rateLimitKey = `ratelimit:${domain}`;
const limitInfo = await env.WEBHOOK_CACHE.get(rateLimitKey, { type: 'json' });
if (limitInfo && limitInfo.resetAt > Date.now() && limitInfo.remaining === 0) {
const waitTime = limitInfo.resetAt - Date.now();
console.log(`Rate limited by ${domain}, waiting ${waitTime}ms`);
await sleep(waitTime);
}
const response = await fetch(url, options);
// Rate limit header'larını oku ve sakla
const remaining = response.headers.get('X-RateLimit-Remaining');
const resetAt = response.headers.get('X-RateLimit-Reset');
if (remaining !== null) {
await env.WEBHOOK_CACHE.put(
rateLimitKey,
JSON.stringify({
remaining: parseInt(remaining),
resetAt: parseInt(resetAt) * 1000
}),
{ expirationTtl: 3600 }
);
}
return response;
}
Monitoring ve Logging
Workers ortamında traditional logging araçları çalışmaz. Cloudflare’in kendi logpush özelliğini veya harici bir logging servisi kullanman gerekir.
// src/utils/logger.js
class WebhookLogger {
constructor(env, requestId) {
this.env = env;
this.requestId = requestId;
this.logs = [];
}
log(level, message, data = {}) {
const entry = {
timestamp: new Date().toISOString(),
level,
requestId: this.requestId,
message,
...data
};
this.logs.push(entry);
console.log(JSON.stringify(entry));
}
info(message, data) { this.log('INFO', message, data); }
warn(message, data) { this.log('WARN', message, data); }
error(message, data) { this.log('ERROR', message, data); }
async flush() {
if (this.logs.length === 0) return;
// Axiom, Logtail veya başka bir logging servisine gönder
await fetch(this.env.LOGGING_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.env.LOGGING_API_KEY}`
},
body: JSON.stringify({ logs: this.logs })
}).catch(err => console.error('Failed to flush logs:', err));
}
}
// Metrics tracking
async function trackMetric(env, metric, value, tags = {}) {
const key = `metrics:${metric}:${new Date().toISOString().slice(0, 13)}`;
const existing = await env.WEBHOOK_CACHE.get(key, { type: 'json' }) || { count: 0, sum: 0 };
await env.WEBHOOK_CACHE.put(key, JSON.stringify({
count: existing.count + 1,
sum: existing.sum + value,
tags
}), { expirationTtl: 7200 });
}
Secrets Yönetimi ve Deployment
Güvenli credential yönetimi için Wrangler’ın secrets özelliğini kullan. Bu değerler şifreli olarak Cloudflare tarafında saklanır.
# Secret'ları ekle (production)
wrangler secret put GITHUB_WEBHOOK_SECRET
wrangler secret put STRIPE_WEBHOOK_SECRET
wrangler secret put DEPLOY_API_TOKEN
wrangler secret put SLACK_WEBHOOK_URL
wrangler secret put LOGGING_API_KEY
# Secret listesini kontrol et
wrangler secret list
# Yerel geliştirme için .dev.vars dosyası oluştur
cat > .dev.vars << 'EOF'
GITHUB_WEBHOOK_SECRET=dev_secret_buraya
STRIPE_WEBHOOK_SECRET=whsec_test_buraya
DEPLOY_API_TOKEN=test_token_buraya
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/test
EOF
# Yerel geliştirme sunucusunu başlat
wrangler dev
# Production'a deploy et
wrangler deploy
# Belirli bir environment'a deploy et
wrangler deploy --env staging
Test Stratejisi
Workers kodunu test etmek için @cloudflare/vitest-pool-workers paketini kullanabilirsin. Bu paket gerçek Workers runtime’ını test ortamında simüle eder.
# Test bağımlılıklarını kur
npm install -D vitest @cloudflare/vitest-pool-workers
# package.json'a test script'i ekle
npm pkg set scripts.test="vitest"
npm pkg set scripts.test:watch="vitest --watch"
// src/__tests__/webhook.test.js
import { describe, it, expect, vi } from 'vitest';
import { SELF } from 'cloudflare:test';
describe('GitHub Webhook Handler', () => {
it('geçersiz imzayı reddeder', async () => {
const response = await SELF.fetch('https://example.com/webhooks/github', {
method: 'POST',
headers: {
'X-Hub-Signature-256': 'sha256=invalid_signature',
'X-GitHub-Event': 'push',
'Content-Type': 'application/json'
},
body: JSON.stringify({ ref: 'refs/heads/main' })
});
expect(response.status).toBe(401);
});
it('health endpoint 200 döner', async () => {
const response = await SELF.fetch('https://example.com/health');
const body = await response.json();
expect(response.status).toBe(200);
expect(body.status).toBe('ok');
});
});
Sonuç
Cloudflare Workers ile webhook işleme sistemi kurmak, geleneksel sunucu tabanlı yaklaşıma kıyasla hem teknik hem de operasyonel açıdan büyük avantajlar sağlıyor. Burada anlattıklarımı özetlersek:
- İmza doğrulaması her zaman ilk adım olmalı, bypass etme düşüncesi aklından bile geçmesin
ctx.waitUntil()kullanarak webhook sağlayıcılarına hızlı yanıt verip işlemi arka planda sürdür- Idempotency mekanizması olmadan duplicate işlem problemi kaçınılmaz olur
- KV store durumsal işlemler, rate limiting ve idempotency kontrolü için mükemmel bir araç
- Secrets asla kod içinde saklanmaz,
wrangler secretkullan - Retry mekanizması exponential backoff ve jitter ile birlikte implement edilmeli
- Monitoring için harici bir logging servisi entegrasyonu production’da zorunlu
Sistemin büyüdükçe Cloudflare Queues’u daha agresif kullanmayı düşün. Özellikle bir webhook’un tetiklediği işlem uzun sürüyorsa veya birden fazla alt işleme bölünüyorsa Queue mimarisi çok daha temiz bir çözüm sunar. Durable Objects ise daha gelişmiş state yönetimi senaryoları için bir sonraki durağın olmalı.
Bu altyapıyı bir kez doğru kurduğunda, webhook entegrasyonları eklemek dakikalar içinde halledebileceğin rutin bir iş haline geliyor.
