Cloudflare Workers ile Sunucusuz Edge Uygulama Geliştirme

Sunucu yönetiminden bıktığınız, gece yarısı “disk dolu” alarmlarıyla uyandığınız, load balancer konfigürasyonlarıyla boğuştuğunuz günler artık geride kalıyor. Cloudflare Workers, kodunuzu dünyanın 300’den fazla noktasında çalıştırmanıza olanak tanıyan bir edge computing platformu. Sunucu yok, SSH yok, systemd servisi yok. Sadece kod yazıyorsunuz ve Cloudflare gerisini hallediyor.

Bu yazıda sysadmin gözüyle Cloudflare Workers’ı inceleyeceğiz. Basit “Hello World” değil, gerçek dünyada kullanabileceğiniz senaryolar üzerinden gideceğiz. Rate limiting, kimlik doğrulama proxy’si, webhook işleme, statik asset önbelleği gibi konulara el atacağız.

Cloudflare Workers Nedir ve Neden Önemli?

Geleneksel mimaride bir API endpoint’i yazıyorsunuz, bunu bir sunucuya deploy ediyorsunuz, önüne nginx koyuyorsunuz, SSL sertifikası ayarlıyorsunuz, monitoring kuruyorsunuz. Bir haftalık iş. Workers’ta bu süreç dakikalara iniyor.

Workers, V8 JavaScript motorunu kullanıyor ama Node.js değil. Bu önemli bir ayrım. Node.js API’larının tamamına erişiminiz yok, ama Cloudflare’in sunduğu Web API’ları var. fetch, Request, Response, Cache, KV, Durable Objects bunların başında geliyor.

Neden edge’de çalışması önemli?

  • İstanbul’daki bir kullanıcı isteği yapınca Cloudflare’in Frankfurt ya da İstanbul PoP’una gidiyor, Londra’daki sunucunuza değil
  • Gecikme süreleri dramatik şekilde düşüyor
  • DDoS koruması ücretsiz geliyor
  • Cold start süresi 0ms seviyesinde (Lambda gibi saniyeler beklemiyor)

Sınırlamalar da var:

  • CPU süresi istek başına 10ms (ücretsiz plan), 30ms (ücretli)
  • Çalışma ortamı izole edilmiş, dosya sistemi yok
  • Node.js modülleri direkt çalışmıyor
  • Uzun süren işlemler için uygun değil

Ortam Kurulumu

Wrangler, Cloudflare’in CLI aracı. Bunu olmadan Workers geliştirmek oldukça zahmetli olurdu.

# Node.js kurulu olduğunu varsayıyoruz
npm install -g wrangler

# Cloudflare hesabınıza giriş yapın
wrangler login

# Yeni proje oluşturun
wrangler init benim-worker --type javascript

# Dizine girin
cd benim-worker

# Yerel geliştirme sunucusunu başlatın
wrangler dev

wrangler init komutu size birkaç soru sorar. TypeScript kullanmak isteyip istemediğinizi, test framework tercihini vs. Ben bu yazıda sade JavaScript ile gideceğim ama production’da TypeScript şiddetle tavsiye ederim.

wrangler.toml dosyası projenizin kalbi:

# wrangler.toml
name = "benim-worker"
main = "src/index.js"
compatibility_date = "2024-01-01"

[vars]
ENVIRONMENT = "production"
API_BASE_URL = "https://api.example.com"

# KV namespace bağlamak için (daha sonra göreceğiz)
# [[kv_namespaces]]
# binding = "MY_KV"
# id = "xxxxxxxxxxxxx"

İlk Gerçek Senaryo: Reverse Proxy ve Request Manipülasyonu

Diyelim ki legacy bir API’nız var. Her istekte X-API-Key header’ı gerekiyor ama bu key’i client’lara vermek istemiyorsunuz. Workers bunu güzelce çözüyor.

