İçeriğe atla

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.

Ayhan Sipahi Ayhan Sipahi

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#

  1. İ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
  2. 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
  3. Geçiş araçlarını otomatikleştirin - İstemci geçiş script’ini kullanımdan kaldırma duyurusu çıkmadan önce yazın
  4. Sunset takvimini en yavaş istemciye göre kurun - Kurumsal ve kamu tarafındaki tüketicilerin geçiş penceresi çeyreklerle ölçülür
  5. 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#

İlgili yazılar