İçeriğe atla

Tek Amaçlı Lambda'ları Hono Lambdalith'e Route Route Taşımak

API Gateway arkasındaki tek amaçlı Lambda'ları Hono Lambdalith'e route route taşıyın; auth scope, doğrulama, throttling ve metrikler için route başına karar kuralı.

Ayhan Sipahi Ayhan Sipahi

Zaten çalışan bir API Gateway’in arkasındaki tek amaçlı Lambda fonksiyonlarını tek bir Hono Lambdalith’te toplamak, kod probleminden önce bir yönlendirme problemidir. Authorizer scope’ları, istek doğrulama, throttling ve route başına metrikler gateway route’u üzerinde tanımlıdır. Reserved concurrency, IAM rolü ve fonksiyon başına metrikler ise her fonksiyonun kendi üzerinde durur. On beş route’u silip yerine tek bir {proxy+} catch-all ekleyen tek bir deploy, bu ayarların hepsini aynı anda değiştirir ve route bazında geri dönüş yolu bırakmaz. Hem HTTP API’de hem REST API’de açık (explicit) bir route catch-all’a her zaman üstün gelir. Güvenli sıra bu kuraldan çıkar: Hono fonksiyonunu önce catch-all route’a yerleştirin, her açık route’un entegrasyonunu ona yönlendirin (retarget) ve bir route’u ancak üzerinde gateway’e ihtiyaç duyan hiçbir şey kalmadığında silin. TypeScript ekipleri için CDK ile kurulan bu sıranın altı parçası vardır: süreci yöneten envanter, HTTP API için catch-all ve retarget kodu, REST’teki farklar, Hono’da yeniden kurulan gateway özellikleri, her geçişi onaylayan parite testi ve tek amaçlı kalan route’lar.

Birleştirip birleştirmemek ayrı bir karardır ve AWS Lambda: Tek Amaçlı Fonksiyonlar vs Lambdalith yazısında sonuca bağlanmıştır: varsayılan tek amaçlı fonksiyondur, tek alanlı Lambdalith ise beş kriteri karşılayarak hak edilen bir istisnadır. Aşağıdaki her şey, bu kararın tek bir bounded context için (burada bir sipariş API’si) verildiğini varsayar. Ulaşılmak istenen son durum hibrit bir yapıdır.

Sonra (hibrit son durum)

$default

POST /orders (retarget edildi, scope'larını korur)

POST /orders/import

API Gateway

Orders Lambdalith (Hono)

import-orders (reserved concurrency)

Önce (route başına bir fonksiyon)

GET /orders/{id}

POST /orders

GET /orders

POST /orders/import

API Gateway

get-order

create-order

list-orders

import-orders

İlk Deploy’dan Önce Route Envanteri#

Sonraki her adım tek bir route hakkında verilen bir karardır. Bu yüzden ilk çıktı bir tablodur: her route için bir satır, taşınırken kaybolabilecek her özellik için bir sütun. Sütunlar şunlardır: authorizer ve authorization scope’ları, istek doğrulayıcı (request validator) veya model, throttle override’ları, o route’un gateway metriklerine bağlı bir dashboard ya da alarm olup olmadığı, reserved veya provisioned concurrency, kardeş route’ların hiçbirinin ihtiyaç duymadığı IAM ifadeleri ve entegrasyonun bugün kullandığı payload format sürümü.

Sütunlar, API Gateway ve Lambda’nın her özelliği nerede sakladığından türer. HTTP API’de JWT authorizer’lar ve scope’lar route’a bağlıdır; AWS’nin ifadesiyle, “If you configure scopes for a route, the token must include at least one of the route’s scopes.” Route seviyesinde throttling stage üzerinde, route key’e göre tutulur; route başına gateway metrikleri ise yalnızca stage’de detaylı metrikler açıkken üretilir. REST API buna istek doğrulayıcıları, metot başına throttle içeren usage plan’leri, API key’leri ve önbelleği ekler. Bunların hepsi metot ya da stage ve metot düzeyinde yapılandırılır ve hiçbiri HTTP API’de yoktur. Buna karşılık reserved ve provisioned concurrency, çalıştırma rolü ve Lambda’nın kendi fonksiyon başına metrikleri fonksiyonun özellikleridir. Bir route’un trafiği başka bir fonksiyondan akmaya başladığı anda bu özellikler o route’u tanımlamaz olur.

Tablonun büyük kısmını AWS CLI çıkarır. HTTP API için:

API_ID=abc123
aws apigatewayv2 get-routes --api-id "$API_ID" \
  --query 'Items[].[RouteKey,AuthorizationType,AuthorizationScopes,Target]' --output table
aws apigatewayv2 get-stage --api-id "$API_ID" --stage-name '$default' \
  --query '{detailed:DefaultRouteSettings.DetailedMetricsEnabled,routes:RouteSettings}'

REST API için:

API_ID=abc123
aws apigateway get-resources --rest-api-id "$API_ID" --embed methods \
  --query 'items[].{path:path,methods:resourceMethods}'
aws apigateway get-stage --rest-api-id "$API_ID" --stage-name prod --query methodSettings
aws apigateway get-usage-plans --query 'items[].apiStages'

Fonksiyon başına:

aws lambda get-function-concurrency --function-name get-order
aws lambda get-provisioned-concurrency-config --function-name get-order --qualifier live

Aşağıdaki matris, sonraki her adımın uyguladığı kuraldır. Retarget edilmiş, açık route’un yerinde kaldığı ve Lambdalith’i hedeflediği anlamına gelir. Silinmiş ise route’un kalktığı ve yolu catch-all’ın karşıladığı anlamına gelir.

ÖzellikNerede yaşarRetarget sonrası kalır mıSilme sonrası kalır mı
JWT authorizer ve scope’lar (HTTP API)routeevetyalnızca catch-all’ın authorizer’ı ve scope’ları eşleşirse ya da kontrol Hono’da yeniden kurulduysa
İstek doğrulayıcı ve model (REST)metotevethayır, Hono doğrulayıcısıyla yeniden kurun
Route throttle (HTTP API) veya usage-plan metot throttle’ı (REST)stage route ayarları veya usage planevetHono karşılığı yok
Route başına gateway metrikleristage ve routeevetcatch-all’ın serisine karışması beklenir
Reserved veya provisioned concurrencyfonksiyonhayırhayır
IAM rol kapsamıfonksiyonhayır, rol bir birleşime dönüşürhayır
Fonksiyon başına Lambda metriklerifonksiyonhayır, EMF boyutu olarak yeniden kurunhayır, aynı
Fonksiyon seviyesi ölçekleme hızıfonksiyonpaylaşılırpaylaşılır

Fonksiyon sayısının kendisi çoğu zaman taşımayı başlatan baskıdır; CloudFormation’ın 500 Kaynak Sınırını Aşmak o tarafı ele alır. Envanter size hangi satırların silinmiş sütununa ulaşabileceğini, hangilerinin retarget edilmişte duracağını ve hangilerinin hiç yerinden kıpırdamayacağını söyler.

Bounded Context için Tek Hono Uygulaması#

Hono, Web Standartları üzerine kurulu bir router’dır. Handler bir c context’i alır, isteği c.req üzerinden okur ve standart bir Response döndürür. app.route(prefix, subApp) bir alt uygulamayı yol öneki altına bağlar, app.use(path, middleware) eşleşen handler’dan önce middleware çalıştırır, c.env ise runtime adapter’ının bağladığı her şeyi taşır. Lambda’da bu, ham event ve Lambda context’idir. Adapter @hono/aws-lambda paketindedir: Hono v4.13.10 runtime adapter’larını ayrı paketlere taşıdı; eski hono/aws-lambda alt yolu v4’te hâlâ çalışır ama v5 öncesinde kullanımdan kaldırıldı (deprecated). Eski eğitimlerin, bu sitedeki önceki yazılar dahil, farklı bir yoldan import etmesinin nedeni budur.

Pariteyi denetlenebilir kılan hamle, daha hiç Hono kodu yokken her eski handler’ın çekirdeğini sade bir fonksiyona çıkarmaktır. Eski handler ile Hono route’u aynı fonksiyonu çağırır; böylece parite yapı gereği sağlanır ve her route’un diff’i küçük kalır. Callback tarzı handler’ları aynı anda async’e çevirin: Node.js 24 runtime’ı callback handler imzasını context.done, context.succeed ve context.fail ile birlikte kaldırdı; dolayısıyla route taşınsa da taşınmasa da bu dönüşümün vakti gelmiştir.

// src/legacy/get-order.ts (çekirdek çıkarıldıktan sonra, route taşınmadan önce)
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Context } from 'aws-lambda'
import { getOrderById } from '../orders/core/get-order-by-id'