// src/index.js
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    // Sadece /api/ altındaki istekleri proxy'le
    if (!url.pathname.startsWith('/api/')) {
      return new Response('Not Found', { status: 404 });
    }
    
    // Hedef URL'i oluştur
    const targetUrl = `${env.API_BASE_URL}${url.pathname}${url.search}`;
    
    // Orijinal header'ları kopyala, API key ekle
    const modifiedHeaders = new Headers(request.headers);
    modifiedHeaders.set('X-API-Key', env.SECRET_API_KEY);
    modifiedHeaders.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP'));
    modifiedHeaders.delete('Cookie'); // Cookie'leri upstream'e gönderme
    
    // İsteği ilet
    const response = await fetch(targetUrl, {
      method: request.method,
      headers: modifiedHeaders,
      body: request.method !== 'GET' ? request.body : null,
    });
    
    // Response header'larını temizle
    const cleanHeaders = new Headers(response.headers);
    cleanHeaders.delete('X-Powered-By');
    cleanHeaders.set('X-Cache', 'WORKER');
    
    return new Response(response.body, {
      status: response.status,
      headers: cleanHeaders,
    });
  },
};

Secret değerleri wrangler.toml‘a yazmıyorsunuz, Cloudflare’in secret manager’ını kullanıyorsunuz:

# Secret eklemek
wrangler secret put SECRET_API_KEY

# Mevcut secret'ları listele
wrangler secret list

# Deploy et
wrangler deploy

KV Store ile Basit Rate Limiting

Cloudflare Workers KV, dünyanın her yerinden okuma yapabilen düşük gecikmeli bir key-value store. Rate limiting için mükemmel.

Önce KV namespace oluşturalım:

# KV namespace oluştur
wrangler kv:namespace create "RATE_LIMIT"

# Çıktı size ID verecek, bunu wrangler.toml'a ekleyin:
# [[kv_namespaces]]
# binding = "RATE_LIMIT"
# id = "buraya_id_gelecek"

# Preview namespace (dev için)
wrangler kv:namespace create "RATE_LIMIT" --preview

Şimdi rate limiting mantığını yazalım:

// src/rate-limiter.js
const RATE_LIMIT = 100; // 1 dakikada maksimum istek
const WINDOW_SECONDS = 60;

export async function checkRateLimit(env, identifier) {
  const key = `rate_limit:${identifier}`;
  const now = Math.floor(Date.now() / 1000);
  const windowKey = `${key}:${Math.floor(now / WINDOW_SECONDS)}`;
  
  // Mevcut sayacı al
  const current = await env.RATE_LIMIT.get(windowKey);
  const count = current ? parseInt(current) : 0;
  
  if (count >= RATE_LIMIT) {
    return {
      allowed: false,
      remaining: 0,
      resetAt: (Math.floor(now / WINDOW_SECONDS) + 1) * WINDOW_SECONDS,
    };
  }
  
  // Sayacı artır, TTL ile birlikte kaydet
  await env.RATE_LIMIT.put(windowKey, String(count + 1), {
    expirationTtl: WINDOW_SECONDS * 2, // Biraz fazla TTL ver
  });
  
  return {
    allowed: true,
    remaining: RATE_LIMIT - count - 1,
    resetAt: (Math.floor(now / WINDOW_SECONDS) + 1) * WINDOW_SECONDS,
  };
}

// Ana worker'a entegre et
export default {
  async fetch(request, env, ctx) {
    const ip = request.headers.get('CF-Connecting-IP');
    const result = await checkRateLimit(env, ip);
    
    if (!result.allowed) {
      return new Response(
        JSON.stringify({ error: 'Rate limit exceeded', resetAt: result.resetAt }),
        {
          status: 429,
          headers: {
            'Content-Type': 'application/json',
            'X-RateLimit-Limit': String(RATE_LIMIT),
            'X-RateLimit-Remaining': '0',
            'X-RateLimit-Reset': String(result.resetAt),
            'Retry-After': String(result.resetAt - Math.floor(Date.now() / 1000)),
          },
        }
      );
    }
    
    // Normal iş akışına devam et
    return new Response('OK', {
      headers: {
        'X-RateLimit-Remaining': String(result.remaining),
      },
    });
  },
};

Webhook İşleme ve Doğrulama

GitHub, Stripe, Slack gibi servisler webhook gönderir. Bu webhook’ları doğrulamak ve işlemek için Workers harika bir yer. Kendi sunucunuzu expose etmek zorunda kalmıyorsunuz.

