İçeriğe atla
Ayhan Sipahi Ayhan Sipahi

AWS Lambda: Tek Amaçlı Fonksiyonlar mı, Lambdalith mi?

AWS Lambda fonksiyonlarını nasıl bölmeli: varsayılan single-purpose, single-domain Lambdalith'i hak edilmiş istisna gör, kararı veren platform güçlerini tanı.

Problem

Bir ekip AWS Lambda üzerinde HTTP API kurarken, erken alınan bir karar sonrasında gelen her şeyin şeklini sessizce belirler: kaç fonksiyon deploy edileceği. Tutorial’lar bir route’u bir fonksiyona eşler, ekipler de bunu kopyalar ve servis başına 60 fonksiyona ulaşır; her birinin kendi IAM rolü, bundling yapılandırması ve dağılmış bir template’in bir dilimi vardır. Konsolidasyon tepkisi buna bir “Lambdalith” ile yanıt verir: servis başına tek fonksiyon, içinde in-process bir router çalıştırır. Benim duruşum bu tepkinin tam tersi. Varsayılan olarak single-purpose fonksiyonlar seçin (route başına bir, event başına bir) ve single-domain Lambdalith’i bir başlangıç noktası değil, hak ettiğiniz bir konsolidasyon olarak görün. Önce spektrum gelir, sonra bu spektrumda nereye yerleşeceğinizi belirleyen platform güçleri (cold start, concurrency, IAM, observability).

Karar kolayca yanlış verilir çünkü single-purpose aleyhine sayılan maliyetler gürültülüdür, konsolidasyonun maliyetleri ise sessiz. Fonksiyon sprawl’ı konsolunuzda ilk gün görünür. Bir Lambdalith’in yarattığı paylaşılan concurrency havuzu ise yalnızca production’da bir route ısındığında görünür. Bu yüzden naif okuma birleştirmeyi destekler ve bu tam olarak tersinedir.

Spektrumdan önce, burada işin çoğunu yapan tek bir ayrım var. “Single responsibility” bir kod ilkesidir: her handler’ı odaklı tutun. “Kaç fonksiyon deploy edileceği” ise deploy birimleriyle ilgili bir topoloji kararıdır. Bunlar birbirinden bağımsız. Bir Lambdalith, router’ının arkasında kusursuz biçimde single-responsibility handler’lar tutarken tek bir deploy birimi olabilir; bir single-purpose fonksiyon ise karmakarışık bir god-handler tutabilir. Her zaman single-responsibility kod istersiniz, varsayılan olarak da single-purpose deploy birimleri. İkisini karıştırmak, “AWS artık monolit’leri onaylıyor” argümanını olduğundan güçlü gösteren şeydir. Onları ayrı tutun, gerilimin çoğu çözülür.

Note

Buradaki konu handler topolojisi, yani deploy ettiğiniz fonksiyon sayısı; stack düzeni değil. Bu fonksiyonların etrafındaki CDK construct’larını ve stack’lerini nasıl düzenleyeceğiniz için AWS CDK kod organizasyonu: servis mi domain bazlı mı yazısına bakın. İki karar ilişkili ama ayrıdır.

Granülerlik Spektrumu