const json = { 'content-type': 'application/json' }

export const handler = async (
  event: APIGatewayProxyEventV2,
  _context: Context,
): Promise<APIGatewayProxyStructuredResultV2> => {
  const order = await getOrderById(event.pathParameters?.id ?? '')
  return order
    ? { statusCode: 200, headers: json, body: JSON.stringify(order) }
    : { statusCode: 404, headers: json, body: JSON.stringify({ message: 'Not Found' }) }
}

Hono tarafı; binding’ler için tek bir Env tipi, eski fonksiyon başına bir alt uygulama, tek bir kök uygulama ve tek bir handler dosyasıdır.

// src/orders/env.ts
import type { LambdaEvent } from '@hono/aws-lambda'
import type { Context } from 'aws-lambda'

// Binding, Lambda'nın kendi Context tipini kullanır çünkü Powertools Logger onu bekler.
// Hono'nun LambdaContext'i, o tipin hâlâ taşıdığı kullanımdan kalkmış done/fail/succeed üyelerini içermez.
export type Env = {
  Bindings: { event: LambdaEvent; lambdaContext: Context }
}
// src/orders/routes/get-order.ts
import { Hono } from 'hono'
import { getOrderById } from '../core/get-order-by-id'
import type { Env } from '../env'

export const getOrder = new Hono<Env>().get('/:id', async (c) => {
  const order = await getOrderById(c.req.param('id'))
  return order ? c.json(order) : c.json({ message: 'Not Found' }, 404)
})
// src/orders/app.ts
import { Hono } from 'hono'
import type { Env } from './env'
import { observe } from './observe'
import { createOrder } from './routes/create-order'
import { getOrder } from './routes/get-order'

export const app = new Hono<Env>()
app.use('*', observe)
app.route('/orders', getOrder)
app.route('/orders', createOrder)
app.notFound((c) => c.json({ message: 'Not Found' }, 404))
// src/orders/handler.ts
import { handle } from '@hono/aws-lambda'
import { app } from './app'

export const handler = handle(app)