// src/webhook-handler.js
async function verifyGithubWebhook(request, secret) {
  const payload = await request.text();
  const signature = request.headers.get('X-Hub-Signature-256');
  
  if (!signature) {
    return { valid: false, payload: null };
  }
  
  // HMAC-SHA256 ile imzayı doğrula
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  
  const mac = await crypto.subtle.sign(
    'HMAC',
    key,
    encoder.encode(payload)
  );
  
  // Hex'e çevir
  const hashArray = Array.from(new Uint8Array(mac));
  const expectedSignature = 'sha256=' + hashArray
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
  
  // Timing-safe karşılaştırma için basit yaklaşım
  const valid = signature === expectedSignature;
  
  return { valid, payload: valid ? JSON.parse(payload) : null };
}

export default {
  async fetch(request, env, ctx) {
    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }
    
    const { valid, payload } = await verifyGithubWebhook(request, env.GITHUB_WEBHOOK_SECRET);
    
    if (!valid) {
      return new Response('Unauthorized', { status: 401 });
    }
    
    const event = request.headers.get('X-GitHub-Event');
    
    // Event tipine göre işlem yap
    if (event === 'push') {
      // Deploy tetikle, Slack bildir vs.
      ctx.waitUntil(handlePushEvent(payload, env));
    } else if (event === 'pull_request') {
      ctx.waitUntil(handlePREvent(payload, env));
    }
    
    // Hemen 200 dön, işlemi arka planda yap
    return new Response('OK', { status: 200 });
  },
};

async function handlePushEvent(payload, env) {
  const message = `🚀 ${payload.pusher.name} pushed to ${payload.ref} in ${payload.repository.full_name}`;
  
  await fetch(env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text: message }),
  });
}

async function handlePREvent(payload, env) {
  const { action, pull_request } = payload;
  const message = `📝 PR ${action}: ${pull_request.title} by ${pull_request.user.login}`;
  
  await fetch(env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text: message }),
  });
}

ctx.waitUntil() burada kritik. Response’u hemen döndürüyorsunuz ama arka plan işlemi devam ediyor. GitHub’ın timeout’una takılmıyorsunuz.

Router Yapısı ile Çoklu Endpoint

Gerçek uygulamalarda tek bir handler yeterli olmaz. Basit bir router yazalım:

// src/router.js
export class Router {
  constructor() {
    this.routes = [];
  }
  
  add(method, path, handler) {
    // Path'i regex'e çevir
    const pattern = new RegExp(
      '^' + path.replace(/:[^/]+/g, '([^/]+)') + '$'
    );
    this.routes.push({ method, pattern, path, handler });
    return this;
  }
  
  get(path, handler) { return this.add('GET', path, handler); }
  post(path, handler) { return this.add('POST', path, handler); }
  put(path, handler) { return this.add('PUT', path, handler); }
  delete(path, handler) { return this.add('DELETE', path, handler); }
  
  async handle(request, env, ctx) {
    const url = new URL(request.url);
    const pathname = url.pathname;
    const method = request.method;
    
    for (const route of this.routes) {
      if (route.method !== method && route.method !== '*') continue;
      
      const match = pathname.match(route.pattern);
      if (!match) continue;
      
      // URL parametrelerini çıkar
      const paramNames = [...route.path.matchAll(/:([^/]+)/g)].map(m => m[1]);
      const params = {};
      paramNames.forEach((name, i) => { params[name] = match[i + 1]; });
      
      return route.handler(request, env, ctx, params);
    }
    
    return new Response('Not Found', { status: 404 });
  }
}

// Kullanım
import { Router } from './router.js';

const router = new Router();

router.get('/health', async (request, env) => {
  return Response.json({ status: 'ok', timestamp: Date.now() });
});

router.get('/users/:id', async (request, env, ctx, params) => {
  const user = await env.KV.get(`user:${params.id}`, { type: 'json' });
  
  if (!user) {
    return Response.json({ error: 'User not found' }, { status: 404 });
  }
  
  return Response.json(user);
});