Granülerlik tek bir eksen üzerindedir; en çok fonksiyondan en aza doğru. Onu üç adlandırılmış nokta sabitler.

  • Single-purpose (nano-fonksiyon): route veya aksiyon başına bir Lambda. POST /orders ve GET /orders/{id} ayrı fonksiyonlardır. Maksimum izolasyon, maksimum sayı. Varsayılan budur.
  • Single-scope: resource veya özellik başına bir Lambda. Tüm /orders/* tek bir fonksiyonda yaşar. Daha ılımlı konsolidasyon; bir orta yol.
  • Single-domain (Lambdalith): bounded context başına bir Lambda; API Gateway veya ALB proxy entegrasyonu arkasında in-process bir HTTP router (Hono, Fastify, Express veya AWS Lambda Web Adapter aracılığıyla herhangi bir framework) çalıştırır. En az fonksiyon; hak edilmiş istisna.

Single-purpose route basina bir fn

Single-scope resource basina bir fn

Single-domain Lambdalith

Izolasyon, tek birimlik blast radius

Sadelik, paylasilan havuz

En yetkili iki ses de single-purpose’u ilke düzeyinde destekler. AWS Well-Architected Serverless Lens bunu doğrudan söyler: “Speedy, simple, singular: Functions are concise, short, single-purpose.” Yan Cui aynı sorunun analizinde “having many single-purposed functions is clearly the better way to go” sonucuna varır; keşfedilebilirlik, debugging ve ekip ölçeklenmesi açısından. Fonksiyon sayısı itirazının fonksiyonları birleştirerek değil, tooling ve isimlendirme kurallarıyla çözüldüğünü savunur. Yazısı Lambdalith teriminden ve bugünkü tooling’in çoğundan önce gelir, bu yüzden tarihi bir güncellik çekincesi olarak görün; akıl yürütme hâlâ geçerli. Buradaki iş bu ortodoksiyi tekrar etmek değil. En güçlü konsolidasyon argümanlarını kendi zemininde savunmaktır.

Asıl örnek varsayılanın kendisidir: bir handler, bir iş, sıkıca scope’lanmış.

// src/orders/create.ts - bir fonksiyon, bir is
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";

export const handler: APIGatewayProxyHandlerV2 = async (event) => {
  const body = JSON.parse(event.body ?? "{}");
  // ...siparisi olustur
  return { statusCode: 201, body: JSON.stringify(body) };
};
// IaC'de bu fonksiyon tam olarak Orders tablosuna scope'lanmis bir rol,
// kendi concurrency kontrolunu ve kendi metric ile alarm'larini bedavaya alir.

İşte o “bedavaya” tüm argümandır ve sonraki bölümler bunu somutlaştırır. Aşağıdaki her güç, ayrı deploy ederek elde ettiğiniz ve birleştirerek kaybettiğiniz bir özelliktir.

Lens 1: Cold Start ve Trafik Dağılımı

En yaygın Lambdalith lehine iddia şudur: “daha az fonksiyon, daha az cold start.” Bir gerçek gibi söylendiğinde yanlıştır; asıl ilişki trafik dağılımıyla ilgilidir.

Lambda her eşzamanlı istek için ayrı bir execution environment sağlar ve yeni bir environment başlatması gerektiğinde bir cold start yaşar. Yani bir fonksiyon, hesabınızdaki toplam fonksiyon sayısıyla değil, kendi eşzamanlı trafiğiyle orantılı olarak sıcak kalır. On düşük trafikli route’u tek bir fonksiyonda birleştirmek onların trafiğini yoğunlaştırır ki bu o tek fonksiyonun sıcaklığını artırabilir. Ama bu trafik dağılımının bir sonucudur, konsolidasyonun size verdiği bedava bir azalma değil. Anlamlı ve istikrarlı trafik taşıyan bir single-purpose fonksiyon, hiçbir birleştirme olmadan kendi sıcak environment’larını korur. Dolayısıyla dürüst çerçeve şudur: konsolidasyon, tek başına sıcak kalamayacak kadar sessiz route’lar için sıcaklığı iyileştirebilir ve bu, ileride istisnaya giren gerçek bir girdidir, toptan bir kazanım değil.

Bu lens gündeme geldiğinde iki sınır önemli. Birincisi, INIT süresi için yayımlanmış bir AWS hedefi yoktur. AWS’in belgelediği şey, INIT fazının 10 saniyeyle sınırlı olduğu ve bitmezse Lambda’nın yapılandırılmış fonksiyon timeout’u altında ilk çağrıda INIT’i tekrar denediğidir (10 saniyelik sınır provisioned concurrency veya SnapStart için geçerli değildir). Sıkça tekrarlanan “500 ms’nin altında” rakamı bir AWS sayısı değil, topluluk çerçevesidir. İkincisi, daha büyük bir Lambdalith paketinin bireysel bir cold start’ı anlamlı biçimde kötüleştirip kötüleştirmediği makul ama AWS’in ölçmediği bir konudur ve tree-shaking ile bundling bunun çoğunu düzleştirir; bunu ölçülmüş bir ceza olarak değil, niteliksel bir trade-off olarak tartın.

Buradaki cold start, granülerlik kararı üzerine bir lens’tir, bir tuning rehberi değil. SnapStart, provisioned concurrency ve paket-init taktikleri için AWS Lambda cold start optimizasyonu yazısına bakın.

Lens 2: Concurrency ve Noisy Neighbor

Bu, single-purpose varsayılanı için en güçlü tek argümandır ve bir anekdot değil, belgelenmiş bir mekanizmadır.

Bir Lambdalith, hizmet verdiği her route üzerinde tek bir fonksiyonun concurrency’sini paylaşır. Lambda bir fonksiyonu, reserved concurrency limitine ulaştığında ya da hesap bölgesel tavanına çarptığında throttle eder (varsayılan olarak 1.000 eşzamanlı yürütme; yükseltebileceğiniz soft bir limit). Ayrıca concurrency kotasının on katı kadar bir istek-oranı limiti vardır, yani varsayılanda saniyede 10.000 istek; bu, 100 ms’nin altındaki handler’lar için önemlidir. Bunları bir araya koyun: bir Lambdalith içinde, sıcak bir route üzerindeki trafik patlaması fonksiyonun paylaşılan havuzunu tüketir ve oluşan throttle’lar tüm fonksiyona iner. Yavaş bir checkout route’u, aynı router’ın arkasında bulunan bir health check’i throttle edebilir. Noisy neighbor problemi budur ve Lambdalith bunu yapısı gereği yaratır.

Single-purpose: route basina limit

API GW

fn A (kendi limiti)

fn B (kendi limiti)

fn C (kendi limiti)

Lambdalith: paylasilan havuz

API GW proxy

Tek fonksiyon (tek concurrency havuzu)

route A (sicak)

route B (throttle)

route C (throttle)

Single-purpose fonksiyonlar her route’u kendi concurrency kontrol birimi yapar. Bu otomatik izolasyon değildir: varsayılan olarak her fonksiyon aynı bölgesel reserved-olmayan havuzdan çeker, dolayısıyla bir patlama yine de hesap concurrency’sini tüketip kardeş fonksiyonları throttle edebilir. Fark kontrol noktasındadır. Route başına bir fonksiyonla, herhangi bir route’a kendi reserved concurrency’sini verip gürültülü bir route’u sınırlayabilir ya da sakin olana kendi tabanını garantileyebilirsiniz. Bir Lambdalith içinde bunu yapamazsınız, çünkü her route tek bir fonksiyonun concurrency’sini paylaşır ve birim fonksiyondur; uzanacağınız route başına bir düğme yoktur. Bir Lambdalith içinde bir route’u izole etmenin tek yolu onu kendi fonksiyonuna geri çıkarmaktır. Buna karşılık, single-purpose’un yarattığı sprawl, IaC modülleri ve isimlendirme kurallarıyla çözdüğünüz bir tooling problemidir. Trade’in bir tarafı birleştirerek kaybettiğiniz bir kontrol noktasıdır; diğeri ev işidir.

Kaldıraçlar üzerine bir not, çünkü sıkça karıştırılırlar. Reserved ve provisioned concurrency birbirinin yerine geçen değil, tamamlayıcıdır ve ikisi de fonksiyon başına yapılandırılır. Reserved concurrency bir fonksiyonun maksimum eşzamanlı örnek sayısını belirler, o kapasiteyi hesap havuzundan ayırır ve ek ücret taşımaz; fonksiyon o reserved limite ulaştığında throttle edilir. Provisioned concurrency environment’ları önceden başlatır, böylece provisioned sayı içinde cold start olmaz ve bir örnek hiç istek işlemese bile Lambda o başlatma için ücret keser. Granülerlik için yük taşıyan nokta: bu kontroller fonksiyon başına olduğundan, kendi ölçeklenme bütçesine ihtiyaç duyan her route’un kendi fonksiyonu olmalıdır. Konsolidasyon route başına ölçeklenme kontrolünü feda eder.

Lens 3: IAM Blast Radius

Bir single-purpose fonksiyon, tam olarak o tek handler’ın dokunduğu şeye scope’lanmış bir IAM rolü alır. Sipariş oluşturan fonksiyon Orders tablosuna yazabilir, başka hiçbir şeye değil. Bu, platformun sunduğu en ince taneli least privilege’dır ve zaten bir rol ekleyecek olduğunuz anlamında bedavadır.

Bir Lambdalith, route’larının gerektirdiği her iznin birleşimine ihtiyaç duyar. Bounded context üç tablo okuyor, ikisine yazıyor, bir topic’e publish ediyor ve bir secret okuyorsa, tek rol bunların hepsini tutar ve her route bu tam set ile çalışır. Bir bug’ın ya da ele geçirilmiş bir bağımlılığın blast radius’u, mevcut isteğin ihtiyaç duyduğu tek izin değil, birleşimdir. Bunu kod içi yetkilendirme kontrolleriyle daraltabilirsiniz, ama bu, platformun uyguladığı bir garantiyi yazmak, test etmek ve doğru tutmak zorunda olduğunuz uygulama mantığına taşır. Aynı asimetri geçerli: fonksiyon başına IAM scope’lama bir platform sınırıdır; onu paylaşılan bir fonksiyon içinde yeniden kurmak artık kodunuzun işidir.

Bu lens konsolidasyonu yasaklamaz. Onu fiyatlandırır. Birleştirme, route’lar gerçekten bir IAM scope’unu paylaştığında kabul edilebilir; öyle ki birleşim her route’tan tek başına yalnızca biraz daha geniştir. Paylaşmadıklarında konsolidasyon fazladan yetki verir ve bu fazla yetki, route’lar bir fonksiyonu paylaştığı sürece kalıcıdır.

Lens 4: Hazır Observability ve Yeniden Kurulan Observability

Fonksiyon başına CloudWatch metric’leri otomatiktir. Invocation, error, duration, throttle ve concurrency, hiçbir enstrümantasyon olmadan fonksiyona göre ayrılmış olarak gelir. Single-purpose fonksiyonlarla bu ayrım route başınadır; yani p50, p95 ve p99 latency, error oranı ve throttle sayısı, hiçbir çaba olmadan tam route’a atfedilmiş durumdadır.

Bir Lambdalith her route’u tek bir fonksiyonun metric’lerinde birleştirir. p99’daki bir patlama size domain’in yavaş olduğunu söyler, hangi route’un olduğunu değil. Route başına granülerliği geri kazanmak için eşleşen route’u yapılandırılmış-log ya da Embedded Metric Format boyutu olarak yayar ve route başına görünümü oradan yeniden kurarsınız. Powertools for AWS Lambda bunu yapmanın yerleşik yoludur. Bu tamamen yapılabilir, ama şekli fark edin: single-purpose size route başına observability’yi platform varsayılanı olarak verir, Lambdalith ise onu uygulama kodunda yeniden kurmanızı ister. Buradan çıkan öneri: konsolide ediyorsanız route boyutunu ilk gün ekleyin, bir incident sırasında ihtiyaç duymadan önce, çünkü baskı altında eklemek en kötü zamandır.

Lambdalith Savunması, Dürüstçe

Konsolidasyon argümanı boş değil ve en güçlü hâli adil bir değerlendirmeyi hak ediyor. Fonksiyon sprawl’ı gerçektir: servis başına 60 fonksiyon, 60 rol, 60 log group ve okuması ile üzerinde akıl yürütmesi gerçekten daha zor bir template demektir. Route’lar arası refactor’lar birçok deploy birimine dokunur. Ayrıca AWS’in kendisi Lambda Web Adapter’ı gönderir; normal bir Express, Fastify, Koa veya Next.js uygulamasını handler imzasına yeniden yazmadan Lambda içinde çalıştırmanızı sağlayan AWS-bakımlı bir extension. Adapter, Lambda invoke’unu web sunucunuza giden bir localhost HTTP isteğine çevirir. Bu, Lambdalith’in onaylanmış bir pattern olduğuna dair güçlü bir sinyaldir.

İstisna haklı olduğunda şekli şudur. Tek bir fonksiyonun arkasındaki in-process router:

// src/orders/handler.ts - tum Orders domain'i icin tek fonksiyon (ISTISNA)
import { Hono } from "hono";
import { handle } from "hono/aws-lambda";

const app = new Hono();
app.get("/orders/:id", (c) => c.json({ id: c.req.param("id") }));
app.post("/orders", async (c) => c.json(await c.req.json(), 201));

export const handler = handle(app); // her Orders route'u icin tek Lambda handler

API Gateway tarafında, ANY metoduyla tek bir greedy {proxy+} resource her path ve metodu bu tek fonksiyona iletir. Lambda Web Adapter, mevcut web sunucunuzu adapter katmanının arkasında değiştirmeden çalıştırarak aynı hedefe yeniden yazmadan ulaşır.

Ama o sinyalin aslında ne dediğini okuyun. Web Adapter Lambdalith’i bir seçenek olarak onaylar; onu varsayılan yapmaz. AWS’in web framework’lerini Lambda’da çalıştıran bir araç göndermesi, AWS’in kendi Well-Architected rehberinden “Functions are concise, short, single-purpose” ifadesini geri çekmesi değildir. İkisi de doğru olabilir: single-purpose varsayılandır ve Lambdalith, hak ettiğinizde meşru ve desteklenen bir seçimdir. Ve sprawl şikâyeti, dürüstçe incelendiğinde, bir tooling problemidir. Bir isimlendirme kuralıyla yeniden kullanılabilir bir IaC modülü üzerinden tanımlanan altmış fonksiyon, elle bakımı yapılan altmış template değildir; altmış kez örneklenen tek bir modüldür. İşte asimetri bir kez daha: sprawl, kontrol ettiğiniz tooling ile çözülebilir, oysa birleştirerek vazgeçtiğiniz şey (route başına concurrency kontrolü, IAM, metric’ler, tek birimlik bir deploy blast radius’u) yapısaldır ve daha fazla kod yazarak yeniden yaratamazsınız.

Konsolidasyonun geri tepme biçimi öngörülebilirdir. Bir ekip, fonksiyon sayısını azaltmak için cohesive bir context’i tek bir Lambdalith’te birleştirir ve bir route bursty trafik almaya başlayana kadar her şey temiz görünür. O route paylaşılan havuzu tüketir, sibling route’lar throttle olmaya başlar ve fonksiyon başına metric’ler, sonradan bir route boyutu eklenene kadar nedenin hangi route olduğunu söyleyemez. Bunda egzotik hiçbir şey yoktur; concurrency ve observability lens’leri tam mekanizmanın öngördüğü gibi birlikte gelir. Çözüm, sıcak route’u reserved concurrency ile kendi fonksiyonuna geri çekmektir; yani: o route için varsayılan doğruydu ve birleştirme, problemi içerecek olan sınırı sessizce kaldırmıştı.

Lambdalith İçin İstisna Koşulları

Bir single-domain Lambdalith’e yalnızca cohesive bir bounded context için bunların her biri sağlandığında uzanın. Biri bile başarısız olursa single-purpose’da kalın; çoğu sağlanır ama bir iki route izolasyona ihtiyaç duyarsa, o route’ları kendi fonksiyonları olarak tutun ve geri kalanını bir Lambdalith arkasına koyun (bir hibrit).

  • Çok sayıda düşük trafikli route. Context’te, bireysel fonksiyonları nadiren sıcak kalacak route’lar var; öyle ki trafiklerini yoğunlaştırmak teorik değil gerçek bir sıcaklık kazancı.
  • Hiçbir route izole concurrency’ye ihtiyaç duymaz. Reserved ve provisioned concurrency fonksiyon başına olduğundan, kendi ölçeklenme bütçesine ihtiyaç duyan her route kendi fonksiyonu olarak kalmalı. Birleştirdiğiniz her route için paylaşılan bir havuz kabul edilebilir olmalı.
  • Ortak bir IAM scope. Birleştirme anlamlı biçimde fazla yetki vermez; izinlerin birleşimi her route’tan tek başına yalnızca biraz daha geniştir, böylece least privilege domain tanesinde hayatta kalır.
  • Tek deploy cadence ve owner. Hiçbir route diğerlerinden çok daha sık değişmez ya da farklı bir ekibe ait değildir; tüm domain’lik bir deploy blast radius’u kabul edilebilir.
  • Yeniden kurulabilir observability. Bedava fonksiyon başına metric’leri kaybetmeyi kabul edersiniz ve route başına p50/p95/p99 ile throttle sayılarını geri kazanmak için eşleşen route’u yapılandırılmış-log veya EMF boyutu olarak yayarsınız.

Single-scope (resource ailesi başına bir fonksiyon) bu istisnanın daha ılımlı hâlidir. Bir resource ailesi gerçekten tek bir deploy biriminden fayda gördüğünde ama tam bounded-context Lambdalith, acının haklı çıkardığından daha fazla birleştirme olduğunda kullanın.

Bu fonksiyonlarda yaşayan kod için, single-purpose ya da Lambdalith fark etmeksizin, iki ev işi notu: modüler AWS SDK v3’ü (@aws-sdk/*) kullanın, çünkü SDK v2 2025-09-08’de end-of-support’a ulaştı, ve güncel bir runtime hedefleyin (nodejs24.x, Lambda’da güncel Node LTS’dir). İkisi de granülerlik seçiminize bağlı değil, ama ikisi de birçok fonksiyon boyunca onunla birlikte birikir. Topolojiden bağımsız olarak zarar veren handler-içi alışkanlıklar için AWS Lambda anti-pattern’leri, geliştiricilerin monolit’lerden getirdikleri yazısına bakın.

Kapanış

Varsayılan olarak single-purpose Lambda fonksiyonları seçin, route başına bir ve event başına bir, ve handler kodunuzu topolojiden bağımsız olarak single-responsibility tutun. Single-purpose’un sağladığı özellikler (route başına concurrency kontrolü, least-privilege IAM, bedava route başına metric’ler ve tek birimlik bir deploy blast radius’u) yapısaldır ve birleştirdikten sonra geri takılamaz, oysa konsolidasyonu savunan fonksiyon sayısı acısı, IaC modülleri ve isimlendirme kurallarıyla çözebileceğiniz bir tooling problemidir. Yani “single responsibility, single scope mu, single domain mı?” sorusunun yanıtı varsayılan olarak single responsibility’dir. Bir single-domain Lambdalith’e yalnızca cohesive bir context’in fonksiyon sayısı acısı kanıtlandığında ve beş istisna koşulunun hepsi sağlandığında uzanın: çok sayıda düşük trafikli route, izole concurrency’ye ihtiyaç duyan hiçbir route’un olmaması, ortak bir IAM scope, tek deploy cadence ve owner, ve yapılandırılmış log’lardan yeniden kurmaya razı olduğunuz observability. Atılmaya değer tek sonraki adım, herhangi bir şeyi birleştirmeden önce aday context’inizin bu beşten hangilerini gerçekten sağladığını yazıya dökmektir.

Kaynaklar

İlgili yazılar