handle, event’in şekline bakar ve API Gateway REST (payload format 1.0), HTTP API ve Function URL (payload format 2.0), ALB ve VPC Lattice event’lerini kabul eder. 1.0 için istek URL’sini event.path’ten, 2.0 için event.rawPath’ten kurar. İki yol da olduğu gibi kullanılır; stage önekleri söz konusu olduğunda bu ileride önem kazanacak. Set-Cookie başlıkları 1.0’da multiValueHeaders olarak, 2.0’da cookies dizisi olarak çıkar. İçerik tipi beş text/* tipinden biri (plain, html, css, javascript, csv) ya da bir JSON veya XML tipi değilse yanıt gövdesi isBase64Encoded: true ile base64 kodlanır; dolayısıyla text/markdown ve text/event-stream de kodlanır; handle(app, { isContentTypeBinary }) bu kontrolü değiştirir. Paylaşılan istemciler ve yapılandırma modül kapsamında durmalıdır ki tek bir cold start her route’un bedelini ödesin; bu, Init Amortizasyonuyla Kod Mimarisi yazısındaki desendir. TypeScript ile AWS Lambda Cold Start Optimizasyonu yazısındaki handler alışkanlıkları da tek bir bundle bütün route’ları taşımaya başlayınca daha çok önem kazanır.

HTTP API’de Catch-All Route#

API Gateway en spesifik route’u seçer: önce metot ve yol üzerinde tam eşleşme, sonra greedy {proxy+} değişkenli eşleşme, en sonda $default. AWS son adımı açıkça belirtir: “Routes with greedy path variables have higher priority than the $default route.” Dolayısıyla bir $default route’u yalnızca başka hiçbir şeyle eşleşmeyen istekleri alır. O var olmadan önce bu istekler gateway’in kendi {"message":"Not Found"} yanıtını alır; sonrasında Hono’nun notFound handler’ına ulaşır. Catch-all’ı yerleştirmek ilk deploy’dur ve mevcut trafiğin hiçbirini taşımaz.

// lib/orders-stack.ts (mevcut stack'in içine eklenenler)
import { Duration } from 'aws-cdk-lib'
import { HttpRoute, HttpRouteKey } from 'aws-cdk-lib/aws-apigatewayv2'
import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'
import { Runtime } from 'aws-cdk-lib/aws-lambda'
import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'

// `api` (HttpApi) ve `jwt` (HttpJwtAuthorizer) bu stack'te zaten var.
const ordersLith = new NodejsFunction(this, 'OrdersLith', {
  entry: 'src/orders/handler.ts',
  runtime: Runtime.NODEJS_24_X,
  timeout: Duration.seconds(10),
  bundling: { format: OutputFormat.ESM, minify: true, sourceMap: true },
  environment: {
    NODE_OPTIONS: '--enable-source-maps',
    POWERTOOLS_SERVICE_NAME: 'orders',
    POWERTOOLS_METRICS_NAMESPACE: 'orders',
  },
})

// Lambdalith'i hedefleyen her route'un yeniden kullandığı tek entegrasyon nesnesi.
// scopePermissionToRoute: false, fonksiyon üzerinde tek bir invoke izni bırakır.
const lith = new HttpLambdaIntegration('OrdersLith', ordersLith, {
  scopePermissionToRoute: false,
})

// Catch-all. Her açık route hâlâ ona üstün gelir.
new HttpRoute(this, 'OrdersDefaultRoute', {
  httpApi: api,
  routeKey: HttpRouteKey.DEFAULT,
  integration: lith,
  authorizer: jwt,
})

HttpLambdaIntegration varsayılan olarak payload format 2.0 ve 29 saniyelik entegrasyon zaman aşımı kullanır. scopePermissionToRoute varsayılanında bırakılırsa CDK her route için ayrı bir invoke izni ekler; CDK dokümantasyonu false değeri için şöyle der: “This is useful for reducing the AWS Lambda policy size for cases where the same AWS Lambda function is reused for many integrations.” HttpApi’nin $default route’unu oluşturan bir defaultIntegration prop’u da vardır, ancak mevcut bir API’de açık bir HttpRoute daha dar bir araçtır. Authorizer’ı orada bağlamak onu tek route’ta tutar; API seviyesinde verilen defaultAuthorizer ise kendi authorizer’ı olmadan eklenen her route’a uygulanır. CDK’nın varsayılan olarak oluşturduğu $default stage’ini koruyun; adlandırılmış bir stage’de rawPath’in stage adıyla başlamasını bekleyin. Catch-all olarak ANY /{proxy+} yerine $default kullanın. Her metodu ve kök yolu kapsar, öncelik sırasında greedy route’ların altında durur; dolayısıyla CORS preflight için sonradan ekleyeceğiniz bir {proxy+} route’u yine ona üstün gelir. NodejsFunction üzerindeki bundler ayarları için CDK TypeScript Lambda: Bundler Seçimi, construct’ın CDK uygulamasında nerede duracağı için AWS CDK Proje Yapısı yazılarına bakın.

Catch-all’ın bedeli küçüktür ama vardır. İlk deploy mevcut route’lar için risk taşımaz; ancak eskiden gateway’de duran kimliği doğrulanmış trafik (yazım hataları, kullanımdan kaldırılmış istemciler, kaldırılmış route’lara yapılan çağrılar) artık Lambda’yı çağırır ve invocation olarak faturalanır. Kimliği doğrulanmamış istekler JWT authorizer’da 401 ile durmaya devam eder. Aşağıdaki observe middleware’i bu trafiği /* olarak etiketler; böylece bu trafik talepten ayrı, bir maliyet kalemi olarak izlenebilir. Kapsamı daraltılmamış tek invoke izni Lambdalith’in amaçlanan davranışıdır; aynı zamanda route başına izinlerden daha geniş bir yetkidir, çünkü o API’deki herhangi bir route fonksiyonu çağırabilir.

Önce Retarget, Sonra Silme#

Her route üç durumdan geçer. Tek amaçlı durumda açık bir route’u ve kendi fonksiyonu vardır. Retarget edilmiş durumda açık route’unu, authorizer’ını, scope’larını, doğrulayıcısını ve throttle’ını korur; yalnızca entegrasyon artık Lambdalith’i gösterir ve gateway, route üzerinde yaşayan her şeyi uygulamaya devam eder. Silinmiş durumda açık route yoktur, yolu catch-all karşılar ve yalnızca Hono’da yeniden kurduğunuz route başına özellikler kalır.

import { HttpMethod } from 'aws-cdk-lib/aws-apigatewayv2'

// Retarget: aynı addRoutes çağrısı, yalnızca entegrasyon değişti.
// Authorizer, scope'lar ve route seviyesindeki throttle tam oldukları yerde kalır.
api.addRoutes({
  path: '/orders/{id}',
  methods: [HttpMethod.GET],
  integration: lith, // önce: getOrderIntegration
  authorizer: jwt,
  authorizationScopes: ['orders/read'],
})

CDK, route’un construct id’sini metot ve yoldan türetir; bu yüzden integration değerini değiştirmek mevcut AWS::ApiGatewayV2::Route kaynağının güncellenmesidir. İlk retarget’ten önce cdk diff çalıştırın ve çıktının route hedefinde bir güncelleme gösterdiğini, hiçbir kaynağın yeniden oluşturulmadığını doğrulayın; yeniden oluşturma, route’un hiç olmadığı kısa bir pencere anlamına gelir. Güncelleme yalnızca construct id’si değişmediği sürece geçerlidir: başlangıçta new HttpRoute(this, 'SomeId', ...) ile oluşturulmuş bir route’un id’si addRoutes ile eklenenden farklıdır; bu yüzden onu yeniden tanımlamak yerine aynı construct üzerinden retarget edin.

Retarget sonrasında eski fonksiyon trafik almaz ama stack’te kalır. Geri almak, eski entegrasyona dönen tek satırlık değişikliktir ve bu yalnızca eski fonksiyon var olduğu sürece işe yarar. Fonksiyonu bir bekleme süresi boyunca yerinde bırakın, sonra ayrı bir deploy’da kaldırın. Trafiği taşımak ile kodu silmek ayrı değişikliklerdir ve her biri kendi başına geri alınabilir.

Route’u silmek, addRoutes çağrısını kaldırmaktır. Bunu yalnızca o route’un envanter satırındaki her sütun boş ya da yeniden kurulmuş olduğunda yapın. Bazı satırlar bu şartı hiçbir zaman sağlamaz: bir REST usage-plan throttle’ı ya da catch-all’ın authorizer’ının kontrol ettiğinden farklı bir scope. Bu route’lar kalıcı olarak retarget edilmiş kalır ve bu iyi bir sonuçtur; fonksiyon sayısı yine düşer, açık route ek bir maliyet getirmez. Taşıma boyunca her durumdaki route sayısını takip edin. Uzun süre karışık durumda kalmak bu yaklaşımın başlıca başarısızlık biçimidir; ilk retarget’ten önce route başına üzerinde anlaşılmış bir son durum bunu önler.

entegrasyonu retarget et

entegrasyonu geri al

parite ve bekleme süresi geçti, route'u sil

yalnızca gateway'de olan özellik korundu

fonksiyon başına özellik gerekiyor

Tek amaçlı (kendi fonksiyonu)

Retarget edildi (açık route, hedef Lambdalith)

Silindi (catch-all karşılar)

Kalıcı olarak açık kaldı

Tek amaçlı kalır

Eski fonksiyon kaldırıldı

Bu sıranın bedeli deploy sayısıdır: route başına bir deploy, eski fonksiyonları kaldıran temizlik deploy’ları, bakım yükü olarak envanter ve kaydedilmiş fixture’lar. Karşılığında her deploy’un değişiklik kapsamı tek bir route olur ve geri alma hiçbir zaman birden fazla entegrasyona dokunmaz.

REST API’de Aynı Sıra#

Sıra REST API’de de aynıdır; beş ayrıntı farklıdır. Catch-all, kökte RestApi.root.addProxy() ile oluşturulan bir proxy kaynağıdır. LambdaRestApi’yi proxy: true ile kullanmayın: constructor’ı kök üzerindeki addResource, addMethod ve addProxy fonksiyonlarını, hata fırlatan ve proxy değerini false yapmanızı söyleyen fonksiyonlarla değiştirir; oysa strangler yaklaşımı açık kaynaklar ile proxy’nin yan yana durmasını gerektirir.

import { LambdaIntegration } from 'aws-cdk-lib/aws-apigateway'

// `api` (RestApi) her tek amaçlı fonksiyon için açık kaynaklara zaten sahip,
// örneğin: const orders = api.root.addResource('orders')
// `cognitoAuthorizer`, `bodyValidator` ve `newOrderModel` de stack'te mevcut.
const lithRest = new LambdaIntegration(ordersLith, { scopePermissionToMethod: false })
api.root.addProxy({ defaultIntegration: lithRest, anyMethod: true })

// Tek bir metodu retarget etme: aynı addMethod çağrısı, entegrasyon değişti.
// İstek doğrulayıcı ve authorizer metot üzerinde kalır.
orders.addMethod('POST', lithRest, { // önce: createOrderIntegration
  authorizer: cognitoAuthorizer,
  requestValidator: bodyValidator,
  requestModels: { 'application/json': newOrderModel },
})

İkincisi, /{proxy+} üst yolu kapsamaz. CDK’nın ProxyResource construct’ı, proxy / altına bağlandığında her metodu köke de ekler (kök o metoda zaten sahip değilse); böylece boş yol da proxy’lenir. AWS bunun önlediği hatayı belgeler: kök metodu olmayan bir API’ye gelen kök isteği Missing Authentication Token mesajıyla 403 Forbidden döner. Catch-all yerleştikten sonra GET / isteğini açıkça test edin.

Üçüncüsü, REST önce kaynağı, sonra metodu eşleştirir ve açık kardeş kaynakları proxy’nin dışında tutar. AWS’nin ifadesiyle, “a method request against a specific resource takes precedence over a method request against a generic resource at the same level of the resource hierarchy.” Strangler için sonucu şudur: herhangi bir metodu koruyan bir kaynağın, kendi yolundaki her metot için kök proxy’yi gölgelemesi beklenir. /orders kaynağı GET metodunu korurken POST /orders metodunu silerseniz, POST /orders isteğinin Hono’ya hiç ulaşmadan 403 Missing Authentication Token döndürmesini bekleyin. Bu nedenle REST’te bir kaynaktaki her metodu lithRest’e retarget edin ve kaynağı ancak bütün metotları taşındığında silin. Retarget-sonra-sil sırasını REST’te zorunlu kılan budur; HTTP API’de ise bu yalnızca daha güvenli sıradır.

Dördüncüsü, event içindeki yol. Varsayılan execute-api URL’sinde event.path stage adını taşımaz. Özel bir alan adı base path mapping kullandığında ise mapping önekinin (örneğin /v1/orders) event.path içinde görünmesini bekleyin; AWS’nin HTTP API dokümantasyonu, bir API mapping değerini görmenin yolu olarak format 1.0 ve path alanını işaret eder. Bu durumda uygulamayı app.basePath('/v1') ile bağlayın ve öneke güvenmeden önce loglanmış tek bir event ile doğrulayın.

Beşincisi, ikili (binary) yanıtlar. Hono metin olmayan gövdeleri base64 kodlar; bir REST API ise bunları istemci için yalnızca RestApi üzerinde binaryMediaTypes tanımlıysa (örneğin */*) çözer. Ayarlayın ve tek bir ikili route’u test edin. İstek doğrulayıcıları bir sonraki bölümün gösterdiği gibi Hono’ya taşınabilir. Usage-plan metot throttle’ları ve API key’leri taşınamaz; onlara bağlı route’lar retarget edilmiş kalır.