router.post('/users', async (request, env) => {
  const body = await request.json();
  
  if (!body.email || !body.name) {
    return Response.json({ error: 'email ve name zorunlu' }, { status: 400 });
  }
  
  const id = crypto.randomUUID();
  await env.KV.put(`user:${id}`, JSON.stringify({ id, ...body }));
  
  return Response.json({ id }, { status: 201 });
});

export default {
  fetch: (request, env, ctx) => router.handle(request, env, ctx),
};

Scheduled Workers ile Cron Görevleri

Sunucusuz diye cron job’lardan vazgeçmek zorunda değilsiniz. Workers’ın cron trigger özelliği var.

# wrangler.toml'a ekle
[triggers]
crons = ["0 */6 * * *"]  # Her 6 saatte bir
// src/index.js - scheduled handler ekle
export default {
  async fetch(request, env, ctx) {
    // HTTP istekleri buradan
    return new Response('Worker aktif');
  },
  
  async scheduled(event, env, ctx) {
    console.log(`Cron çalıştı: ${event.cron} - ${new Date().toISOString()}`);
    
    // Eski KV kayıtlarını temizle
    ctx.waitUntil(cleanupExpiredSessions(env));
    
    // Harici API'dan veri çek, KV'ye kaydet
    ctx.waitUntil(syncExternalData(env));
  },
};

async function cleanupExpiredSessions(env) {
  const list = await env.SESSIONS.list({ prefix: 'session:' });
  
  const now = Date.now();
  const deletePromises = [];
  
  for (const key of list.keys) {
    const session = await env.SESSIONS.get(key.name, { type: 'json' });
    if (session && session.expiresAt < now) {
      deletePromises.push(env.SESSIONS.delete(key.name));
    }
  }
  
  await Promise.all(deletePromises);
  console.log(`${deletePromises.length} oturum temizlendi`);
}

async function syncExternalData(env) {
  const response = await fetch('https://api.example.com/products');
  const data = await response.json();
  
  await env.KV.put('cached:products', JSON.stringify(data), {
    expirationTtl: 3600 * 6, // 6 saat
  });
  
  console.log(`${data.length} ürün senkronize edildi`);
}

Cron’u test etmek için:

# Local olarak scheduled event'i tetikle
wrangler dev --test-scheduled

# Başka terminalde
curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"

Monitoring ve Hata Yakalama

Production’da ne olduğunu görmek için basit bir hata yakalama ve loglama katmanı ekleyelim:

// src/middleware.js
export function withErrorHandling(handler) {
  return async (request, env, ctx) => {
    const startTime = Date.now();
    const requestId = crypto.randomUUID().slice(0, 8);
    
    try {
      const response = await handler(request, env, ctx);
      
      const duration = Date.now() - startTime;
      console.log(JSON.stringify({
        requestId,
        method: request.method,
        url: request.url,
        status: response.status,
        duration,
        country: request.headers.get('CF-IPCountry'),
      }));
      
      // Response'a debugging header ekle
      const newHeaders = new Headers(response.headers);
      newHeaders.set('X-Request-Id', requestId);
      newHeaders.set('X-Response-Time', `${duration}ms`);
      
      return new Response(response.body, {
        status: response.status,
        headers: newHeaders,
      });
      
    } catch (error) {
      const duration = Date.now() - startTime;
      
      console.error(JSON.stringify({
        requestId,
        method: request.method,
        url: request.url,
        error: error.message,
        stack: error.stack,
        duration,
      }));
      
      // Hata bildirimini arka planda gönder
      ctx.waitUntil(
        fetch(env.ERROR_WEBHOOK_URL, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            text: `❌ Worker hatası: ${error.message}nURL: ${request.url}nRequest ID: ${requestId}`,
          }),
        }).catch(() => {}) // İkincil hata olmasın
      );
      
      return Response.json(
        { error: 'Internal Server Error', requestId },
        { status: 500 }
      );
    }
  };
}

// Kullanım
import { withErrorHandling } from './middleware.js';
import { router } from './routes.js';

export default {
  fetch: withErrorHandling((request, env, ctx) => router.handle(request, env, ctx)),
};

Logları görmek için:

# Canlı log stream
wrangler tail

# Belirli worker için
wrangler tail benim-worker

