Katmanı Private API Gateway ile Kurmak
Private REST API, onu açan resource policy, route bazlı AWS_IAM yetkileri, iki CDK stack'i ve Node 22 Lambda'dan çağrıyı imzalamak.
Katmanı seçtiniz (101. bölüm) ve neyi taşıyacağını karara bağladınız (102. bölüm). Bu bölüm kurulum: private REST API, onu erişilebilir yapan resource policy, route bazına daraltılmış AWS_IAM yetkileri, sahiplik sınırına göre bölünen iki CDK stack’i, Node 22 Lambda’dan atılan SigV4 çağrısı ve bütün bunların çalışıp çalışmayacağına karar veren, VPC’ye bağlı çağıranlarla ilgili operasyonel gerçekler. Desenin merkezindeki değişiklik küçük kalıyor: çağıranın execution role’ü, fonksiyon ARN’i üzerinde lambda:InvokeFunction yerine bir route ARN’i üzerinde execute-api:Invoke taşır ve çağrılan fonksiyonun ARN’i kendi stack’inden hiç dışarı çıkmaz. Önce private endpoint kısıtı geliyor, çünkü API tipini her şeyden önce o belirliyor.
Private endpoint yalnızca REST’te var
API Gateway geliştirici rehberi bu konuda net; Considerations for private APIs bölümünde şöyle diyor: “Only REST APIs are supported” (Private REST API’ler).
Bu tek cümle, en yaygın maliyet itirazını ortadan kaldırır. HTTP API’ler istek başına daha ucuzdur ve topluluk tartışmalarında sürekli önerilir; ama HTTP API’nin private endpoint tipi yoktur. HTTP API’lerde olan şey VPC Link V2’dir ve ters yönde çalışır. Bu link, public bir API Gateway’in sizin VPC’nize uzanıp bir ALB’ye, NLB’ye veya Cloud Map servisine ulaşmasını sağlar (VPC link V2). Yani private bir entegrasyondur, private bir endpoint değil. API’nin kendisi internetten erişilemez olacaksa bunu yapabilen tek tür REST’tir ve REST tarifesini ödersiniz.
Aynı sayfadaki üç not daha tasarım defterinize girmeli: private API’ler TLS 1.2’yi taban olarak dayatır, HTTP/2 istekleri HTTP/1.1’e zorlanır ve private bir API’yi yalnızca IPv4 ile sınırlayamazsınız, çünkü yalnızca dualstack destekleniyor. Bir not da endpoint tipleri sayfasından geliyor: private API endpoint’leri tüm header adlarını olduğu gibi geçirir (API endpoint tipleri). Edge-optimized endpoint’ler bu adları büyük harfe çevirir; yani sessizce bu büyük harfe bağımlı kalmış bir servis, private API’nin arkasına taşınınca bozulabilir.
Resource policy açma anahtarıdır
Yeni oluşturulmuş bir private API erişilebilir değildir. AWS bunu Create a private API sayfasının 3. adımında açıkça söyler: “Your current private API is inaccessible to all VPCs” (private API oluşturma). Bir resource policy VPC’nizi veya endpoint’inizi adıyla anana kadar her çağrı başarısız olur. Bu hata yeterince DNS hatasına benzediği için insanlar bir saat boyunca Route 53 kurcalar.
Kanonik biçim, aws:SourceVpce üzerinden kurulan izin-ver-sonra-reddet kalıbıdır (resource policy örnekleri):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": ["execute-api:/*"]
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": ["execute-api:/*"],
"Condition": {
"StringNotEquals": { "aws:SourceVpce": "vpce-1a2b3c4d" }
}
}
]
}
execute-api:/* kısaltması, API Gateway politikayı kaydederken tam ARN’e açılır. Route düzeyindeki biçim execute-api:/{stage}/{METHOD}/{path} şeklindedir; bir statement’ı API’nin tamamı yerine tek bir route’a daraltmanın yolu budur.
AWS_IAM ile route bazlı yetki
Bir method’da AWS_IAM yetkilendirmesi açıkken çağıran, isteği SigV4 ile imzalar (servis adı execute-api) ve API Gateway çağıranın identity policy’sini değerlendirir. Yetki şuna benzer:
execute-api:Invoke on arn:aws:execute-api:{region}:{account}:{apiId}/{stage}/POST/orders
Yetkilendirme hikâyesinin tamamı bu ve yeni bir politika diline ihtiyaç duymaz. Çağrılan servis arkadaki Lambda’yı baştan yazabilir, adını değiştirebilir veya tamamen kaldırabilir; hiçbir tüketici politikası değişmez, çünkü hiçbir tüketici politikası fonksiyondan bahsetmiyordu.
Burada açıkça söylenmesi gereken bir tuzak var, çünkü hata mesajı başka yeri işaret ediyor. Resource policy bir AWS principal’ını (örneğin bir role ARN’ini) adıyla anıyorsa ve herhangi bir method hâlâ NONE yetkilendirmesindeyse, API Gateway User: anonymous is not authorized to perform: execute-api:Invoke döner. Create a private API sayfasının sorun giderme bölümü çözümü söylüyor: her method AWS_IAM kullanmalı. Bunu method başına değil, CDK’da bir kez varsayılan olarak ayarlayın; böylece yetkilendirmesiz yeni bir route eklenemez.
CDK stack’leri
İki stack, çünkü mesele tam da sahiplik sınırı. Platform stack’i VPC’yi ve tek bir paylaşılan interface endpoint’i tutar. AWS’in kendi best practice’i birden çok private API için tek bir VPC endpoint kullanmaktır. Sabit maliyeti tüm yapıya yayan şey de budur.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
export class PlatformNetworkStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
public readonly apiEndpoint: ec2.InterfaceVpcEndpoint;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Yalnızca isolated subnet: doğu-batı trafiği NAT gateway istemez.
// Diğer AWS API'lerine (DynamoDB, SQS) çağrılar kendi endpoint'lerini ister.
this.vpc = new ec2.Vpc(this, 'Vpc', {
maxAzs: 2,
natGateways: 0,
subnetConfiguration: [
{ name: 'isolated', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 },
],
});
// Endpoint ENI'si çağıran Lambda'lardan 443'ü kabul etmeli.
// securityGroups'u atlarsanız CDK open: true varsayar ve 443'ü tüm
// VPC CIDR'ına açar. Buradaki tanım onu çağıran SG'ye daraltıyor.
const endpointSg = new ec2.SecurityGroup(this, 'ApiEndpointSg', {
vpc: this.vpc,
description: 'execute-api interface endpoint',
allowAllOutbound: false,
});
endpointSg.addIngressRule(
ec2.Peer.ipv4(this.vpc.vpcCidrBlock),
ec2.Port.tcp(443),
'HTTPS from in-VPC callers',
);
// Tek endpoint, çok sayıda private API. Katmanın sabit maliyeti budur.
this.apiEndpoint = new ec2.InterfaceVpcEndpoint(this, 'ExecuteApiEndpoint', {
vpc: this.vpc,
service: ec2.InterfaceVpcEndpointAwsService.APIGATEWAY,
privateDnsEnabled: true,
securityGroups: [endpointSg],
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
});
}
}
Servis stack’i API’yi ve handler’ı tutar. En işe yarar CDK detayı burada: endpointConfiguration.vpcEndpoints resource policy’yi yazmaz. CDK referansı bu prop’u “A list of VPC Endpoints against which to create Route53 ALIASes” diye tanımlar (EndpointConfiguration). Politika ya policy prop’undan gelir ya da grantInvokeFromVpcEndpointsOnly() metodundan; dokümantasyon bu metodu “a resource policy that only allows API execution from a VPC Endpoint” ekliyor diye tanımlar (RestApi). İki prop, iki ayrı iş. Doğru kurulmuş görünen bir API’nin her çağrıda 403 dönmesinin sebebi, yalnızca birincisini ayarlamaktır.
vpcEndpoints’in yaptığı iş daha dar. Alttaki kaynakta vpcEndpointIds alanını ayarlıyor, yani endpoint’i API ile ilişkilendiriyor; alias’ı da oradan API Gateway üretiyor. aws-cdk-lib’i 2.179.0 veya sonrasına sabitleyin; grantInvokeFromVpcEndpointsOnly() ilk orada çıkıyor. Daha eski bir sürümde sebebini söylemeyen çıplak bir TypeScript hatası alırsınız.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as logs from 'aws-cdk-lib/aws-logs';
interface OrdersServiceStackProps extends cdk.StackProps {
readonly apiEndpoint: ec2.IInterfaceVpcEndpoint;
}
export class OrdersServiceStack extends cdk.Stack {
public readonly api: apigateway.RestApi;
constructor(scope: Construct, id: string, props: OrdersServiceStackProps) {
super(scope, id, props);
const handler = new nodejs.NodejsFunction(this, 'OrdersHandler', {
entry: 'src/orders/handler.ts',
runtime: lambda.Runtime.NODEJS_22_X,
timeout: cdk.Duration.seconds(10),
});
this.api = new apigateway.RestApi(this, 'OrdersApi', {
restApiName: 'orders',
endpointConfiguration: {
types: [apigateway.EndpointType.PRIVATE],
// Yalnızca Route53 ALIAS kaydı. Resource policy'yi OLUŞTURMAZ.
vpcEndpoints: [props.apiEndpoint],
},
// Her method'a uygulanır; yetkilendirmesiz route canlıya çıkamaz.
defaultMethodOptions: {
authorizationType: apigateway.AuthorizationType.IAM,
},
deployOptions: {
stageName: 'v1',
tracingEnabled: true,
metricsEnabled: true,
// Direct invoke'un ifade edemediği throttling sözleşmesi.
throttlingRateLimit: 200,
throttlingBurstLimit: 400,
accessLogDestination: new apigateway.LogGroupLogDestination(
new logs.LogGroup(this, 'OrdersAccessLogs', {
retention: logs.RetentionDays.ONE_MONTH,
}),
),
accessLogFormat: apigateway.AccessLogFormat.jsonWithStandardFields(),
},
});
// API'yi erişilebilir yapan satır. Bu olmadan her VPC reddedilir.
this.api.grantInvokeFromVpcEndpointsOnly([props.apiEndpoint]);
this.api.root
.addResource('orders')
.addMethod('POST', new apigateway.LambdaIntegration(handler));
}
}
Çağıranın stack’i yetkiyi route üzerinde verir ve ARN’i arnForExecuteApi üretir:
import * as iam from 'aws-cdk-lib/aws-iam';
const checkout = new nodejs.NodejsFunction(this, 'CheckoutHandler', {
entry: 'src/checkout/handler.ts',
runtime: lambda.Runtime.NODEJS_22_X,
vpc: props.vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
environment: {
ORDERS_URL: props.ordersApi.urlForPath('/orders'),
},
});
// Fonksiyon ARN'i üzerinde değil, route üzerinde yetki.
checkout.addToRolePolicy(new iam.PolicyStatement({
actions: ['execute-api:Invoke'],
resources: [props.ordersApi.arnForExecuteApi('POST', '/orders', 'v1')],
}));
Planlamanız gereken bir dağıtım detayı: bir VPC endpoint’i private API ile ilişkilendirmek veya ilişkiyi kaldırmak API’nin yeniden deploy edilmesini gerektirir ve DNS yayılımı birkaç dakika sürer. Platform stack’ini servis stack’lerinden önce sıraya koyun.
Node 22 Lambda’dan çağrıyı imzalamak
Çağıran, SigV4 ile execute-api servis adına imza atar. lambda değil, apigateway değil. Yanlış servis adı, yardımcı olmayan bir mesajla gelen 403 demektir.
import { SignatureV4 } from '@smithy/signature-v4';
import { HttpRequest } from '@smithy/protocol-http';
import { Sha256 } from '@aws-crypto/sha256-js';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
const ordersUrl = new URL(process.env.ORDERS_URL!);
// Execution environment başına bir kez kurulur, invoke'lar arasında yeniden kullanılır.
const signer = new SignatureV4({
service: 'execute-api',
region: process.env.AWS_REGION!,
credentials: defaultProvider(),
sha256: Sha256,
});
export const handler = async (event: { cartId: string }) => {
const body = JSON.stringify({ cartId: event.cartId });
const request = new HttpRequest({
protocol: ordersUrl.protocol,
hostname: ordersUrl.hostname,
path: ordersUrl.pathname,
method: 'POST',
headers: {
host: ordersUrl.hostname, // imzalanan header'lar arasında zorunlu
'content-type': 'application/json',
},
body,
});
const signed = await signer.sign(request);
// Global fetch; Node 22'de ayrı bir HTTP istemcisi gerekmiyor.
const response = await fetch(ordersUrl, {
method: signed.method,
headers: signed.headers,
body: signed.body,
});
if (!response.ok) {
throw new Error(`orders returned ${response.status}: ${await response.text()}`);
}
return response.json();
};
İmzaladığınız gövde ile gönderdiğiniz gövde bayt bayt aynı olmalı. İki kez serialize etmek veya bir istemci kütüphanesinin payload’ı yeniden kodlamasına izin vermek, payload hash uyuşmazlığı ve bir 403 daha üretir.
Çağıranlar VPC’ye bağlı olmak zorunda
Çağıran, interface endpoint’i VPC’nin içinden çözer ve ona oradan ulaşır; yani bir VPC’ye bağlı olması gerekir. Bunu API Gateway’i seçmenin bedeli hanesine yazmayın. Bir Lambda çağıranı VPC Lattice’e de bir VPC’den ulaşmak zorunda: ya service network’e bağlanmış bir VPC’den ya da service network tipi bir VPC endpoint’i barındıran bir VPC’den. İki endpoint de VPC’nin etrafına duvar örmüyor. Trafik dışarıdan da gelebilir: peering, Transit Gateway, Direct Connect veya VPN üzerinden. Private API Gateway de şirket içi çağıranları aynı şekilde Direct Connect üzerinden kabul ediyor. Ayakta kalan kural göründüğünden dar: hangi taşımayı seçerseniz seçin, bir Lambda çağıranı VPC’ye bağlıdır. Bu, bu tasarımın kestiği bir vergi değil; AWS’te private doğu-batı yapmanın bir özelliği.
Refleks itiraz, VPC’ye bağlı Lambda’ların yavaş cold start yaptığıdır. Bu, Hyperplane değişikliğinden beri doğru değil. Güncel dokümantasyon şunu söylüyor: ENI, invoke anında değil, fonksiyon oluşturulduğunda veya VPC ayarları güncellendiğinde yaratılır. Her Hyperplane ENI’si 65.000 bağlantıya kadar destekler ve bu kapasite, aynı subnet ile security group kombinasyonunu kullanan fonksiyonlar arasında paylaşılır (Lambda VPC ağı). 2019’daki duyuru, bir X-Ray izinde cold start’ın 14,8 saniyeden 933 milisaniyeye düştüğünü kaydetmişti (iyileştirilmiş VPC ağı). Bu, mitin neden öldüğüne dair tarihsel bağlamdır; plan yapılacak bir benchmark değil.
Bundan üç operasyonel sonuç çıkar. Yeni oluşturulan bir VPC fonksiyonu, ENI’si hazır olana kadar Pending durumunda kalır ve bu dakikalar sürebilir; soğuk bir pipeline’da buna yer ayırın. Fonksiyonlarınız arasında tek bir subnet artı security group kombinasyonunu tekrar kullanmak, Lambda’nın yeni ENI üretmek yerine mevcutları paylaşmasını sağlar.
Üçüncüsü tam da seyrek kullanılan bir iç servis katmanını vurur. Bir VPC fonksiyonu 14 gün boyunca boş kalırsa Lambda kullanılmayan Hyperplane ENI’sini geri alır ve fonksiyonu Inactive durumuna geçirir. Sonraki invoke başarısız olur ve fonksiyon yeniden Pending durumuna girer. Nadiren çağrılan bir iç endpoint tam olarak bu trafik profilindedir; sessiz geçen iki haftanın ardından gelen ilk çağrıyı, çağıranın retry etmesi gereken bir hata olarak kabul edin.
Sık yapılan hatalar
- Daha ucuz diye HTTP API’ye uzanmak. HTTP API’lerin private endpoint tipi yok.
EndpointType.PRIVATEile REST API kullanın ve milyon başına $3,50 için bütçe ayırın. endpointConfigurationiçindevpcEndpointsayarlayıp resource policy’nin de onunla geldiğini varsaymak. O prop yalnızca Route 53 alias’ı üretir.grantInvokeFromVpcEndpointsOnly()çağırın veyapolicyverin.- Resource policy’de bir IAM role’ünü
Principalolarak yazıp bir method’uNONEyetkilendirmesinde bırakmak. Sonuç:User: anonymous is not authorized to perform: execute-api:Invoke.AWS_IAM’idefaultMethodOptionsiçine koyun. - Interface endpoint’i varsayılan security group’ta bırakmak. ENI’sinin, çağıranlardan 443’ü kabul eden açık bir kurala ihtiyacı var.
- Yanlış servis adıyla imzalamak. Doğrusu
execute-api. - Her API için ayrı bir VPC endpoint kurmak. AWS’in best practice’i tek endpoint, çok private API; API başına kurmak sabit maliyeti N ile çarpar.
- 256 KB’lık asenkron payload limitini tekrarlamak. Güncel kota asenkronda 1 MB, senkronda her yönde 6 MB.
- VPC’ye bağlı Lambda’ların yavaş cold start yaptığını varsaymak. 2018 blog yazılarını değil, güncel ENI dokümanını referans alın.
Katman ayakta ve hangi servisin çağırdığını kanıtlıyor. Kimin adına çağırdığı ise ayrı bir soru. 104. bölüm onu cevaplıyor ve taşıma katmanının gerçekte neyi şifrelediğini anlatıyor.
Kaynaklar
- Private REST APIs in API Gateway - Taşıyıcı sayfa: “Only REST APIs are supported”, çok sayıda API için tek endpoint ve TLS 1.2 ile HTTP/1.1 notları.
- Set up VPC links V2 in API Gateway - HTTP API’deki private hikâyesinin bir private endpoint değil, gateway’den VPC’ye giden private bir entegrasyon olduğunu gösterir.
- API endpoint tipleri - Private API endpoint’leri tüm header adlarını olduğu gibi geçirir; edge-optimized endpoint’ler ise büyük harfe çevirir.
- Create a private API - “Your current private API is inaccessible to all VPCs” ve her method’da AWS_IAM’i zorunlu kılan anonymous principal sorun giderme kaydı.
- API Gateway resource policy examples - Kanonik izin-ver-sonra-reddet
aws:SourceVpcepolitikası ve route düzeyi kısaltma. - CDK: interface EndpointConfiguration -
vpcEndpoints“A list of VPC Endpoints against which to create Route53 ALIASes” olarak tanımlıdır; resource policy’yi oluşturmaz. - CDK: class RestApi - Bu deseni taşıyan iki metot:
grantInvokeFromVpcEndpointsOnly()vearnForExecuteApi(). - Lambda VPC networking - Hyperplane ENI’sinin fonksiyon oluşturma veya güncelleme anında yaratılması, ENI başına 65.000 bağlantı ve seyrek kullanılan bir iç servisi vuran 14 günlük boşta kalma geri alımı.
- AWS Lambda için iyileştirilmiş VPC ağı - 2019 duyurusu: bir VPC cold start’ının 14,8 saniyeden 933 milisaniyeye düşmesi. Yavaş cold start mitinin neden öldüğüne dair tarihsel bağlam.
Serverless İç Servis Katmanı: Kurmadığınız, Oluşturduğunuz Mesh
Çok takımlı bir serverless yapıda service mesh benzeri doğu-batı iletişimini private API Gateway üzerine kurmak. Kimlik, şifreleme, wire format'lar, maliyet ve endüstrinin gerçekte nereye gittiği.
Bu Serideki Tüm Yazılar
İlgili yazılar
İç servis katmanı kurmadan önce kurup kurmayacağınıza karar verin. Katmanın çağrı başına maliyeti, VPC Lattice'in kazandığı hacim ve direct invoke'un hâlâ kazandığı an.
Private REST API gRPC’yi yapısal olarak taşıyamaz ve gRPC konuşan her AWS yüzeyi Lambda hedeflerini dışlar. gRPC’den neyi tutmalı, neyi bırakmalı.
SigV4 hangi servisin çağırdığını kanıtlar, kimin adına çağırdığını değil. Doğrulanmış özneyi nasıl taşırsınız ve taşıma katmanı gerçekte neyi şifreler.
Aynı hesapta resource policy ile çağıranın identity policy'si bir OR'dur. Hesaplar arası çağrıda AND'e döner ve sessizlik reddeder. Veri sınırında bunun sonuçları.
API Gateway throttle kovasını ön kapınızla paylaşır, Lambda entegrasyonunu asla retry etmez ve kendi üzerinden geçen döngüleri göremez. Neyi yeniden kurmalısınız.