Hono’da Yeniden Kurulan Gateway Özellikleri#

Yeniden kurulacak ilk şey authorization scope’larıdır. JWT authorizer $default üzerindeyken token doğrulaması gateway’de yapılmaya devam eder ve claim’ler event içinde gelir. Koda yalnızca route başına scope kontrolü taşınır.

// src/orders/require-scope.ts
import type { APIGatewayProxyEventV2WithJWTAuthorizer } from 'aws-lambda'
import { createMiddleware } from 'hono/factory'
import type { Env } from './env'

// Cast, JWT ile yetkilendirilmiş event şeklini adlandırır; Hono'nun LambdaEvent birleşimi buna daraltılamaz.
export const requireScope = (scope: string) =>
  createMiddleware<Env>(async (c, next) => {
    const { jwt } = (c.env.event as unknown as APIGatewayProxyEventV2WithJWTAuthorizer).requestContext.authorizer
    // Claim'i token'dan okuyun, jwt.scopes'tan değil: gateway jwt.scopes'u yalnızca authorizationScopes
    // tanımlı route'larda doldurur; `$default` route'unda bu alan null gelir.
    const raw = jwt.claims.scope ?? jwt.claims.scp ?? ''
    const granted = Array.isArray(raw) ? raw : String(raw).split(' ')
    if (!granted.includes(scope)) {
      return c.json({ message: 'Forbidden' }, 403)
    }
    await next()
  })