# JSON formatında filtrele
wrangler tail --format json | jq '.logs[].message'

Deploy Süreci ve Environment Yönetimi

Staging ve production ortamlarını ayırmak için wrangler.toml‘u şöyle düzenleyin:

# wrangler.toml
name = "benim-worker"
main = "src/index.js"
compatibility_date = "2024-01-01"

[env.staging]
name = "benim-worker-staging"
vars = { ENVIRONMENT = "staging" }

[env.production]
name = "benim-worker-prod"
vars = { ENVIRONMENT = "production" }
  routes = [
    { pattern = "api.example.com/*", zone_name = "example.com" }
  ]

Deploy komutları:

# Staging'e deploy
wrangler deploy --env staging

# Production'a deploy
wrangler deploy --env production

# Belirli bir versiyona rollback
wrangler rollback --env production

# Deploy geçmişini gör
wrangler deployments list

# Anlık metrikleri kontrol et
wrangler metrics --env production

Gerçek Dünya: Statik Site için Edge Cache

Son olarak, statik bir sitenin önüne akıllı bir cache katmanı koymak çok yaygın bir ihtiyaç. Bunu Workers ile nasıl yapacağımıza bakalım:

// src/cache-worker.js
const CACHE_TTL = 3600; // 1 saat

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    // Sadece GET isteklerini cache'le
    if (request.method !== 'GET') {
      return fetch(request);
    }
    
    // Cache'e bak
    const cache = caches.default;
    const cacheKey = new Request(url.toString(), request);
    let response = await cache.match(cacheKey);
    
    if (response) {
      // Cache hit
      const newHeaders = new Headers(response.headers);
      newHeaders.set('X-Cache-Status', 'HIT');
      return new Response(response.body, { headers: newHeaders });
    }
    
    // Cache miss, origin'den al
    response = await fetch(request);
    
    // Sadece başarılı yanıtları cache'le
    if (response.ok) {
      const contentType = response.headers.get('Content-Type') || '';
      
      // Statik asset'ler daha uzun cache'lensin
      const isStaticAsset = url.pathname.match(/.(js|css|png|jpg|gif|ico|woff2?)$/);
      const ttl = isStaticAsset ? CACHE_TTL * 24 : CACHE_TTL;
      
      const cachedResponse = new Response(response.clone().body, {
        headers: {
          ...Object.fromEntries(response.headers),
          'Cache-Control': `public, max-age=${ttl}`,
          'X-Cache-Status': 'MISS',
        },
      });
      
      ctx.waitUntil(cache.put(cacheKey, cachedResponse));
    }
    
    return response;
  },
};

Sonuç

Cloudflare Workers, sysadmin’lerin araç kutusuna güçlü bir ekleme yapıyor. Sunucu kurulumu, SSL yönetimi, scaling endişesi olmadan production-grade uygulamalar çalıştırabiliyorsunuz. KV store ile durum tutabiliyorsunuz, cron triggerlar ile zamanlanmış görevler koşturabiliyorsunuz, webhook’ları güvenle işleyebiliyorsunuz.

Elbette her şey pembe değil. Uzun süren işlemler için uygun değil, Node.js ekosisteminin tamamına erişemiyorsunuz ve KV’nin eventually consistent yapısı bazen sürpriz yaratıyor. Ama bir API proxy, rate limiter, webhook handler ya da edge cache için gerçekten mükemmel bir araç.

Başlangıç için önerim şu: Mevcut bir projenizde tek bir küçük sorunu Workers ile çözün. Belki rate limiting, belki bir webhook endpoint’i. Deneyim kazanınca daha büyük parçaları taşıyabilirsiniz. Wrangler’ın yerel geliştirme ortamı da oldukça olgun, production’a atmadan her şeyi local’de test edebiliyorsunuz.

Workers, “serverless” sözcüğünü gerçekten anlayan nadir platformlardan biri. Ücretsiz plan günde 100.000 istek kapsıyor, bu da denemek için gayet yeterli. wrangler init yapın, birkaç saat oynayın, ne kadar çok şeyi ne kadar az karmaşıklıkla yapabildiğinizi göreceksiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir