Deno Deploy ile Çoklu Tenant SaaS Mimarisi ve İzolasyon Stratejileri
Çoklu tenant bir SaaS uygulaması yönetmek, tek bir sunucuda her şeyi koşturmaktan çok daha karmaşık bir denklem. Bir müşterinin yoğun trafiği diğerini etkiliyor, bir güvenlik açığı tüm sistemi tehdit ediyor ve ölçeklendirme kararları her tenant için ayrı ayrı düşünülmesi gereken bir hal alıyor. İşte tam bu noktada Deno Deploy’un edge computing modeli ve WebAssembly izolasyon yetenekleri devreye giriyor. Bu yazıda gerçek dünya senaryoları üzerinden Deno Deploy ile çoklu tenant SaaS mimarisi kuracağız, izolasyon stratejilerini inceleyeceğiz ve production’da karşılaşacağınız problemlere pratik çözümler getireceğiz.
Deno Deploy’un Çoklu Tenant Mimarisine Katkısı
Deno Deploy, V8 izolat (isolate) modelini kullanarak her istek için ayrı bir JavaScript çalışma ortamı sağlıyor. Bu model, geleneksel container tabanlı izolasyondan farklı olarak microsecond seviyesinde başlatma süresi sunuyor. Her izolat kendi bellek alanına, kendi global scope’una ve kendi kaynak limitine sahip.
Sysadmin perspektifinden baktığımızda bu şu anlama geliyor: Tenant A’nın çılgın bir döngüsü, Tenant B’nin response süresini etkilemiyor. Bu özellik, özellikle fintech veya sağlık gibi SLA kritik sektörlerde SaaS sunan şirketler için büyük avantaj.
Deno Deploy’un temel izolasyon bileşenlerini şöyle sıralayabiliriz:
- V8 Isolate: Her tenant için ayrı JavaScript runtime ortamı
- Deno.permissions: Granüler dosya sistemi, ağ ve ortam değişkeni izinleri
- KV Store: Tenant bazlı veri izolasyonu için namespace desteği
- Edge Middleware: İstek yönlendirmede tenant tespiti ve rate limiting
Temel Mimari Tasarım
Mimariyi üç katmana ayırıyoruz: Edge Router, Tenant Context ve Service Layer. Edge Router her isteği subdomain veya header üzerinden tanımlıyor, Tenant Context o tenant’a ait konfigürasyonu ve izinleri yüklüyor, Service Layer ise iş mantığını çalıştırıyor.
# Proje yapısını oluşturalım
mkdir -p saas-platform/{routes,middleware,tenants,services,utils}
cd saas-platform
# Deno yapılandırma dosyası
cat > deno.json << 'EOF'
{
"tasks": {
"dev": "deno run --allow-net --allow-env --allow-read --watch main.ts",
"deploy": "deployctl deploy --project=saas-platform main.ts"
},
"imports": {
"hono/": "https://deno.land/x/[email protected]/",
"std/": "https://deno.land/[email protected]/"
}
}
EOF
Ana giriş noktasını yazalım. Burada önemli olan her isteğin tenant context’i taşıması ve bu context’in downstream servislere güvenli şekilde iletilmesi:
cat > main.ts << 'EOF'
import { Hono } from "hono/mod.ts";
import { tenantMiddleware } from "./middleware/tenant.ts";
import { rateLimitMiddleware } from "./middleware/rateLimit.ts";
import { apiRouter } from "./routes/api.ts";
const app = new Hono();
// Global middleware zinciri
app.use("*", tenantMiddleware);
app.use("/api/*", rateLimitMiddleware);
app.route("/api", apiRouter);
app.get("/health", (c) => {
return c.json({
status: "ok",
region: Deno.env.get("DENO_REGION") ?? "local",
timestamp: new Date().toISOString()
});
});
Deno.serve(app.fetch);
EOF
Tenant Tespit Middleware’i
Tenant tespiti için iki yaygın strateji var: subdomain bazlı routing ve header bazlı routing. Production ortamlarında genellikle ikisini birden desteklemek gerekiyor. Bir API gateway üzerinden gelen istekler header kullanırken, son kullanıcılar subdomain üzerinden geliyor.
cat > middleware/tenant.ts << 'EOF'
import { Context, Next } from "hono/mod.ts";
import { TenantConfig, getTenantConfig } from "../services/tenantService.ts";
export async function tenantMiddleware(c: Context, next: Next) {
let tenantId: string | null = null;
// Önce header kontrol et (API gateway istekleri)
const headerTenant = c.req.header("X-Tenant-ID");
if (headerTenant) {
tenantId = headerTenant;
}
// Subdomain üzerinden tespit
if (!tenantId) {
const host = c.req.header("host") ?? "";
const subdomain = host.split(".")[0];
// reserved subdomainleri atla
const reserved = ["www", "api", "admin", "app"];
if (!reserved.includes(subdomain) && subdomain !== "") {
tenantId = subdomain;
}
}
// Path prefix üzerinden tespit (/t/{tenantId}/...)
if (!tenantId) {
const pathMatch = c.req.path.match(/^/t/([a-z0-9-]+)//);
if (pathMatch) {
tenantId = pathMatch[1];
}
}
if (!tenantId) {
return c.json({ error: "Tenant tespit edilemedi" }, 400);
}
// Tenant konfigürasyonunu yükle ve validate et
const tenantConfig = await getTenantConfig(tenantId);
if (!tenantConfig) {
return c.json({ error: "Geçersiz tenant" }, 404);
}
if (tenantConfig.status === "suspended") {
return c.json({ error: "Bu hesap askıya alınmış" }, 403);
}
// Context'e tenant bilgisini ekle
c.set("tenantId", tenantId);
c.set("tenantConfig", tenantConfig);
await next();
}
EOF
KV Store ile Tenant İzolasyonu
Deno Deploy’un built-in KV store’u multi-tenant veri izolasyonu için harika bir araç. Namespace (prefix) stratejisi kullanarak her tenant’ın verisini birbirinden ayırıyoruz. Önemli nokta: asla tenant ID’yi client’tan direkt almayın, her zaman doğrulanmış context’ten alın.
cat > services/tenantService.ts << 'EOF'
export interface TenantConfig {
id: string;
name: string;
plan: "starter" | "pro" | "enterprise";
status: "active" | "suspended" | "trial";
rateLimits: {
requestsPerMinute: number;
requestsPerDay: number;
};
features: string[];
createdAt: string;
}
// KV instance - Deno Deploy'da globally optimize edilmiş
const kv = await Deno.openKv();
// Tenant config önbelleği - izolat başına
const configCache = new Map<string, { config: TenantConfig; cachedAt: number }>();
const CACHE_TTL = 60 * 1000; // 60 saniye
export async function getTenantConfig(tenantId: string): Promise<TenantConfig | null> {
// Önbellekten kontrol et
const cached = configCache.get(tenantId);
if (cached && Date.now() - cached.cachedAt < CACHE_TTL) {
return cached.config;
}
// KV'den çek - ["tenants", tenantId] namespace'i ile
const result = await kv.get<TenantConfig>(["tenants", tenantId]);
if (!result.value) {
return null;
}
// Önbelleğe kaydet
configCache.set(tenantId, {
config: result.value,
cachedAt: Date.now()
});
return result.value;
}
export async function setTenantData<T>(
tenantId: string,
key: string[],
value: T
): Promise<void> {
// Her tenant verisi kendi namespace'inde tutuluyor
await kv.set(["tenant_data", tenantId, ...key], value);
}
export async function getTenantData<T>(
tenantId: string,
key: string[]
): Promise<T | null> {
const result = await kv.get<T>(["tenant_data", tenantId, ...key]);
return result.value;
}
export async function listTenantData<T>(
tenantId: string,
prefix: string[]
): Promise<Array<{ key: Deno.KvKey; value: T }>> {
const entries: Array<{ key: Deno.KvKey; value: T }> = [];
const iter = kv.list<T>({
prefix: ["tenant_data", tenantId, ...prefix]
});
for await (const entry of iter) {
entries.push({ key: entry.key, value: entry.value });
}
return entries;
}
EOF
Rate Limiting ve Kaynak İzolasyonu
Rate limiting, çoklu tenant sistemlerde en çok ihmal edilen ama en kritik konulardan biri. Bir tenant’ın API’yi patlatması diğerlerini etkilememeli. Deno Deploy’da bu kontrolü edge’de, dolayısıyla uygulamanıza ulaşmadan önce yapabilirsiniz.
cat > middleware/rateLimit.ts << 'EOF'
import { Context, Next } from "hono/mod.ts";
import { TenantConfig } from "../services/tenantService.ts";
const kv = await Deno.openKv();
interface RateLimitState {
count: number;
windowStart: number;
}
export async function rateLimitMiddleware(c: Context, next: Next) {
const tenantId = c.get("tenantId") as string;
const config = c.get("tenantConfig") as TenantConfig;
const now = Date.now();
const windowMs = 60 * 1000; // 1 dakikalık pencere
const windowKey = Math.floor(now / windowMs);
const rateLimitKey = ["rate_limits", tenantId, "minute", windowKey.toString()];
// Atomic işlem ile count artır
const result = await kv.atomic()
.mutate({
type: "sum",
key: rateLimitKey,
value: new Deno.KvU64(1n)
})
.commit();
if (!result.ok) {
return c.json({ error: "Rate limit işlemi başarısız" }, 500);
}
// Mevcut sayıyı oku
const countResult = await kv.get<Deno.KvU64>(rateLimitKey);
const currentCount = Number(countResult.value?.value ?? 0n);
const limit = config.rateLimits.requestsPerMinute;
// Response header'larına rate limit bilgisi ekle
c.header("X-RateLimit-Limit", limit.toString());
c.header("X-RateLimit-Remaining", Math.max(0, limit - currentCount).toString());
c.header("X-RateLimit-Reset", ((windowKey + 1) * windowMs).toString());
if (currentCount > limit) {
return c.json({
error: "Rate limit aşıldı",
retryAfter: Math.ceil(((windowKey + 1) * windowMs - now) / 1000)
}, 429);
}
// TTL ile otomatik temizleme - 2 dakika sonra sil
await kv.set(rateLimitKey, countResult.value, { expireIn: 2 * windowMs });
await next();
}
EOF
Plan Bazlı Feature Flag Sistemi
Enterprise SaaS’larda farklı müşteriler farklı özelliklere erişebilmeli. Bu kontrolü middleware seviyesinde yaparak iş mantığı kodunu temiz tutuyoruz.
cat > middleware/featureGate.ts << 'EOF'
import { Context, Next } from "hono/mod.ts";
import { TenantConfig } from "../services/tenantService.ts";
// Plan bazlı özellik tanımları
const PLAN_FEATURES: Record<string, string[]> = {
starter: ["basic_api", "webhooks", "csv_export"],
pro: ["basic_api", "webhooks", "csv_export", "advanced_analytics",
"custom_domains", "sso"],
enterprise: ["basic_api", "webhooks", "csv_export", "advanced_analytics",
"custom_domains", "sso", "audit_logs", "dedicated_support",
"custom_integrations", "sla_guarantee"]
};
export function requireFeature(featureName: string) {
return async (c: Context, next: Next) => {
const config = c.get("tenantConfig") as TenantConfig;
const planFeatures = PLAN_FEATURES[config.plan] ?? [];
const hasFeature = planFeatures.includes(featureName) ||
config.features.includes(featureName);
if (!hasFeature) {
return c.json({
error: "Bu özellik planınıza dahil değil",
feature: featureName,
currentPlan: config.plan,
upgradeUrl: `https://app.example.com/upgrade?tenant=${config.id}`
}, 403);
}
await next();
};
}
// Kullanım örneği route'larda:
// app.get("/analytics", requireFeature("advanced_analytics"), analyticsHandler)
EOF
Tenant Bazlı Webhook İzolasyonu
Webhook sistemleri multi-tenant mimarilerde özellikle dikkat gerektiriyor. Bir tenant’ın webhook hatası diğerlerini yavaşlatmamalı, her tenant’ın retry politikası ayrı olmalı.
cat > services/webhookService.ts << 'EOF'
import { setTenantData, getTenantData, listTenantData } from "./tenantService.ts";
interface WebhookEvent {
id: string;
tenantId: string;
type: string;
payload: unknown;
status: "pending" | "delivered" | "failed";
attempts: number;
createdAt: string;
nextRetryAt?: string;
}
const kv = await Deno.openKv();
export async function queueWebhookEvent(
tenantId: string,
eventType: string,
payload: unknown
): Promise<string> {
const eventId = crypto.randomUUID();
const event: WebhookEvent = {
id: eventId,
tenantId,
type: eventType,
payload,
status: "pending",
attempts: 0,
createdAt: new Date().toISOString()
};
// Tenant bazlı kuyrukta sakla
await setTenantData(tenantId, ["webhooks", "queue", eventId], event);
// Deno Deploy Queue ile async işleme gönder
await kv.enqueue({ tenantId, eventId }, {
delay: 0,
keysIfUndelivered: [["webhook_dlq", tenantId, eventId]]
});
return eventId;
}
// Queue consumer - her tenant için izole çalışır
kv.listenQueue(async (message: { tenantId: string; eventId: string }) => {
const { tenantId, eventId } = message;
const event = await getTenantData<WebhookEvent>(
tenantId,
["webhooks", "queue", eventId]
);
if (!event) return;
// Tenant'ın webhook URL'ini al
const webhookConfig = await getTenantData<{ url: string; secret: string }>(
tenantId,
["config", "webhook"]
);
if (!webhookConfig) return;
try {
const signature = await generateSignature(
JSON.stringify(event.payload),
webhookConfig.secret
);
const response = await fetch(webhookConfig.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Signature": signature,
"X-Webhook-Event": event.type,
"X-Webhook-ID": eventId
},
body: JSON.stringify(event.payload),
signal: AbortSignal.timeout(10000) // 10 saniye timeout
});
const updatedEvent = {
...event,
status: response.ok ? "delivered" as const : "failed" as const,
attempts: event.attempts + 1
};
await setTenantData(tenantId, ["webhooks", "queue", eventId], updatedEvent);
// Başarısız ise exponential backoff ile retry
if (!response.ok && event.attempts < 5) {
const delayMs = Math.pow(2, event.attempts) * 1000;
await kv.enqueue(message, { delay: delayMs });
}
} catch (error) {
console.error(`Tenant ${tenantId} webhook hatası:`, error);
}
});
async function generateSignature(payload: string, secret: string): Promise<string> {
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const key = await crypto.subtle.importKey(
"raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}
EOF
Audit Log ve Compliance
Enterprise müşteriler için audit log kritik. GDPR veya HIPAA gibi compliance gereksinimleri olan tenant’lar için tüm işlemlerin kaydedilmesi şart. Bu kaydı tenant bazlı namespace’lerde tutarak hem izolasyonu hem de yasal uyumluluğu sağlıyoruz.
cat > services/auditService.ts << 'EOF'
import { setTenantData } from "./tenantService.ts";
interface AuditEntry {
id: string;
tenantId: string;
userId: string;
action: string;
resource: string;
resourceId: string;
changes?: Record<string, unknown>;
ipAddress: string;
userAgent: string;
timestamp: string;
result: "success" | "failure";
}
export async function logAuditEvent(params: Omit<AuditEntry, "id" | "timestamp">): Promise<void> {
const entry: AuditEntry = {
...params,
id: crypto.randomUUID(),
timestamp: new Date().toISOString()
};
// Zaman damgalı key ile audit log namespace'ine kaydet
// Bu yapı zaman bazlı sorgulara izin veriyor
const datePrefix = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
await setTenantData(
params.tenantId,
["audit_logs", datePrefix, entry.id],
entry
);
// Enterprise tenant'lar için real-time streaming
// Buraya SIEM entegrasyonu eklenebilir
if (params.result === "failure") {
console.warn(`AUDIT FAILURE - Tenant: ${params.tenantId}, ` +
`Action: ${params.action}, User: ${params.userId}`);
}
}
// Middleware olarak da kullanılabilir
export function auditMiddleware(action: string, resource: string) {
return async (c: any, next: any) => {
const tenantId = c.get("tenantId");
const userId = c.get("userId") ?? "anonymous";
const startTime = Date.now();
await next();
const result = c.res.status < 400 ? "success" : "failure";
await logAuditEvent({
tenantId,
userId,
action,
resource,
resourceId: c.req.param("id") ?? "N/A",
ipAddress: c.req.header("CF-Connecting-IP") ??
c.req.header("x-forwarded-for") ?? "unknown",
userAgent: c.req.header("user-agent") ?? "unknown",
result
});
};
}
EOF
Production Deployment ve Monitoring
Deno Deploy’a production deployment için birkaç önemli nokta var. Özellikle secrets yönetimi ve environment ayarlarını doğru yapılandırmak kritik.
# deployctl ile deployment
npm install -g deployctl
# Proje oluştur ve secrets ayarla
deployctl projects create --name=saas-platform
# Tenant yönetimi için admin secret
deployctl secrets set KV_ENCRYPTION_KEY="$(openssl rand -base64 32)"
deployctl secrets set WEBHOOK_MASTER_SECRET="$(openssl rand -hex 32)"
deployctl secrets set ADMIN_API_KEY="$(openssl rand -hex 32)"
# Production deploy
deployctl deploy
--project=saas-platform
--entrypoint=main.ts
--include=*.ts
--include=deno.json
# Deployment durumunu kontrol et
deployctl deployments list --project=saas-platform
# Log izleme (production sorunlarını debug ederken hayat kurtarır)
deployctl logs --project=saas-platform --since=1h | grep "ERROR"
# Belirli bir tenant için log filtrele
deployctl logs --project=saas-platform | grep "tenantId:acme-corp"
Monitoring tarafında Deno Deploy’un built-in metrics endpoint’ini kullanabilirsiniz, ancak custom tenant bazlı metrikler için ayrı bir sistem kurmak gerekiyor.
# Tenant sağlık durumu kontrol scripti - cron'a eklenebilir
cat > scripts/tenant_health_check.sh << 'EOF'
#!/bin/bash
TENANTS=("acme-corp" "globex" "initech" "umbrella")
BASE_URL="https://saas-platform.deno.dev"
ALERT_WEBHOOK="https://hooks.slack.com/your-webhook-url"
for tenant in "${TENANTS[@]}"; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}"
-H "X-Tenant-ID: $tenant"
-H "Authorization: Bearer $HEALTH_CHECK_TOKEN"
"$BASE_URL/api/health"
--max-time 5)
if [ "$HTTP_CODE" != "200" ]; then
echo "$(date): HATA - $tenant tenant erişilemiyor, HTTP: $HTTP_CODE"
curl -s -X POST "$ALERT_WEBHOOK"
-H "Content-Type: application/json"
-d "{"text": "⚠️ Tenant $tenant health check başarısız: HTTP $HTTP_CODE"}"
else
echo "$(date): OK - $tenant tenant aktif"
fi
done
EOF
chmod +x scripts/tenant_health_check.sh
İzolasyon Stratejileri: Güçlü ve Zayıf Yönler
Deno Deploy’un izolasyon modeli hakkında gerçekçi olmak gerekiyor. Her şey mükemmel değil:
Güçlü yönler:
- V8 isolate seviyesinde bellek izolasyonu kod seviyesinde sağlanıyor
- Her istek için bağımsız execution context, global state sızıntısı yok
- KV namespace stratejisi ile veri izolasyonu açık ve auditable
- Edge’de çalışması sayesinde tenant bazlı coğrafi yakınlık sağlanabiliyor
Dikkat edilmesi gereken noktalar:
- KV store’da namespace hatası yaparsanız veri izolasyonu bozulabilir, her zaman doğrulanmış tenantId kullanın
- Shared KV instance üzerinde çok sayıda tenant yoğun yazma yaparsa contention oluşabilir, bu durumda tenant bazlı KV bölümleme düşünülmeli
- Deno Deploy’un cold start süresi ihmal edilebilir düzeyde olsa da çok büyük WASM modülleri yüklendiğinde artabiliyor
- Global state (Map, Set gibi yapılar) izolat restart’lardan sonra sıfırlanıyor, bunu cache stratejinizde göz önünde bulundurun
Ek güvenlik katmanı için öneriler:
- Her tenant isteğinde JWT içindeki tenantId ile header tenantId’yi çapraz doğrulayın
- Admin endpoint’leri için ayrı bir service authentication katmanı ekleyin
- Tenant bazlı IP whitelist özelliğini KV store’da saklayarak edge middleware’de uygulayın
- Düzenli aralıklarla KV store’un cross-tenant erişim testi yapın
Sonuç
Deno Deploy ile çoklu tenant SaaS mimarisi kurmak, geleneksel Kubernetes bazlı yaklaşımlara göre çok daha hızlı hayata geçirilebiliyor. V8 isolate modeli sayesinde tenant izolasyonu doğrudan runtime seviyesinde sağlanıyor, KV store namespace stratejisi ile veri sızıntısının önüne geçililiyor ve edge’de çalışması sayesinde global ölçekte düşük latency elde ediliyor.
Ancak bu mimariyi production’a taşımadan önce birkaç şeyi mutlaka yapın: tenant ID doğrulamasını her katmanda uygulayın, rate limiting’i ihmal etmeyin ve audit log altyapısını baştan kurun. Sonradan eklemeye çalışmak hem zor hem de riskli oluyor.
Bu yazıda gösterdiğimiz yapı bir başlangıç noktası. Gerçek bir production sistemde tenant onboarding workflow’ları, fatura entegrasyonu, custom domain provisioning ve daha detaylı monitoring eklenecek. Ama temel izolasyon katmanlarını sağlam kurduğunuzda üstüne inşa etmek çok daha kolay oluyor.
Deno Deploy’un bu alandaki en büyük avantajı deployment karmaşıklığını ortadan kaldırması. Kubernetes cluster yönetmeden, ingress controller yapılandırmadan, tenant bazlı namespace sorunlarıyla boğuşmadan doğrudan iş mantığına odaklanabiliyorsunuz. Sysadmin olarak söylüyorum: bu fark, özellikle küçük ekiplerde büyük zaman tasarrufu anlamına geliyor.