Scope’u olan her route’un handler’ının önüne requireScope('orders/read') ekleyin. Route retarget edilmişken bu kontrol gateway’inkinin tekrarıdır; route silindiğinde ise tek kontrol budur. Middleware, requestContext.authorizer.jwt.scopes yerine token’ın scope ya da scp claim’ini okur. Gateway bu diziyi yalnızca authorizationScopes tanımlayan route’larda doldurur; $default route’u hiçbirini tanımlamadığı için orada değer null gelir. jwt.scopes üzerine kurulu bir kontrol route retarget edilmişken geçer, açık route silindiği anda ise her çağırana 403 döndürür. Claim adını loglanmış tek bir $default event’inde doğrulayın ve en az bir parite fixture’ını açık route’tan değil $default üzerinden gelen bir istekten kaydedin.

İstek doğrulama, REST doğrulayıcısı ile modelinin yerine bir Hono doğrulayıcısı koyar. @hono/zod-validator paketi json, query, param, header, form ve cookie hedeflerini doğrular; c.req.valid('json') tipli gövdeyi döndürür. Aynı yapı, kaybedecek istek doğrulaması hiç olmamış HTTP API için de çalışır. Bunun yerini aldığı handler içi doğrulama desenleri için Middy ve Zod: Tip Güvenli AWS Lambda Middleware Doğrulaması yazısına bakın.

// src/orders/routes/create-order.ts
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { z } from 'zod'
import { createOrder as createOrderCore } from '../core/create-order'
import type { Env } from '../env'
import { requireScope } from '../require-scope'

// Bu metot için REST istek doğrulayıcısının ve modelinin yerini alır.
const newOrder = z.object({
  sku: z.string().min(1),
  quantity: z.number().int().positive(),
})

