AWS API Gateway ve CDK ile API Versiyonlama Rehberi
AWS API Gateway ve CDK ile yol tabanlı API versiyonlama: versiyon başına yaşam döngüsü kaydı, ayrı Lambda handler'ları, keşif endpoint'i ve sunset sinyalleri.
Yol tabanlı versiyonlama (/v1/users, /v2/users) ve versiyon başına tutulan açık bir yaşam döngüsü kaydı, API Gateway üzerinde tercih edilmesi gereken varsayılan yaklaşımdır. Sürekli değişen tek bir endpoint’e göre daha fazla altyapı maliyeti getirir; buna karşılık sizin takviminize göre güncelleme yapamayan istemcilerle birlikte ayakta kalan yaklaşım budur.
API’nin değişimi sürekli bir gerilim yaratır: sözleşme gelişmek zorundadır, mevcut entegrasyonlar ise çalışmaya devam etmelidir. Kurumsal müşterilerde bu gerilim keskinleşir. Sürüm döngüleri haftalık dağıtımlardan çeyreklerle ölçülen güncelleme pencerelerine kadar uzanır ve entegrasyonlar çoğu zaman kimsenin dokunmak istemediği sistemlerin içine gömülüdür. Varsayılanın CDK’daki karşılığı şudur: bir versiyon kaydı, versiyon başına Lambda handler’ları, bir keşif endpoint’i ve ileride sunset’i mümkün kılan kullanımdan kaldırma sinyalleri.
Tıkanan Yaklaşımlar#
Üç versiyonlama stratejisi tahtada makul görünür; istemciler devreye girdiğinde her biri farklı bir noktada tıkanır.
Hiç Versiyonlamamak#
Bu strateji tüm istemcilerin aynı anda güncellenebileceğini varsayar ve versiyonlama ihtiyacını tamamen ortadan kaldırır. Yalnızca tüm tüketicileri siz kontrol ettiğiniz sürece geçerlidir.
Nerede tıkanır:
- İnternete kapalı ya da sıkı regülasyona tabi ağlardaki istemciler, sizin sıkıştıramayacağınız kendi döngülerine göre güncellenir
- Güvenlik düzeltmeleri, her istemcinin hâlâ çalıştırdığı sürüme elle geri taşınmak zorunda kalır
- Eski davranışı gayriresmî olarak ayakta tutmak, adı ve testi olmayan bir gölge API’yi bakımda tutmak demektir
- Her değişiklik yayınlanmadan önce uyumluluk analizi gerektirir, dolayısıyla hız düşer
Her Ekseni Ayrı Ayrı Versiyonlamak#
Sonraki strateji endpoint’leri, header’ları ve yanıt formatlarını birbirinden bağımsız versiyonlar.
GET /v2/users?response_version=1.3
X-API-Version: 2.1
Accept: application/vnd.company.user.v4+json
Nerede tıkanır:
- Her yanıt versiyonunun her endpoint versiyonuyla test edilmesi gerekir, dolayısıyla kombinasyonlar çarparak büyür
- Bir istemcinin gerçekte neyi çalıştırdığını tek bir değer söylemez
- İstemci entegrasyonu zorlaşır, çünkü istemci artık üç değeri birden doğru seçmek zorundadır
- Dokümantasyonun üç eksenin çarpımını anlatması gerekir; hiçbir referans sayfası bunu kaldırmaz
Parmak İzine Göre Yönlendirme#
Üçüncü strateji istemciyi inceleyip (user agent, SDK header’ı, IP aralığı) isteği otomatik olarak bir versiyona yönlendirir; genellikle önde bir Lambda@Edge ya da CloudFront Function ile.
Nerede tıkanır:
- Yönlendirici tüm versiyonların önünde durur, dolayısıyla onun arızası herkesin arızasıdır
- Parmak izi bir tahmindir; HTTP kütüphanesini yükselten bir istemci sessizce versiyon değiştirebilir
- Ek atlama her isteğe gecikme ekler, yönlendirmeye hiç ihtiyacı olmayan isteklere bile
- “Bu istemci hangi versiyonu aldı” sorusunu yanıtlamak için yönlendiricinin o anki durumunu yeniden üretmek gerekir
Yol tabanlı versiyonlama bu üç kırılmayı da tek bir nedenle atlatır: versiyon URL’in içindedir, yani açıktır, loglanabilir ve istemcinin kendi seçimidir.
Yaşam Döngüsü Kayıtlı Yol Tabanlı Versiyonlama#
Varsayılan yaklaşım, yol tabanlı yönlendirmeyi versiyon başına bir yaşam döngüsü kaydı ve otomatik kullanımdan kaldırma uyarılarıyla birleştirir.
// lib/config/api-versions.ts
export interface ApiVersion {
version: string;
status: 'alpha' | 'beta' | 'stable' | 'deprecated' | 'sunset';
launchedAt: Date;
deprecatedAt?: Date;
sunsetAt?: Date;
monthlyActiveClients?: number; // Bunu takip edin!
breakingChanges: string[];
supportedFeatures: Set<string>;
}
export const API_VERSIONS: Record<string, ApiVersion> = {
v1: {
version: 'v1',
status: 'deprecated',
launchedAt: new Date('2022-01-15'),
deprecatedAt: new Date('2024-01-15'),
sunsetAt: new Date('2025-01-15'),
monthlyActiveClients: 28, // Legacy devlet müşterileri
breakingChanges: [],
supportedFeatures: new Set(['basic-crud']),
},
v2: {
version: 'v2',
status: 'stable',
launchedAt: new Date('2023-06-01'),
monthlyActiveClients: 156,
breakingChanges: [
'Tüm yanıtlarda userId yerine user_id kullanıldı',
'XML desteği kaldırıldı',
'Email alanı zorunlu hale getirildi',
],
supportedFeatures: new Set(['basic-crud', 'pagination', 'filtering']),
},
v3: {
version: 'v3',
status: 'beta',
launchedAt: new Date('2024-03-01'),
monthlyActiveClients: 42,
breakingChanges: [
'JSON:API spec\'e geçildi',
'Tüm ID\'ler UUID\'ye değiştirildi',
'Kaynaklar data özelliği altında yuvalandı',
],
supportedFeatures: new Set([
'basic-crud',
'pagination',
'filtering',
'webhooks',
'graphql',
'batch-operations'
]),
},
};
CDK Stack Kurulumu#
Stack, kaydı okur ve yayında olan her versiyonu kendi kaynak ağacına bağlar:
// lib/stacks/versioned-api-stack.ts
import { RestApi, MethodLoggingLevel, LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import { Alarm, Metric } from 'aws-cdk-lib/aws-cloudwatch';
import { Construct } from 'constructs';
export class VersionedApiStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
const api = new RestApi(this, 'MultiVersionAPI', {
restApiName: 'production-api',
// CloudWatch'u en baştan açın; versiyona özgü sorunlar aksi halde görünmez
deployOptions: {
loggingLevel: MethodLoggingLevel.INFO,
dataTraceEnabled: true, // Versiyona özgü hataları ayıklamak için şart
metricsEnabled: true,
tracingEnabled: true,
},
});
// Versiyon kontrol Lambda'sını ekle - bu kritik
const versionCheckFn = new NodejsFunction(this, 'VersionCheck', {
entry: 'src/middleware/version-check.ts',
memorySize: 256, // Fazla gerek yok
timeout: Duration.seconds(3),
environment: {
VERSIONS: JSON.stringify(API_VERSIONS),
SLACK_WEBHOOK: process.env.SLACK_WEBHOOK!, // Kullanımdan kaldırılan versiyon kullanımında uyar
},
});
// Her versiyonu ayarla
Object.entries(API_VERSIONS).forEach(([version, config]) => {
if (config.status === 'sunset') return; // Sunset versiyonlarını dağıtma
const versionResource = api.root.addResource(version);
this.setupVersionEndpoints(versionResource, config);
});
// Kritik: versiyon keşif endpoint'i
this.addVersionDiscovery(api);
// Kullanımdan kaldırılan versiyona gelen trafik, sunset'i engelleyen sinyaldir
new Alarm(this, 'DeprecatedVersionHighUsage', {
metric: new Metric({
namespace: 'API/Versions',
metricName: 'DeprecatedVersionCalls',
statistic: 'Sum',
}),
threshold: 1000,
evaluationPeriods: 1,
});
}
private setupVersionEndpoints(resource: IResource, config: ApiVersion) {
// Versiyon başına ayrı fonksiyon: daha fazla fonksiyon, ama v3'teki bir
// değişikliğin v1 istemcisine ulaşabileceği ortak kod yolu yok
const handlers = new Map<string, Function>();
// Kullanıcı endpoint'leri - çoğu kırıcı değişikliğin kaynağı
const usersResource = resource.addResource('users');
const listUsersHandler = new NodejsFunction(this, `ListUsers-${config.version}`, {
entry: `src/handlers/${config.version}/users/list.ts`,
memorySize: config.version === 'v1' ? 512 : 1024, // V1 verimsiz
timeout: Duration.seconds(29), // API Gateway maximum timeout (REST API'ler için 29 saniyeye kadar)
environment: {
TABLE_NAME: process.env.USERS_TABLE!,
VERSION: config.version,
FEATURES: [...config.supportedFeatures].join(','),
// Bu sayısız kez hata ayıklama zamanı kazandırdı
DEPLOYMENT_TIME: new Date().toISOString(),
},
bundling: {
// Versiyona özel bağımlılıklar
externalModules: [
'@aws-sdk/client-dynamodb', // AWS SDK v3 for Node.js 18+ runtime
'@aws-sdk/client-cloudwatch',
...(config.version === 'v1' ? ['xmlbuilder'] : []), // V1 XML desteği
],
},
});
usersResource.addMethod('GET', new LambdaIntegration(listUsersHandler), {
requestParameters: {
'method.request.querystring.page': config.supportedFeatures.has('pagination'),
'method.request.querystring.limit': config.supportedFeatures.has('pagination'),
'method.request.querystring.filter': config.supportedFeatures.has('filtering'),
// V3 özel parametreler
'method.request.querystring.include': config.version === 'v3',
'method.request.querystring.fields': config.version === 'v3',
},
});
// Her versiyon çağrısını takip et - bu metrik altın değerinde
listUsersHandler.metricInvocations().createAlarm(this, `HighTraffic-${config.version}`, {
threshold: 10000,
evaluationPeriods: 1,
alarmDescription: `${config.version} üzerinde yüksek trafik - ölçeklendirmeyi kontrol et`,
});
}
}
Versiyon Handler Kodu#
Her versiyonun kendi handler’ı var. Her handler’ın başındaki dönüşüm, versiyon sözleşmesinin gerçekte durduğu yerdir:
// src/handlers/v1/users/list.ts
// Legacy v1 implementasyonu; güvenlik düzeltmeleri dışında dondurulmuş durumda
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
console.log('V1 handler çağrıldı', {
path: event.path,
clientIp: event.requestContext.identity.sourceIp,
userAgent: event.headers['User-Agent'],
});
try {
// V1 sayfalamayı desteklemiyor, her şeyi döndürüyor
// V1'in tasarım kısıtı; uyumluluk için korunuyor
const users = await getAllUsers(); // Tüm kullanıcıları döndürür; sayfalama v2'de geldi
// Alan adı değişiklikleri kırıcı değişikliklerin en yaygın kaynağı
const transformedUsers = users.map(u => ({
userId: u.user_id, // V1 camelCase kullanıyor
userName: u.name,
userEmail: u.email,
createdDate: u.created_at, // Sözleşme bozulmasın diye V1'deki ad korunuyor
}));
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'X-API-Version': 'v1',
'X-API-Deprecated': 'true',
'X-API-Sunset': '2025-01-15',
'Warning': '299 - "API v1 kullanımdan kaldırıldı. Lütfen v2\'ye geçin. Kılavuzlar: https://docs.api.com/migration"',
// Finans sektörü müşterilerinin beklediği header
'X-Total-Count': transformedUsers.length.toString(),
},
body: JSON.stringify(transformedUsers),
};
} catch (error) {
// Sorun gidermek için kapsamlı hata logu
console.error('V1 handler hatası', {
error,
stack: error.stack,
event: JSON.stringify(event),
});
return {
statusCode: 500,
body: JSON.stringify({
error: 'Internal Server Error',
// V1 müşterileri tam olarak bu formatı bekliyor
errorCode: 'INTERNAL_ERROR',
timestamp: new Date().toISOString(),
}),
};
}
};
// src/handlers/v2/users/list.ts
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
// V2, V1'de hiç olmayan sayfalamayı ekliyor
const page = parseInt(event.queryStringParameters?.page || '1');
const limit = Math.min(
parseInt(event.queryStringParameters?.limit || '20'),
100 // Performans için üst sayfa boyutu
);
const metrics = {
version: 'v2',
page,
limit,
clientIp: event.requestContext.identity.sourceIp,
};
// Kullanımdan kaldırılan versiyon kullanımını takip et
if (event.headers['User-Agent']?.includes('OldSDK/1.')) {
await cloudwatch.send(new PutMetricDataCommand({
Namespace: 'API/Clients',
MetricData: [{
MetricName: 'OutdatedSDKUsage',
Value: 1,
Dimensions: [{ Name: 'Version', Value: 'v2' }],
}],
}));
}
try {
const { users, total } = await getUsersPaginated({ page, limit });
// Sayfalama bilgisini içeren V2 yanıt formatı
const response = {
data: users.map(u => ({
id: u.user_id, // userId'den değişti
name: u.name,
email: u.email,
status: u.status || 'active', // Yeni zorunlu alan
created_at: u.created_at, // Her yerde snake case
updated_at: u.updated_at,
})),
pagination: {
page,
limit,
total,
total_pages: Math.ceil(total / limit),
has_next: page < Math.ceil(total / limit),
has_prev: page > 1,
},
// İstemci gezinmesi için HATEOAS bağlantıları
_links: {
self: `/v2/users?page=${page}&limit=${limit}`,
next: page < Math.ceil(total / limit) ? `/v2/users?page=${page + 1}&limit=${limit}` : null,
prev: page > 1 ? `/v2/users?page=${page - 1}&limit=${limit}` : null,
},
};
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'X-API-Version': 'v2',
'X-RateLimit-Limit': '500',
'X-RateLimit-Remaining': await getRateLimitRemaining(event),
'Cache-Control': 'private, max-age=60', // İstenmeyen önbellekleme olmasın diye
},
body: JSON.stringify(response),
};
} catch (error) {
logger.error('V2 handler hatası', { error, metrics });
throw error; // API Gateway halletsin
}
};
// src/handlers/v3/users/list.ts
// V3: JSON:API spesifikasyonu implementasyonu
export const handler = middy(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
// Kurumsal entegrasyon için JSON:API uyumu
const params = parseJsonApiParams(event.queryStringParameters);
// Kademeli dağıtım için özellik bayrakları
const features = await getFeatureFlags('v3', event.headers['X-Client-Id']);
const { users, total, included } = await getUsersWithRelationships({
...params,
includeRelationships: params.include,
sparseFields: params.fields,
experimentalFeatures: features,
});
// JSON:API formatı - sevin ya da nefret edin
const response = {
data: users.map(u => ({
type: 'users',
id: u.id, // Tutarlılık için UUID formatı
attributes: {
name: u.name,
email: u.email,
status: u.status,
created_at: u.created_at,
updated_at: u.updated_at,
},
relationships: {
organization: {
data: { type: 'organizations', id: u.organization_id },
},
roles: {
data: u.role_ids.map(id => ({ type: 'roles', id })),
},
},
links: {
self: `/v3/users/${u.id}`,
},
})),
included: included, // İlgili kaynaklar
meta: {
pagination: {
page: params.page.number,
pages: Math.ceil(total / params.page.size),
count: users.length,
total: total,
},
api_version: 'v3',
generated_at: new Date().toISOString(),
experimental_features: [...features],
},
links: generateJsonApiLinks(params, total),
};
return {
statusCode: 200,
headers: {
'Content-Type': 'application/vnd.api+json', // JSON:API gereksinimi
'X-API-Version': 'v3',
'X-RateLimit-Limit': '1000',
'X-RateLimit-Remaining': await getRateLimitRemaining(event),
'Vary': 'Accept, X-Client-Id', // Önbellekleme için önemli
},
body: JSON.stringify(response),
};
})
.use(jsonBodyParser())
.use(httpErrorHandler())
.use(correlationIds())
.use(logTimeout())
.use(warmup());
Geçişin Zor Noktaları ve Çözümleri#
Kesinti Olmadan Alan Adı Değiştirmek#
V1’den V2’ye geçiş, userId (string) alanını user_id (UUID) olarak yeniden adlandırır. Güvenli yol iki geçiştir: önce yeni alan eskisinin yanına yazılır, eski alan ise ancak tüm istemciler V1’den ayrıldıktan sonra kaldırılır.
// migrations/v1-to-v2-user-ids.ts
export const migrateUserIds = async () => {
const BATCH_SIZE = 100;
let lastEvaluatedKey: any = undefined;
let migrated = 0;
let failed = 0;
// İlk geçiş: Yeni alan ekle
do {
const { Items, LastEvaluatedKey } = await docClient.send(new ScanCommand({
TableName: process.env.USERS_TABLE!,
Limit: BATCH_SIZE,
ExclusiveStartKey: lastEvaluatedKey,
}));
const batch = Items?.map(item => ({
PutRequest: {
Item: {
...item,
user_id: item.userId || generateUUID(), // Yeni alan
_migration: 'v1-to-v2-phase1',
_migrated_at: new Date().toISOString(),
},
},
})) || [];
if (batch.length > 0) {
try {
await docClient.send(new BatchWriteCommand({
RequestItems: { [process.env.USERS_TABLE!]: batch },
}));
migrated += batch.length;
} catch (error) {
// Logla ama durma - başarısız öğeleri yeniden deneyeceğiz
console.error('Batch başarısız', { error, batch: batch.map(b => b.PutRequest.Item.userId) });
failed += batch.length;
}
}
lastEvaluatedKey = LastEvaluatedKey;
// Sıcak partition'lardan kaçınmak için throttle
await new Promise(resolve => setTimeout(resolve, 100));
} while (lastEvaluatedKey);
console.log(`Migrasyon tamamlandı: ${migrated} başarılı, ${failed} başarısız`);
// İkinci geçiş: eski alanı kaldır, ancak V1 kullanımı sıfırlandıktan sonra
};
İstemci SDK’sında Geriye Dönük Uyumluluk#
Birden fazla versiyonu kapsayan bir SDK, üç farklı yanıt şeklini tek bir tipe indirgemek zorundadır. Switch bloğu uzundur, buna karşılık versiyon ayrıntısını uygulama kodunun dışında tutar:
// sdk/src/client.ts
export class ApiClient {
private version: string;
private warned = new Set<string>();
constructor(options: ClientOptions = {}) {
this.version = options.version || 'v2'; // Varsayılan olarak stable
if (this.version === 'v1' && !this.warned.has('deprecation')) {
console.warn(
'\x1b[33m%s\x1b[0m', // Sarı metin
'[KULLANIMDAN KALDIRILMA] API v1 2025-01-15 tarihinde kaldırılacak. ' +
'Migrasyon kılavuzu: https://docs.api.com/migration'
);
this.warned.add('deprecation');
// SDK versiyon kullanımını takip et
this.trackEvent('sdk_deprecation_warning', { version: 'v1' });
}
}
async getUsers(options?: GetUsersOptions) {
const url = this.buildUrl('users', options);
const response = await this.request(url);
// Versiyonlar arasında yanıtları normalize et
return this.normalizeUserResponse(response);
}
private normalizeUserResponse(response: any): User[] {
switch (this.version) {
case 'v1':
// V1 düz dizi döndürür
return response.map((u: any) => ({
id: u.userId,
name: u.userName,
email: u.userEmail,
createdAt: new Date(u.createdDate),
// V1'de bunlar yok
status: 'active',
updatedAt: new Date(u.createdDate),
}));
case 'v2':
// V2 sayfalı yanıt döndürür
return response.data.map((u: any) => ({
id: u.id,
name: u.name,
email: u.email,
status: u.status,
createdAt: new Date(u.created_at),
updatedAt: new Date(u.updated_at),
}));
case 'v3':
// V3 JSON:API formatı döndürür
return response.data.map((u: any) => ({
id: u.id,
name: u.attributes.name,
email: u.attributes.email,
status: u.attributes.status,
createdAt: new Date(u.attributes.created_at),
updatedAt: new Date(u.attributes.updated_at),
// V3 ilişkileri içerir
organizationId: u.relationships?.organization?.data?.id,
roleIds: u.relationships?.roles?.data?.map((r: any) => r.id) || [],
}));
default:
throw new Error(`Bilinmeyen API versiyonu: ${this.version}`);
}
}
}
İzleme ve Uyarı Kurulumu#
İzleme katmanı, versiyon kullanım örüntülerini ve performansı görünür kılar:
// lib/constructs/api-monitoring.ts
export class ApiMonitoring extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
// Gerçekten bakılan dashboard
const dashboard = new Dashboard(this, 'ApiDashboard', {
dashboardName: 'api-versions-prod',
defaultInterval: Duration.hours(3), // Faydalı olacak kadar yakın
});
// Versiyon dağılımı, sunset'in ne zaman yapılabileceğine karar veren sayıdır
dashboard.addWidgets(
new GraphWidget({
title: 'API Versiyon Dağılımı (isteklerin yüzdesi)',
left: [v1Percentage, v2Percentage, v3Percentage],
leftYAxis: { max: 100, min: 0 },
period: Duration.minutes(5),
statistic: 'Average',
// Sunset kararları için asgari kullanım eşiği
leftAnnotations: [{
label: 'Min güvenli eşik',
value: 5,
color: Color.RED,
}],
})
);
// Önemli olan metrik: versiyona göre müşteri hataları
dashboard.addWidgets(
new GraphWidget({
title: 'Versiyona Göre 4xx Hataları',
left: [
new MathExpression({
expression: 'RATE(m1)',
usingMetrics: {
m1: v1Errors,
},
label: 'V1 Hata Oranı',
color: Color.RED,
}),
// v2, v3 için benzer
],
})
);
// Kullanımdan kaldırma uyarısı etkinliği
const deprecationAlarm = new Alarm(this, 'V1StillHighUsage', {
metric: v1Percentage,
threshold: 10,
evaluationPeriods: 3,
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
alarmDescription: 'V1 hâlâ %10\'un üzerinde - sunset ertelenmeli mi?',
treatMissingData: TreatMissingData.NOT_BREACHING,
});
deprecationAlarm.addAlarmAction(
new SnsAction(Topic.fromTopicArn(this, 'AlertTopic', process.env.ALERT_TOPIC_ARN!))
);
}
}
Çoklu Versiyonun Kısıtları#
Sunset Lansmandan Daha Zordur#
Kullanımdan kaldırılan bir versiyon, duyuru yapıldıktan çok sonra bile trafik almaya devam eder ve nedenleri sizin kontrolünüzde değildir:
- Kamu ve regülasyona tabi müşteriler dağıtımlarını en az bir çeyrek öncesinden planlar
- IoT ve gömülü cihazlar URL’leri firmware ile birlikte gönderir, yani endpoint sürüm sürecinden uzun yaşar
- Legacy sistemlerde entegrasyonlar sabit kodlanmıştır ve istemci tarafında artık sahibi yoktur
Kullanımdan kaldırılan versiyonun bakımını, kullanımı gerçekten sıfırlanana kadar süren sabit bir gider kalemi olarak bütçeleyin.
Test Matrisi Çarparak Büyür#
Kırıcı değişiklikler test yükünü toplayarak değil çarparak artırır. Yayında üç API versiyonu, üç SDK kuşağı ve dört yanıt formatı kapsanacak 36 kombinasyon eder; her yeni versiyon bu çarpımın tamamını ölçekler. Kaç versiyonu yayında tutabileceğinizin pratik sınırını genellikle pipeline süresi belirler.
Dokümantasyon Kayması Gizli Bağımlılık Yaratır#
Eski bir versiyonun dokümantasyonu güncellenmeyi bıraktığında, istemciler hiç kimsenin yazıya dökmediği davranışlara bağımlı hale gelir. Bunun sonucu şudur:
- Bir “hata düzeltmesi” ile bozulan, belgelenmemiş yanıtlara bağımlılık
- Eski davranışı geri getirmek için sonradan eklenen özellik bayrakları
- Spesifikasyonda hiç görünmeyen legacy semantik için ek geliştirme yükü
Versiyon Keşfi Kritiktir#
// İstemciye neyin var olduğunu, neyin stable, neyin kalkacak olduğunu söyleyen tek endpoint
app.get('/api', (req, res) => {
res.json({
versions: {
v1: {
status: 'deprecated',
sunset_date: '2025-01-15',
docs: 'https://docs.api.com/v1',
migration_guide: 'https://docs.api.com/v1-to-v2',
},
v2: {
status: 'stable',
docs: 'https://docs.api.com/v2',
},
v3: {
status: 'beta',
docs: 'https://docs.api.com/v3',
breaking_changes: 'https://docs.api.com/v3-breaking-changes',
},
},
current_stable: 'v2',
recommended: 'v2',
your_version: detectVersion(req), // Müşterinin kullandığı
});
});
Operasyonel Değerlendirmeler#
Yayında olan her versiyonun tekrar eden bir maliyeti vardır ve bu maliyetlerin tamamı altyapı faturasında görünmez:
- Altyapı: Yayındaki her versiyon için bir Lambda kümesi ve bir API Gateway kaynak ağacı
- Geliştirme: Versiyonlar arası bir özellik, her versiyon için ayrı ayrı yazılır ve gözden geçirilir
- Test: Pipeline her yayından önce versiyon matrisinin tamamını çalıştırır
- Dokümantasyon: Her versiyonun kendi referansı ve bir sonrakine geçiş kılavuzu gerekir
- Destek: Versiyon karışıklığı sürekli bir bilet kategorisidir; büyük kısmını keşif endpoint’i soğurur
Uygulama Önerileri#
- İlk sürümden itibaren versiyonlamayı tasarlayın - İstemciler sözleşmeye bağımlı hale geldikten sonra yola versiyon segmenti eklemek, o segmenti baştan ayırmaktan çok daha pahalıdır
- Kırıcı değişiklikleri gruplayın - İlişkili değişiklikleri tek versiyonda toplayın; böylece bir çeyreklik küçük düzenleme tek bir versiyon artışına dönüşür
- Geçiş araçlarını otomatikleştirin - İstemci geçiş script’ini kullanımdan kaldırma duyurusu çıkmadan önce yazın
- Sunset takvimini en yavaş istemciye göre kurun - Kurumsal ve kamu tarafındaki tüketicilerin geçiş penceresi çeyreklerle ölçülür
- Kullanım takibini ilk günden açın - Versiyon başına kullanım, sunset kararını savunulabilir kılan tek kanıttır
Önerilen CDK Pattern’i#
Yeni başlıyorsanız, bu yapıyı kullanın:
/api
/v1
/users
/orders
/internal/health
/v2
/users
/orders
/internal/health
/versions (keşif endpoint'i)
/health (versiyondan bağımsız)
Lambda kodunuzu versiyona göre organize edin:
/src
/handlers
/v1
/users
/orders
/v2
/users
/orders
/shared
/database
/auth
/utils
Sonuç#
Yaşam döngüsü kayıtlı yol tabanlı versiyonlama, tüketiciler sizin kontrol etmediğiniz takvimlerle dağıtım yapıyorsa ve sözleşme değişmeye devam edecekse geçerli varsayılandır. Tek tüketicisi sizinle aynı pipeline’dan çıkan bir iç API için yanlış varsayılandır; orada sürekli değişen tek bir endpoint ve tüketici odaklı sözleşme testleri hem daha ucuzdur hem kırılmayı daha erken yakalar. Ekleme niteliğindeki değişiklikler için de yanlış varsayılandır: opsiyonel yeni bir alan versiyon gerektirmez, buna rağmen versiyon çıkarmak istemcilere versiyon numaralarını ciddiye almamayı öğretir.
Geri kalan her şeyi iki karar belirler: versiyonun URL’in neresinde duracağı ve kaydın her versiyon hakkında ne tutacağı. Handler’lar, alarmlar ve geçiş script’leri bu ikisinden türer; o yüzden ilk versiyon yayına çıkmadan karara bağlayın.
Kaynaklar#
- Implement path-based API versioning by using custom domains in Amazon API Gateway (yeni sekmede açılır) - Farklı API sürümlerine özel alan adı yol önekleri üzerinden yönlendirme yapan AWS Prescriptive Guidance çözüm deseni.
- Deploy REST APIs in API Gateway (yeni sekmede açılır) - REST API’lerin aşamalara dağıtımını ve sürüm yaşam döngüsü yönetimini anlatan resmi API Gateway kılavuzu.
- REL03-BP03 Provide service contracts per API - AWS Well-Architected Framework (yeni sekmede açılır) - Tüketicilerin kendi hızlarında taşınmasına olanak tanıyan sözleşme sürümleme stratejilerine ilişkin Well-Architected rehberi.
- AWS CDK versioning - AWS Cloud Development Kit (AWS CDK) v2 (yeni sekmede açılır) - Alpha/beta yapı yaşam döngüsü ve geriye dönük uyumluluk garantilerini içeren resmi CDK sürümleme semantiği.
- Best practices for developing and deploying cloud infrastructure with the AWS CDK (yeni sekmede açılır) - Üretim stack’leri için yapı yeniden kullanımı, yapılandırma desenleri ve dağıtım stratejilerini kapsayan CDK en iyi uygulamaları.
- Best practices for using the AWS CDK in TypeScript to create IaC projects (yeni sekmede açılır) - Yapılar için sürüm kontrolü ve yayın yönetimi dahil TypeScript’e özgü CDK desenleri üzerine AWS Prescriptive Guidance.
- Deploying Lambda functions with AWS CDK (yeni sekmede açılır) - TypeScript ile AWS CDK kullanarak Lambda destekli API’lerin dağıtımını anlatan resmi eğitim.
İlgili yazılar
Dev, staging ve production ortamlarında Lambda Layer versiyonlarını AWS CDK ile yönetmek için pratik yaklaşımlar: otomatik pipeline ve rollback dahil.
aws · lambda · aws-cdk +4
Lambda proxy yerine S3 presigned URL ile büyük dosya yükleme: eksiksiz CDK stack, iki handler, tarayıcı tarafı ve güvenlik ödünleşimleri.
lambda · aws-cdk · aws +2
Lambda API'si ile OpenAPI sözleşmesini senkron tutmak için spec'i Zod şemalarından üretin ve bu üretimi CDK dağıtımına bağlayın.
api-gateway · aws-cdk · lambda +4
İç 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.
aws · aws-cdk · lambda +4
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ı.
aws · aws-cdk · lambda +4