export const createOrder = new Hono<Env>().post(
  '/',
  requireScope('orders/write'),
  zValidator('json', newOrder),
  async (c) => {
    const order = await createOrderCore(c.req.valid('json'))
    return c.json(order, 201)
  },
)

Throttling’in fonksiyonu koruyan bir Hono karşılığı yoktur; çünkü Hono çalıştığı anda fonksiyon zaten çağrılmış ve faturalanmıştır. Korumasını route seviyesinde bir throttle’dan ya da usage-plan metot throttle’ından alan bir route, açık route’unu korur.

Son yeniden kurulan şey route başına gözlemlenebilirliktir ve ilk geçişten önce, ilk günden devreye girer. Silinen route’lar için gateway’in route başına metrikleri catch-all’ın serisine karışır; Lambda’nın fonksiyon başına metrikleri ise Lambdalith’i bir bütün olarak tanımlar. Tek bir middleware, eşleşen route şablonunu EMF boyutu ve yapılandırılmış log alanı olarak yayınlayarak route başına görünümü geri getirir.

// src/orders/observe.ts
import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'
import { createMiddleware } from 'hono/factory'
import { routePath } from 'hono/route'
import type { Env } from './env'

const logger = new Logger()
const metrics = new Metrics()

export const observe = createMiddleware<Env>(async (c, next) => {
  logger.addContext(c.env.lambdaContext)
  const start = performance.now()
  await next()
  // routePath(c, -1) son eşleşen desendir, örneğin "/orders/:id"; asla ham yol değildir.
  // notFound'a düşen bir istek yalnızca bu middleware ile eşleşir ve "/*" raporlar;
  // böylece yalnızca catch-all'a gelen trafik kendi serisi olarak görünür kalır.
  const route = `${c.req.method} ${routePath(c, -1)}`
  metrics.addDimension('route', route)
  metrics.addMetric('requests', MetricUnit.Count, 1)
  metrics.addMetric('faults', MetricUnit.Count, c.res.status >= 500 ? 1 : 0)
  metrics.addMetric('latency', MetricUnit.Milliseconds, performance.now() - start)
  logger.info('request', { route, status: c.res.status })
  metrics.publishStoredMetrics()
})

hono/route içindeki routePath(), Hono’nun v4.8.0’da kullanımdan kaldırdığı c.req.routePath özelliğinin yerini alır. Powertools Metrics, Middy sarmalayıcısı olmadan publishStoredMetrics() ile yayınlar ve bir metriği 29 boyutla sınırlar; boyutun düşük kardinaliteli route şablonu olması ve asla ham yol olmaması için bir neden daha. Özel metriklerin seçenek olmadığı durumlarda route başına görünümlerin erişim loglarından nasıl kurulduğu için Gözlemlenebilirlik ve Service Mesh’lerin Gidişatı yazısına bakın.

Gözlemlenebilirliği yeniden kurmanın da bir bedeli vardır: sahiplik. Route başına gecikme yüzdelikleri ve hata oranları geri gelir; ancak özel metrikler her benzersiz metrik ve boyut kombinasyonu için faturalanır ve middleware, doğru tutmak zorunda olduğunuz bir koddur. Boyut adındaki bir yazım hatası seriyi ikiye böler; publishStoredMetrics() öncesinde middleware’in kendi içinde fırlayan bir istisna isteği sayımın dışında bırakır. Handler hataları için bu geçerli değildir: Hono’nun hata işleyicisi onları 500 yanıtına çevirir ve middleware onları hata olarak sayar.

Deploy Etmeden Parite Testleri#

İki test katmanı, hiçbir şey deploy edilmeden CI’da çalışır. İlki Hono’nun kendi app.request(path, init, bindings) çağrısıdır; uygulamayı standart bir Request ile süreç içinde çalıştırır ve bir Response döndürür. observe middleware’i c.env.lambdaContext okuduğu için üçüncü argüman olarak sahte bir context geçin. İkincisi parite testidir: kaydedilmiş tek bir API Gateway event’i hem eski handler’a hem handle(app) fonksiyonuna gönderilir, durum kodu ve gövde karşılaştırılır. Her retarget’in kapısı bu parite testidir.

// test/get-order.parity.test.ts
import { readFileSync } from 'node:fs'
import { handle, type LambdaEvent } from '@hono/aws-lambda'
import type { APIGatewayProxyEventV2, Context } from 'aws-lambda'
import { describe, expect, it } from 'vitest'
import { handler as legacyGetOrder } from '../src/legacy/get-order'
import { app } from '../src/orders/app'

// Eski fonksiyondan kaydedildi ve kişisel veriler temizlendi. Elle yazılmış fixture'lar,
// canlı gateway'in eklediği stage, cookie ve base64 ayrıntılarını kaçırır.
const event = JSON.parse(
  readFileSync(new URL('./fixtures/get-order.v2.json', import.meta.url), 'utf8'),
) as APIGatewayProxyEventV2
const context = { awsRequestId: 'parity', functionName: 'get-order' } as Context
const lith = handle(app)

describe('GET /orders/{id} parity', () => {
  it('returns what the single-purpose handler returned', async () => {
    const before = await legacyGetOrder(event, context)
    // İki tarafta da aynı JSON. Cast, aws-lambda tipleri ile Hono'nun kendi event arayüzleri arasında köprü kurar.
    const after = await lith(event as unknown as LambdaEvent, context)
    expect(after.statusCode).toBe(before.statusCode)
    expect(JSON.parse(after.body)).toEqual(JSON.parse(before.body ?? '{}'))
  })
})

İki taraf da aynı getOrderById fonksiyonunu çağırır; bu yüzden onu vi.mock ile bir kez stub’layın ya da bir test tablosuna yönlendirin. Parite testi, değişen tek şey olan HTTP çeviri katmanını denetler. Fixture’ları, eski fonksiyonda event’in kişisel verilerden arındırılmış bir kopyasını bir gün boyunca loglayarak kaydedin; kullanımda olan her payload format sürümü için bir fixture. Stage, cookie ve base64 tuhaflıkları yalnızca kaydedilmiş event’lerde ortaya çıkar.

API Tipine Göre Geri Alma Yolları#

Bir retarget’i geri almak, entegrasyon değişikliğinin tersidir ve eski fonksiyon var olduğu sürece tek satırlık bir değişiklik olarak kalır. İki API tipinde de birincil geri alma yolu budur; fonksiyon kaldırmanın route silmenin arkasından gelmesinin nedeni de budur.

Lambda ekiplerinin çoğunun ilk başvurduğu canary burada işe yaramaz. Ağırlıklı (weighted) bir alias, aynı fonksiyonun yayınlanmış iki sürümü arasında trafiği böler; dolayısıyla taşıma bittikten sonra Lambdalith deploy’larını korur. Eski fonksiyon ile Lambdalith arasında ise trafik bölemez, çünkü bunlar iki ayrı fonksiyondur. REST API’de stage canary release deployment’ları toplam stage trafiğinin bir yüzdesini yeni deployment’a yönlendirir; bir deployment yalnızca retarget edilmiş tek bir metot içeriyorsa canary fiilen o route’u kapsar. CDK’nın Stage L2 construct’ı canary ayarlarını açığa çıkarmaz; buna güvenmeden önce CfnStage L1 kaçış yolunu kontrol edin. HTTP API’de ise AWS’nin kendi karşılaştırmasına göre canary release özelliği hiç yoktur. Oradaki güvenlik ağı parite testi, deploy başına tek route ve hızlı geri almadır.

Tek Amaçlı Kalan Route’lar#

Fonksiyon başına bir özelliğe ihtiyaç duyan her route Lambdalith’in dışında kalır; aşağıdaki triyaj bu kararı tek bakışta gösterir. Reserved ve provisioned concurrency fonksiyon başına yapılandırılır; ikisinden birine dayanan bir route fonksiyonunu korur. Ölçekleme hızı da fonksiyon başınadır: AWS, her fonksiyonun 10 saniyede 1.000 çalıştırma ortamı örneği hızıyla bağımsız ölçeklendiğini belirtir; dolayısıyla ani trafik profili olan bir route, diğer route’ların paylaştığı hızı tüketir. Kardeş route’ların hiçbirinin ihtiyaç duymadığı bir IAM ifadesi, Lambdalith’in rolünü bir birleşime genişletir; granülerlik yazısı bu argümanı kurar ve burada route başına geçerlidir. Son olarak 6 MB senkron sınırına yakın yanıtlar döndüren ya da yanıt akışına (streaming) ihtiyaç duyan bir route ayrı kalır: Hono’nun streamHandle fonksiyonu yalnızca yanıt akışı açık Function URL’ler için belgelenmiştir; API Gateway bu kapsamın dışındadır.

reserved veya provisioned concurrency, kendi IAM'i, kendi burst'ü, streaming

hayır

usage-plan throttle, route throttle, farklı scope'lar

hayır ya da Hono'da yeniden kuruldu

Seçilen bounded context'teki route

Fonksiyon başına bir özellik gerekiyor mu?

Tek amaçlı kalır

Açık route'u Lambdalith'e retarget et

Yeniden kurmayacağınız, yalnızca gateway'de olan bir özellik taşıyor mu?

Açık route kalsın, hedef Lambdalith

Route'u sil, catch-all karşılasın

Son durum hibrittir: catch-all üzerinde tek bir Lambdalith, hâlâ onu gösteren birkaç açık route ve birkaç yalıtılmış fonksiyon. Bedeli, bakımı yapılacak ve ekibe anlatılacak iki deploy şeklidir. Kazancı ise yalıtımın tam olarak fonksiyon başına bir özelliğin gerektirdiği yerde olması, başka hiçbir yerde olmamasıdır.

Başka Bir Adapter’ın Daha İyi Oturduğu Durumlar#

Hono, bir Lambda fonksiyonunun içinde çalışan birkaç router’dan biridir ve diğerlerinden ikisinin bakımını AWS yapar. Hono bu taşımaya uyar çünkü event çevirisi ek bir sunucu olmadan süreç içinde gerçekleşir, aynı uygulama bir Function URL’nin ya da ALB’nin arkasında, hatta Lambda’nın tamamen dışında da çalışır ve app.request() route testlerinin API Gateway event fixture’ları olmadan koşmasını sağlar. Her alternatifin kazandığı, adını koymaya değer bir durum vardır.

Powertools for AWS Lambda bir HTTP Event Handler sunar: REST, HTTP API, ALB ve Function URL event’lerini çözümleyen, Standard Schema ile doğrulayan, middleware destekleyen ve REST API ile Function URL’lerde yanıt akışı yapan, AWS bakımındaki bir router. Router için metrik ve tracer middleware’leri de sunar. Ekip loglama, metrik ve izleme için zaten Powertools’u standart edinmişse ve ek bir framework istemiyorsa kazanır; yukarıdaki observe middleware’i o durumda sizin için zaten yazılmıştır. Taşınabilirliğe, daha geniş doğrulayıcı ve OpenAPI ekosistemine ve Request tabanlı test modeline değer veren okuyucu için Hono kazanır.

AWS Lambda Web Adapter, web sunucunuzu başlatan, hazır olmasını bekleyen ve her event’i varsayılan olarak 8080 portuna bir HTTP isteği olarak ileten bir Lambda extension’ı olarak çalışır; zip deploy’ları adapter layer’ını ve AWS_LAMBDA_EXEC_WRAPPER=/opt/bootstrap değişkenini ekler. Bounded context zaten bir Express, Fastify ya da Next.js sunucusu olarak varsa, ya da aynı container’ın ECS veya App Runner üzerinde de çalışması gerekiyorsa kazanır. Elinde handler’lar olan ama sunucusu olmayan bir ekibe uymaz: o ekip her hâlükârda bir router yazacak ve üstüne her cold start’ta bir extension’ın başlatılma bedelini ödeyecektir.

serverless-http, Express, Koa ve benzeri Node framework’leri için süreç içi sarmalayıcıdır; Fastify’ın resmi süreç içi yolu ise @fastify/aws-lambda paketidir; 6.4.0 kritik bir event sahteciliği güvenlik duyurusu nedeniyle kullanımdan kaldırıldığı için 6.4.1 veya üstünü kullanın. Ekip o framework’ün middleware’lerine zaten bağımlıysa ve onu iyi tanıyorsa ikisi de kazanır. Hono’nun onlara karşı savı daha küçük bir yüzeydir: bağımlılığı yoktur, Web Standartları Request ve Response nesnelerini doğrudan konuşur ve Lambda event’i ile framework’ün kendi istek nesnesi arasında ikinci bir çeviri katmanına ihtiyaç duymaz.

Deploy’dan Hatasız Geçen Yanlışlar#

Aşağıdakilerin her biri cdk deploy adımından geçer ve yalnızca trafik altında başarısız olur.

  1. Adlandırılmış HTTP API stage’i. prod adlı bir stage ile rawPath değerinin /prod ile başlamasını bekleyin; Hono’nun 2.0 işleyicisi rawPath değerini olduğu gibi kullandığı için her route 404 döner. $default stage’ini koruyun ya da son çare olarak app.basePath('/prod') çağırın.
  2. $default üzerinde authorizer ile CORS. AWS, $default route’unun tanımlamadığınız bütün metot ve route’lara gelen istekleri, OPTIONS dahil, yakaladığını belgeler ve preflight için yetkilendirme gerektirmeyen bir OPTIONS /{proxy+} route’u önerir. Ayrıca API üzerinde CORS yapılandırıldığında entegrasyondan dönen CORS başlıklarının yok sayıldığını belirtir. Gateway CORS’u ile hono/cors arasında birini seçin, ikisini birlikte kullanmayın; yetkisiz OPTIONS route’unu ekleyin.
  3. Proxy arkasında önbelleğe alınan REST Lambda authorizer’ı. Resource alanı ilk isteğin metot ARN’sini taşıyan önbellekteki bir policy, TTL süresince diğer yollara gelen istekleri reddeder. Önbellek açıkken API’nin bütün metotlarını kapsayan bir policy döndürün ya da önbelleği kapatın.
  4. Metrik boyutu olarak ham yollar. /orders/8f2c... her sipariş id’si için ayrı bir seri oluşturur ve parasını ödediğiniz boyut kardinalitesini şişirir. routePath() fonksiyonunun verdiği route şablonunu kullanın ve notFound trafiğini kendi değeriyle etiketleyin.
  5. Payload format kayması. Eski HTTP API entegrasyonları format 1.0 kullanıyor olabilir; HttpLambdaIntegration ise varsayılan olarak 2.0 kullanır. Hono ikisini de kabul eder, ancak c.env.event değerini doğrudan okuyan her kod (claim’ler, cookie’ler, requestContext) farklı bir şekil görür. payloadFormatVersion değerini açıkça sabitleyin ve her sürüm için kaydedilmiş bir fixture tutun.
  6. Kaynak policy’sinin büyümesi. Route’a daraltılmış izinler tek fonksiyon üzerinde birikir ve kaynak tabanlı policy’yi 20 KB kotasına doğru iter. Yukarıda gösterilen daraltılmamış izin seçeneklerini kullanın ve aws lambda get-policy --function-name <üretilen-ad> --query Policy --output text | wc -c komutuyla kontrol edin; buradaki ad, functionName vermediyseniz CDK’nın NodejsFunction için ürettiği addır.
  7. Node.js 24 üzerinde callback tarzı eski handler’lar. Hono içinde ya da dışında, çalışma zamanında başarısız olurlar. Çekirdek fonksiyonu çıkarırken async yapıya çevirin.
  8. Mevcut bir HttpApi üzerinde defaultAuthorizer değiştirmek. Kendi authorizer’ı olmadan eklenen her route’a uygulanır. Authorizer’ı bunun yerine açık $default HttpRoute construct’ına bağlayın.

Route Route Varsayılanının Sınırı#

Route route sıra; tek bir bounded context sizin kontrolünüzdeki tek bir API Gateway’in arkasında duruyorsa, route’lar bir authorizer’ı paylaşıyorsa ve ekip tek bir geçiş yerine her adımda geri alma istiyorsa geçerlidir. Context zaten bir sunucu uygulaması olarak varsa (Web Adapter) ya da ekip zaten Powertools kullanıyor ve onunla gelen router’ı istiyorsa (Event Handler) farklı bir araca yönelin. Kendi concurrency’sine, IAM kapsamına veya streaming’e ihtiyaç duyan bir route tek amaçlı kalır; hibrit yapı amaçlanan sonuçtur. Envanter tablosuyla başlayın; sonraki kararların hiçbiri onsuz verilemez.

Kaynaklar#

İlgili yazılar