Skip to content

CloudFormation'un 500 Kaynak Sınırını Aşmak: Büyük Ölçekli Altyapı için Pratik Stratejiler

Nested stack'ler, cross-stack referanslar, SSM Parameter Store ve microstack mimarisi kullanarak CloudFormation'un 500 kaynak sınırını aşmak için kanıtlanmış stratejiler. TypeScript CDK örnekleri ve karar çerçeveleri ile.

Özet

AWS CloudFormation'un stack başına 500 kaynak sınırı, production-grade altyapı geliştirirken sıkça karşılaşılan sabit bir kısıtlamadır. Bu sınırla çalışmak bana gösterdi ki, nested stack'ler, cross-stack referanslar, SSM Parameter Store ve microstack mimarisi arasındaki seçim operasyonel tercihlere, deployment patternlerine ve takım yapısına bağlıdır. Bu yazıda beş stratejiyi TypeScript CDK örnekleri, karar çerçeveleri ve bu sınırı aşan altyapıları yeniden yapılandırırken öğrendiğim derslerle paylaşıyorum.

500 Kaynak Sınırını Anlamak

CloudFormation her stack'i maksimum 500 kaynakla sınırlandırıyor; service quota'larla artırılamayan sabit bir limit. Bu kısıtlama CloudFormation'un dahili işleme gereksinimleri nedeniyle var: bağımlılık grafiği karmaşıklığı, rollback operasyon yönetimi ve state senkronizasyonu kaynak sayısı arttıkça exponential olarak artıyor.

Takımlar Bu Limiti Ne Zaman Vuruyor?

Serverless Microservices: Tek bir Lambda function 8-12 CloudFormation kaynağı oluşturuyor:

typescript
// Single Lambda function creates multiple resourcesconst userHandler = new NodejsFunction(this, 'UserHandler', {  entry: 'src/handlers/user.ts'});
// Oluşturulanlar:// - AWS::Lambda::Function (1)// - AWS::IAM::Role (1)// - AWS::IAM::Policy (1-2)// - AWS::Logs::LogGroup (1)// - AWS::Lambda::Version (1)// - DLQ ile: AWS::SQS::Queue (1)// - Alarm'larla: AWS::CloudWatch::Alarm (2-4)// Toplam: Lambda başına 8-12 kaynak
// 60 Lambda function = 480-720 kaynak sadece compute layer için// DynamoDB table'lar, API Gateway, SQS queue'lar, EventBridge rule'lar ekle = limit aşıldı

Production vs Development Farkı: 200 kaynaklı development environment'lar sorunsuz çalışıyor, ama production redundancy, monitoring ve multi-AZ configuration ekliyor:

typescript
// Development: 178 kaynakconst devStack = {  lambdas: 20,  // 160 kaynak  tables: 5,  // 5 kaynak  queues: 3,  // 3 kaynak  apis: 2,  // 10 kaynak  total: 178};
// Production: 505 kaynak (LİMİT AŞILDI)const prodStack = {  lambdas: 20,  // 160 kaynak  tables: 5,  // 5 kaynak  queues: 3,  // 3 kaynak  apis: 2,  // 10 kaynak  alarms: 100,  // 100 kaynak (Lambda başına 5)  dashboards: 5,  // 5 kaynak  backupPlans: 8,  // 16 kaynak  kmsKeys: 3,  // 6 kaynak  multiAzResources: 40, // HA redundancy  total: 505  // LİMİT AŞILDI};

Kaynak Sayısını Takip Etmek

Limite çarpmadan önce proaktif olarak kaynak sayını izle:

bash
# CDK - Deployment öncesi kaynak sayısını saycdk synth -j | jq '.Resources | length'
# Multi-stack app'te belirli stack içincdk synth YourStackName -j | jq '.Resources | length'
# CLI - Mevcut stack kaynaklarını sayaws cloudformation describe-stack-resources --stack-name MyStack \  --query "StackResources[].ResourceType" --output text | \  tr "\t" "\n" | sort | uniq -c | sort -nr
# Örnek çıktı:#  142 AWS::Lambda::Function#  85 AWS::IAM::Role#  78 AWS::Logs::LogGroup#  42 AWS::CloudWatch::Alarm#  28 AWS::DynamoDB::Table#  15 AWS::SQS::Queue#  ---#  390 Toplam

Strateji 0: Kaynak Konsolidasyonu - Ayırmadan Önce Azalt

Stack'leri ayırmadan önce kaynakları konsolide ederek toplam sayıyı azalt. Bu senin ilk adımın olmalı; stack ayırmak operasyonel karmaşıklık ekliyor, konsolidasyon seni limitin altına indirirse bunu tercih et.

Konsolidasyonu Ne Zaman Kullanmalı?

  • Stack splitting'i düşünmeden önce ilk adım olarak
  • Paylaşılabilecek benzer kaynaklarınız olduğunda
  • 500 kaynak limitine çarpmadan önce (proaktif optimizasyon)
  • Operasyonel overhead ve maliyetleri azaltmak için

Pattern 1: Function Başına Role Yerine Shared IAM Role'ler

typescript
// ÖNCE: Her Lambda kendi role'ünü alıyor// 10 Lambda = 10 function + 10 role + 10+ policy = 30+ kaynakconst userHandler = new NodejsFunction(this, 'UserHandler', {  entry: 'src/handlers/user.ts',  // CDK otomatik dedicated role oluşturuyor});
const orderHandler = new NodejsFunction(this, 'OrderHandler', {  entry: 'src/handlers/order.ts',  // Başka bir dedicated role oluşturuldu});
// SONRA: Shared execution role// 10 Lambda = 10 function + 1 role + 1 policy = 12 kaynak// Tasarruf: 18 kaynak (%60 azalma)const sharedLambdaRole = new iam.Role(this, 'SharedLambdaExecutionRole', {  assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),  managedPolicies: [    iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'),    iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),  ],});
// Tüm table'lar/kaynaklar için permission'ları bir kerede eklesharedLambdaRole.addToPolicy(new iam.PolicyStatement({  actions: [    'dynamodb:GetItem',    'dynamodb:PutItem',    'dynamodb:UpdateItem',    'dynamodb:DeleteItem',    'dynamodb:Query',    'dynamodb:Scan',  ],  resources: ['arn:aws:dynamodb:*:*:table/*'],}));
sharedLambdaRole.addToPolicy(new iam.PolicyStatement({  actions: ['sqs:SendMessage', 'sqs:ReceiveMessage', 'sqs:DeleteMessage'],  resources: ['arn:aws:sqs:*:*:*'],}));
// Tüm function'lar için role'ü tekrar kullanconst userHandler = new NodejsFunction(this, 'UserHandler', {  entry: 'src/handlers/user.ts',  role: sharedLambdaRole,});
const orderHandler = new NodejsFunction(this, 'OrderHandler', {  entry: 'src/handlers/order.ts',  role: sharedLambdaRole,});

Pattern 2: Shared Security Group'lar

typescript
// ÖNCE: VPC'deki her Lambda kendi security group'unu alıyor// 20 Lambda = 20 security groupconst userHandlerSG = new ec2.SecurityGroup(this, 'UserHandlerSG', {  vpc,  description: 'Security group for user handler',});
// SONRA: Tüm Lambda function'lar için shared security group// 20 Lambda = 1 security group// Tasarruf: 19 kaynakconst lambdaSecurityGroup = new ec2.SecurityGroup(this, 'LambdaSecurityGroup', {  vpc,  description: 'Shared security group for all Lambda functions',  allowAllOutbound: true,});
lambdaSecurityGroup.addIngressRule(  albSecurityGroup,  ec2.Port.tcp(443),  'Allow HTTPS from ALB');
const lambdaDefaults = {  vpc,  securityGroups: [lambdaSecurityGroup],};

Pattern 3: Aggregate CloudWatch Alarm'lar

typescript
// ÖNCE: Lambda başına ayrı alarm// 20 Lambda × 3 alarm (errors, duration, throttles) = 60 alarm
// SONRA: Metric math ile composite alarm'lar// 20 Lambda = 1 aggregate alarm// Tasarruf: 19 kaynak (error alarm'ları için)const allLambdaErrors = new cloudwatch.MathExpression({  expression: 'SUM([m1, m2, m3, m4, m5])',  usingMetrics: {    m1: userHandler.metricErrors(),    m2: orderHandler.metricErrors(),    m3: paymentHandler.metricErrors(),    // ... expression başına 10 metric'e kadar  },});
const aggregatedAlarm = new cloudwatch.Alarm(this, 'AllLambdaErrors', {  metric: allLambdaErrors,  threshold: 50,  evaluationPeriods: 2,  alarmName: 'aggregate-lambda-errors',  alarmDescription: 'Total errors across all Lambda functions',});
// Trade-off: Daha az granular alerting, ama daha az kaynak

Konsolidasyon Etkisi Örneği

Orijinal Altyapı:- 50 Lambda function: 50 kaynak- 50 IAM role: 50 kaynak- 50 IAM policy: 50 kaynak- 50 Log group: 50 kaynak (otomatik oluşturulan)- 50 Security group: 50 kaynak- 150 CloudWatch alarm (Lambda başına 3): 150 kaynakToplam: 400 kaynak
Konsolidasyon Sonrası:- 50 Lambda function: 50 kaynak- 1 shared IAM role: 1 kaynak- 1 shared IAM policy: 1 kaynak- 50 Log group: 50 kaynak (Lambda için konsolide edilemez)- 1 shared Security group: 1 kaynak- 10 aggregate CloudWatch alarm: 10 kaynakToplam: 113 kaynak
Tasarruf: 287 kaynak (%72 azalma!)

Konsolidasyonun Trade-off'ları

Avantajları:

  • Önemli kaynak sayısı azalması (genellikle %50-70 mümkün)
  • Daha basit IAM yönetimi (audit edilecek daha az role)
  • Daha hızlı deployment'lar (oluşturulacak/güncellenecek daha az kaynak)
  • Azalan CloudFormation template boyutu

Dezavantajları:

  • Güvenlik: Shared role'ler daha geniş permission'lara sahip (least privilege ihlalleri)
  • Blast radius: Role değişikliği onu kullanan tüm kaynakları etkiliyor
  • Debugging: Sorunları belirli function'lara trace etmek zorlaşıyor
  • Compliance: Separation of concerns gereksinimlerini ihlal edebilir
  • Rollback: Bir service için permission'ları bağımsız olarak geri alamazsın

Konsolidasyon için Karar Çerçevesi

typescript
// YÜKSEK KONSOLİDASYON: Development/staging environment'larconst devEnvironment = {  sharedRoles: true,  // Maliyetleri azalt, hızlı deployment'lar  sharedSecurityGroups: true, // Daha basit yönetim  aggregateAlarms: true,  // Daha az kritik monitoring};
// ORTA KONSOLİDASYON: Production environment'larconst prodEnvironment = {  sharedRoles: 'same-service', // Sadece service sınırları içinde  sharedSecurityGroups: true,  // Network izolasyonu korunuyor  aggregateAlarms: false,  // Granular alerting kritik};
// KONSOLİDASYON YOK: Yüksek regüle edilmiş environment'larconst regulatedEnvironment = {  sharedRoles: false,  // Function başına audit trail  sharedSecurityGroups: false, // Network segmentation  aggregateAlarms: false,  // Bireysel compliance monitoring};

Best Practice: Stack splitting'den önce konsolidasyonla başla. Konsolidasyon ile 600'den 400 kaynağa düşebiliyorsan, stack ayırmana gerek kalmayabilir.

Strateji 1: Nested Stack'ler - Resmi Çözüm

Nested stack'ler parent stack'in içinde her biri 500 kaynağa kadar olan birden fazla child stack barındırmasına izin veriyor. Nested stack parent stack'te tek kaynak olarak sayılıyor.

Ne Zaman Kullanmalı?

  • Altyapı mantıksal olarak farklı domain'lere bölünüyor (networking, compute, storage, monitoring)
  • Tüm kaynaklar tek birim olarak deploy olup rollback oluyor
  • Tek deployment operasyonu tercih ediliyor
  • Takım altyapıyı merkezi kontrol noktasından yönetiyor

CDK Implementation

typescript
// lib/stacks/nested/networking-stack.tsimport { NestedStack, NestedStackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as ec2 from 'aws-cdk-lib/aws-ec2';
export class NetworkingNestedStack extends NestedStack {  public readonly vpc: ec2.IVpc;
  constructor(scope: Construct, id: string, props?: NestedStackProps) {    super(scope, id, props);
    this.vpc = new ec2.Vpc(this, 'ApplicationVpc', {      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),      maxAzs: 3,      natGateways: 3,      subnetConfiguration: [        {          name: 'Public',          subnetType: ec2.SubnetType.PUBLIC,          cidrMask: 24,        },        {          name: 'Private',          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,          cidrMask: 24,        },        {          name: 'Isolated',          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,          cidrMask: 24,        },      ],    });
    new ec2.FlowLog(this, 'FlowLog', {      resourceType: ec2.FlowLogResourceType.fromVpc(this.vpc),      destination: ec2.FlowLogDestination.toCloudWatchLogs(),    });  }}
// lib/stacks/nested/storage-stack.tsimport { NestedStack, NestedStackProps, RemovalPolicy } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
export interface StorageNestedStackProps extends NestedStackProps {  readonly environment: string;}
export class StorageNestedStack extends NestedStack {  public readonly userTable: dynamodb.ITable;  public readonly orderTable: dynamodb.ITable;
  constructor(scope: Construct, id: string, props: StorageNestedStackProps) {    super(scope, id, props);
    const isProd = props.environment === 'prod';
    this.userTable = new dynamodb.Table(this, 'UserTable', {      partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,      encryption: dynamodb.TableEncryption.AWS_MANAGED,      pointInTimeRecovery: isProd,      removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,      stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,    });
    this.orderTable = new dynamodb.Table(this, 'OrderTable', {      partitionKey: { name: 'orderId', type: dynamodb.AttributeType.STRING },      sortKey: { name: 'timestamp', type: dynamodb.AttributeType.NUMBER },      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,      encryption: dynamodb.TableEncryption.AWS_MANAGED,      pointInTimeRecovery: isProd,      removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,    });
    this.orderTable.addGlobalSecondaryIndex({      indexName: 'UserOrderIndex',      partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },      sortKey: { name: 'timestamp', type: dynamodb.AttributeType.NUMBER },    });  }}
// lib/stacks/nested/compute-stack.tsimport { NestedStack, NestedStackProps, Duration } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as lambda from 'aws-cdk-lib/aws-lambda';import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';import * as ec2 from 'aws-cdk-lib/aws-ec2';import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';import * as logs from 'aws-cdk-lib/aws-logs';
export interface ComputeNestedStackProps extends NestedStackProps {  readonly vpc: ec2.IVpc;  readonly userTable: dynamodb.ITable;  readonly orderTable: dynamodb.ITable;}
export class ComputeNestedStack extends NestedStack {  public readonly userHandler: nodejs.NodejsFunction;  public readonly orderHandler: nodejs.NodejsFunction;
  constructor(scope: Construct, id: string, props: ComputeNestedStackProps) {    super(scope, id, props);
    const lambdaDefaults = {      runtime: lambda.Runtime.NODEJS_22_X,      timeout: Duration.seconds(30),      memorySize: 1024,      tracing: lambda.Tracing.ACTIVE,      logRetention: logs.RetentionDays.ONE_WEEK,      vpc: props.vpc,      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },      bundling: {        minify: true,        sourceMap: true,        externalModules: ['@aws-sdk/*'],      },    };
    this.userHandler = new nodejs.NodejsFunction(this, 'UserHandler', {      ...lambdaDefaults,      entry: 'src/handlers/user.ts',      environment: {        USER_TABLE_NAME: props.userTable.tableName,      },    });
    props.userTable.grantReadWriteData(this.userHandler);
    this.orderHandler = new nodejs.NodejsFunction(this, 'OrderHandler', {      ...lambdaDefaults,      entry: 'src/handlers/order.ts',      environment: {        ORDER_TABLE_NAME: props.orderTable.tableName,        USER_TABLE_NAME: props.userTable.tableName,      },    });
    props.orderTable.grantReadWriteData(this.orderHandler);    props.userTable.grantReadData(this.orderHandler);  }}
// lib/stacks/parent-stack.tsimport { Stack, StackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';import { NetworkingNestedStack } from './nested/networking-stack';import { StorageNestedStack } from './nested/storage-stack';import { ComputeNestedStack } from './nested/compute-stack';
export interface ParentStackProps extends StackProps {  readonly environment: string;}
export class ParentStack extends Stack {  constructor(scope: Construct, id: string, props: ParentStackProps) {    super(scope, id, props);
    const networkingStack = new NetworkingNestedStack(this, 'Networking');
    const storageStack = new StorageNestedStack(this, 'Storage', {      environment: props.environment,    });
    const computeStack = new ComputeNestedStack(this, 'Compute', {      vpc: networkingStack.vpc,      userTable: storageStack.userTable,      orderTable: storageStack.orderTable,    });  }}

Deployment Süreci

bash
# Her şeyi tek operasyonda deploy etcdk deploy ProductionStack
# CloudFormation oluşturuyor:# 1. ProductionStack (parent)# 2. ProductionStack-Networking (nested)# 3. ProductionStack-Storage (nested)# 4. ProductionStack-Compute (nested)
# Rollback davranışı:# - Herhangi bir nested stack fail olursa, tüm parent stack rollback oluyor# - Tüm kaynaklar atomik olarak oluşturuluyor/güncelleniyor

Avantajlar ve Limitasyonlar

Avantajları:

  • Tek deployment operasyonu
  • Atomik rollback - ya hepsi ya hiçbiri
  • Domain'e göre mantıksal organizasyon
  • Her nested stack 500 kaynak budget'ına sahip
  • Parent stack sadece nested stack'leri sayıyor (örnekte 3 kaynak)

Limitasyonları:

  1. Changeset'ler Opak Oluyor: CloudFormation changeset sadece parent-level değişiklikleri gösteriyor, nested stack'lerin içinde ne değiştiğini göstermiyor.

  2. Drift Detection Karmaşıklığı: Her nested stack'i ayrı ayrı kontrol etmek gerekiyor, sonuçları aggregate etmek için custom script lazım.

  3. Nested Stack Update Failure'ları Stuck State'ler Yaratıyor: Bir nested stack update fail olup resource deletion'ı beklerken takılı kalırsa, tüm parent stack bekliyor, tüm deployment'ları blokluyor.

  4. 2500 Kaynak Operasyon Limiti: Nested stack'lerle bile, tek deployment operasyonu toplamda 2500 kaynakla sınırlı.

  5. Bağımsız Deploy Edilemiyor: Ayrı nested stack'leri deploy edemezsin; her zaman parent üzerinden deploy etmen gerekiyor.

Strateji 2: Cross-Stack Referanslar - Bağımsız Deployment

Çıktıların açık export/import'u ile birden fazla bağımsız stack farklı takımların farklı altyapı componentlerini yönetmesine izin veriyor.

Ne Zaman Kullanmalı?

  • Takımlar altyapı componentlerini bağımsız deploy etmek istiyor
  • Componentler için farklı lifecycle (networking nadiren değişiyor, compute sık değişiyor)
  • Birden fazla takım altyapının farklı parçalarını yönetiyor
  • Kaynakları birden fazla consuming stack arasında paylaşman gerekiyor

CDK Implementation

typescript
// lib/stacks/network-stack.tsimport { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as ec2 from 'aws-cdk-lib/aws-ec2';
export class NetworkStack extends Stack {  public readonly vpc: ec2.IVpc;
  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    this.vpc = new ec2.Vpc(this, 'AppVpc', {      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),      maxAzs: 3,      natGateways: 3,    });
    // Cross-stack referans için VPC ID'yi export et    new CfnOutput(this, 'VpcId', {      value: this.vpc.vpcId,      exportName: 'AppVpcId',      description: 'Application VPC ID',    });
    new CfnOutput(this, 'PrivateSubnetIds', {      value: this.vpc.privateSubnets.map(s => s.subnetId).join(','),      exportName: 'AppVpcPrivateSubnetIds',      description: 'Private subnet IDs',    });  }}
// lib/stacks/storage-stack.tsimport { Stack, StackProps, CfnOutput, RemovalPolicy } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
export class StorageStack extends Stack {  public readonly userTable: dynamodb.ITable;
  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    this.userTable = new dynamodb.Table(this, 'UserTable', {      partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,      encryption: dynamodb.TableEncryption.AWS_MANAGED,      pointInTimeRecovery: true,      removalPolicy: RemovalPolicy.RETAIN,    });
    new CfnOutput(this, 'UserTableName', {      value: this.userTable.tableName,      exportName: 'UserTableName',      description: 'User DynamoDB table name',    });
    new CfnOutput(this, 'UserTableArn', {      value: this.userTable.tableArn,      exportName: 'UserTableArn',      description: 'User DynamoDB table ARN',    });  }}
// lib/stacks/compute-stack.tsimport { Stack, StackProps, Fn, Duration } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as lambda from 'aws-cdk-lib/aws-lambda';import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';import * as ec2 from 'aws-cdk-lib/aws-ec2';import * as iam from 'aws-cdk-lib/aws-iam';
export class ComputeStack extends Stack {  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    // NetworkStack'ten cross-stack referans kullanarak VPC'yi import et    const vpcId = Fn.importValue('AppVpcId');    const vpc = ec2.Vpc.fromLookup(this, 'ImportedVpc', { vpcId });
    const userTableName = Fn.importValue('UserTableName');    const userTableArn = Fn.importValue('UserTableArn');
    const userHandler = new nodejs.NodejsFunction(this, 'UserHandler', {      runtime: lambda.Runtime.NODEJS_22_X,      entry: 'src/handlers/user.ts',      timeout: Duration.seconds(30),      vpc: vpc,      environment: {        USER_TABLE_NAME: userTableName,      },    });
    userHandler.addToRolePolicy(new iam.PolicyStatement({      actions: [        'dynamodb:GetItem',        'dynamodb:PutItem',        'dynamodb:UpdateItem',        'dynamodb:DeleteItem',        'dynamodb:Query',      ],      resources: [userTableArn],    }));  }}
// bin/app.tsimport * as cdk from 'aws-cdk-lib';import { NetworkStack } from '../lib/stacks/network-stack';import { StorageStack } from '../lib/stacks/storage-stack';import { ComputeStack } from '../lib/stacks/compute-stack';
const app = new cdk.App();
const env = {  account: process.env.CDK_DEFAULT_ACCOUNT,  region: process.env.CDK_DEFAULT_REGION,};
const networkStack = new NetworkStack(app, 'NetworkStack', { env });const storageStack = new StorageStack(app, 'StorageStack', { env });const computeStack = new ComputeStack(app, 'ComputeStack', { env });
computeStack.addDependency(networkStack);computeStack.addDependency(storageStack);
app.synth();

Deployment Süreci

bash
# Stack'leri bağımsız deploy etcdk deploy NetworkStackcdk deploy StorageStackcdk deploy ComputeStack
# Network/storage'a dokunmadan compute stack'i güncellecdk deploy ComputeStack
# Tüm stack'leri listelecdk list# Çıktı:# NetworkStack# StorageStack# ComputeStack

Kritik Limitasyon - Export Update Lock

En önemli limitasyon: Export başka bir stack tarafından import ediliyorken güncellenemez veya silinemez.

bash
# VPC'yi değiştiren NetworkStack'i güncellemeye çalış:cdk deploy NetworkStack
# CloudFormation Hatası:# Export AppVpcId cannot be updated as it is in use by ComputeStack
# Çözüm gerektiriyor:# 1. ComputeStack'i sil (DOWNTIME!)# 2. NetworkStack'i güncelle# 3. ComputeStack'i yeniden oluştur

Trade-off Özeti

  • Pro: Bağımsız deployment
  • Pro: Takım özerkliği
  • Con: Export değişiklikleri consuming stack'lerin silinmesini gerektiriyor
  • Con: Daha karmaşık bağımlılık yönetimi
  • Con: İlgili altyapı genelinde atomik güncellemeleri sağlamak zorlaşıyor

Strateji 3: SSM Parameter Store - Gevşek Coupling

AWS Systems Manager Parameter Store kullanarak stack'ler arası değer paylaşımı, sabit cross-stack referanslarından kaçınmaya izin veriyor.

Ne Zaman Kullanmalı?

  • Stack bağımlılıkları olmadan paylaşılan değerleri güncelleme esnekliği gerekiyor
  • Provider ve consumer stack'lerini ayırmak istiyorsun
  • Birden fazla stack aynı değerleri consume ediyor
  • Cross-region deployment'lar (parameterlar replicate edilebilir)

CDK Implementation

typescript
// lib/stacks/network-stack-ssm.tsimport { Stack, StackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as ec2 from 'aws-cdk-lib/aws-ec2';import * as ssm from 'aws-cdk-lib/aws-ssm';
export class NetworkStackSSM extends Stack {  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    const vpc = new ec2.Vpc(this, 'AppVpc', {      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),      maxAzs: 3,    });
    // Export etmek yerine VPC ID'yi Parameter Store'da sakla    new ssm.StringParameter(this, 'VpcIdParameter', {      parameterName: '/app/network/vpc-id',      stringValue: vpc.vpcId,      description: 'Application VPC ID',      tier: ssm.ParameterTier.STANDARD,    });
    new ssm.StringParameter(this, 'PrivateSubnetIdsParameter', {      parameterName: '/app/network/private-subnet-ids',      stringValue: vpc.privateSubnets.map(s => s.subnetId).join(','),      description: 'Private subnet IDs',    });  }}
// lib/stacks/storage-stack-ssm.tsimport { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';import * as ssm from 'aws-cdk-lib/aws-ssm';
export class StorageStackSSM extends Stack {  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    const userTable = new dynamodb.Table(this, 'UserTable', {      partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,      encryption: dynamodb.TableEncryption.AWS_MANAGED,      pointInTimeRecovery: true,      removalPolicy: RemovalPolicy.RETAIN,    });
    new ssm.StringParameter(this, 'UserTableNameParameter', {      parameterName: '/app/storage/user-table-name',      stringValue: userTable.tableName,      description: 'User table name',    });
    new ssm.StringParameter(this, 'UserTableArnParameter', {      parameterName: '/app/storage/user-table-arn',      stringValue: userTable.tableArn,      description: 'User table ARN',    });  }}
// lib/stacks/compute-stack-ssm.tsimport { Stack, StackProps, Duration } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as lambda from 'aws-cdk-lib/aws-lambda';import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';import * as ec2 from 'aws-cdk-lib/aws-ec2';import * as ssm from 'aws-cdk-lib/aws-ssm';import * as iam from 'aws-cdk-lib/aws-iam';
export class ComputeStackSSM extends Stack {  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    // Method 1: Synthesis time'da parametreyi oku (valueFromLookup)    // Pro: Type-safe, erken validation    // Con: Synth'den önce parametrenin var olması gerekiyor, cdk.context.json'da cache'leniyor    const vpcId = ssm.StringParameter.valueFromLookup(this, '/app/network/vpc-id');
    // Method 2: Deployment time'da parametreyi oku (valueForStringParameter)    // Pro: Her zaman son değeri kullanıyor, cache yok    // Con: Synth time'da değer bilinmiyor, daha az type-safe    const userTableName = ssm.StringParameter.valueForStringParameter(      this,      '/app/storage/user-table-name'    );
    const userTableArn = ssm.StringParameter.valueForStringParameter(      this,      '/app/storage/user-table-arn'    );
    const vpc = ec2.Vpc.fromLookup(this, 'ImportedVpc', { vpcId });
    const userHandler = new nodejs.NodejsFunction(this, 'UserHandler', {      runtime: lambda.Runtime.NODEJS_22_X,      entry: 'src/handlers/user.ts',      timeout: Duration.seconds(30),      vpc: vpc,      environment: {        USER_TABLE_NAME: userTableName,      },    });
    userHandler.addToRolePolicy(new iam.PolicyStatement({      actions: ['dynamodb:GetItem', 'dynamodb:PutItem', 'dynamodb:UpdateItem'],      resources: [userTableArn],    }));
    userHandler.addToRolePolicy(new iam.PolicyStatement({      actions: ['ssm:GetParameter', 'ssm:GetParameters'],      resources: [`arn:aws:ssm:${this.region}:${this.account}:parameter/app/*`],    }));  }}

Deployment Süreci

bash
# Herhangi bir sırada deploy et (mantıksal sıra önerilse de)cdk deploy NetworkStackSSMcdk deploy StorageStackSSMcdk deploy ComputeStackSSM
# ComputeStack'i etkilemeden NetworkStack VPC'sini güncellecdk deploy NetworkStackSSM# Parameter değeri güncellendi, export lock sorunu yok
# ComputeStack yeni VPC'yi almak için daha sonra redeploy edilebilir

Avantajlar ve Trade-off'lar

Avantajları:

  • Cross-stack export lock'ları yok
  • Consumer'ları etkilemeden provider stack'i güncelle
  • Birden fazla stack aynı parametreleri okuyabilir
  • Cross-region replication mümkün
  • Rollback için versioned parameterlar kullanılabilir

Trade-off'ları:

  • valueFromLookup cdk.context.json'da cache'leniyor - eski kalabilir
  • valueForStringParameter deploy time'da resolve oluyor - daha az type-safe
  • Runtime parameter okumaları Lambda execution time'ına ekliyor
  • SSM read access için IAM permission'ları gerekiyor
  • Deployment'tan önce parameterların var olması gerekiyor (veya default değerler kullan)

Strateji 4: Birden Fazla Bağımsız Stack - Microservice Pattern

Tek CDK app birden fazla bağımsız stack oluşturuyor, mantıksal olarak organize ama coupling yok.

Ne Zaman Kullanmalı?

  • Microservice mimarisi - her service bağımsız stack
  • Service'ler için farklı deployment schedule'ları
  • Service bazında takım sahipliği
  • Her service 500 kaynağın altında
  • Deployment esnekliği ile mono-repo organizasyonu istiyorsun

CDK Implementation

typescript
// lib/constructs/service-stack.tsimport { Stack, StackProps, Duration, RemovalPolicy } from 'aws-cdk-lib';import { Construct } from 'constructs';import * as lambda from 'aws-cdk-lib/aws-lambda';import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';import * as apigateway from 'aws-cdk-lib/aws-apigateway';import * as logs from 'aws-cdk-lib/aws-logs';import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
export interface ServiceStackProps extends StackProps {  readonly serviceName: string;  readonly stage: string;}
export class ServiceStack extends Stack {  public readonly api: apigateway.RestApi;  public readonly handler: nodejs.NodejsFunction;  public readonly table: dynamodb.Table;
  constructor(scope: Construct, id: string, props: ServiceStackProps) {    super(scope, id, props);
    const isProd = props.stage === 'prod';
    this.table = new dynamodb.Table(this, 'Table', {      tableName: `${props.serviceName}-${props.stage}`,      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,      encryption: dynamodb.TableEncryption.AWS_MANAGED,      pointInTimeRecovery: isProd,      removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,    });
    this.handler = new nodejs.NodejsFunction(this, 'Handler', {      functionName: `${props.serviceName}-handler-${props.stage}`,      runtime: lambda.Runtime.NODEJS_22_X,      entry: `src/services/${props.serviceName}/handler.ts`,      timeout: Duration.seconds(30),      memorySize: 1024,      tracing: lambda.Tracing.ACTIVE,      logRetention: logs.RetentionDays.ONE_WEEK,      environment: {        TABLE_NAME: this.table.tableName,        SERVICE_NAME: props.serviceName,        STAGE: props.stage,      },      bundling: {        minify: true,        sourceMap: true,        externalModules: ['@aws-sdk/*'],      },    });
    this.table.grantReadWriteData(this.handler);
    this.api = new apigateway.RestApi(this, 'Api', {      restApiName: `${props.serviceName}-api-${props.stage}`,      deployOptions: {        stageName: props.stage,        tracingEnabled: true,        loggingLevel: apigateway.MethodLoggingLevel.INFO,        metricsEnabled: true,      },    });
    const integration = new apigateway.LambdaIntegration(this.handler);    this.api.root.addMethod('ANY', integration);
    const resource = this.api.root.addResource('{proxy+}');    resource.addMethod('ANY', integration);
    new cloudwatch.Alarm(this, 'ErrorAlarm', {      metric: this.handler.metricErrors(),      threshold: 10,      evaluationPeriods: 2,      alarmName: `${props.serviceName}-errors-${props.stage}`,    });  }}
// bin/app.tsimport * as cdk from 'aws-cdk-lib';import { ServiceStack } from '../lib/constructs/service-stack';
const app = new cdk.App();
const stage = app.node.tryGetContext('stage') || 'dev';const env = {  account: process.env.CDK_DEFAULT_ACCOUNT,  region: process.env.CDK_DEFAULT_REGION,};
const services = [  'user-service',  'order-service',  'payment-service',  'inventory-service',  'notification-service',];
services.forEach(serviceName => {  new ServiceStack(app, `${serviceName}-${stage}`, {    serviceName,    stage,    env,    stackName: `${serviceName}-${stage}`,  });});
app.synth();

Deployment Seçenekleri

bash
# Tüm stack'leri listelecdk list# Çıktı:# user-service-prod# order-service-prod# payment-service-prod# inventory-service-prod# notification-service-prod
# Tüm service'leri deploy etcdk deploy --all
# Belirli service'i deploy etcdk deploy user-service-prod
# Birden fazla belirli service'i deploy etcdk deploy user-service-prod order-service-prod

Trade-off'lar

Avantajları:

  • Tam deployment bağımsızlığı
  • Her service takımı kendi stack'ine sahip
  • Diğer service'leri etkilemeden sık deploy et
  • Takımlar arası development'ı scale et
  • Yeni service eklemek kolay
  • Net service sınırları

Dezavantajları:

  • Shared altyapı yok (SSM/lookup kullanılmazsa VPC, networking duplike ediliyor)
  • Service discovery mekanizması gerekiyor (SSM, EventBridge, service mesh)
  • Yönetilecek daha fazla stack (5 service = 5 stack)
  • Multi-service deployment'lar için orkestrasyon gerekiyor

Karar Çerçevesi

Operasyonel gereksinimler ve takım yapısına göre stratejini seç:

Nested Stack'leri Ne Zaman Seçmeli:

  • Altyapı tek birim olarak deploy ediliyor
  • Atomik rollback önemli
  • Mantıksal domain ayrımı (network/compute/storage)
  • Tek takım tüm altyapıyı yönetiyor
  • Deployment sıklığı: Düşük-orta

Cross-Stack Referansları Ne Zaman Seçmeli:

  • Altyapı katmanları için farklı lifecycle
  • Networking nadiren değişiyor, compute sık değişiyor
  • Farklı takımlar farklı katmanlara sahip
  • Export update karmaşıklığını tolere edebilirsin
  • Deployment sıklığı: Orta

SSM Parameter Store'u Ne Zaman Seçmeli:

  • Maksimum deployment esnekliği gerekiyor
  • Katı bağımlılıklar olmadan altyapıyı güncelle
  • Cross-region deployment'lar
  • Aynı değerlerin birden fazla consumer'ı var
  • Deployment sıklığı: Yüksek

Birden Fazla Bağımsız Stack'i Ne Zaman Seçmeli:

  • Microservice mimarisi
  • Takım özerkliği kritik
  • Service'ler < 500 kaynak
  • Event-driven iletişim
  • Deployment sıklığı: Çok yüksek (service başına)

Yaygın Tuzaklar ve Çözümler

Tuzak 1: Kaynak Sayısını Proaktif İzlememek

Stack eşiği aşarsa build'i fail eden CI/CD check'i implement et:

bash
#!/bin/bash# .github/workflows/cdk-check.sh
MAX_RESOURCES=450
for stack in $(cdk list); do  resource_count=$(cdk synth $stack -j | jq '.Resources | length')  echo "$stack: $resource_count kaynak"
  if [ $resource_count -gt $MAX_RESOURCES ]; then    echo "HATA: $stack $MAX_RESOURCES kaynağı aşıyor ($resource_count)"    exit 1  fidone

Tuzak 2: Acil Durumda Cross-Stack Export Lock

Problem: Kritik production sorunu networking değişikliği gerektiriyor, ama cross-stack export güncellemeyi engelliyor.

Çözüm: Değişmesi muhtemel altyapı için SSM Parameter Store kullan:

typescript
// Cross-Stack Export Kullan: SABİT, nadiren değişen// - AWS Account ID// - Region// - Root DNS zone ID
// SSM Parameter Kullan: Downtime gerektirmeden DEĞİŞEBİLİR// - VPC ID (networking redesign nedeniyle değişebilir)// - Subnet ID'ler (IP range expansion nedeniyle değişebilir)// - Database endpoint'ler (migration nedeniyle değişebilir)

Tuzak 3: Nested Stack Bağımlılık Döngüleri

Nested stack'leri net hiyerarşide tut. Child stack'teki kaynaklar asla parent stack kaynaklarına referans vermemeli.

typescript
// Parent stack bağımlılıkları yönetiyorclass ParentStack extends Stack {  constructor(scope, id, props) {    super(scope, id, props);
    const network = new NetworkStack(this, 'Network');    const compute = new ComputeStack(this, 'Compute', {      vpc: network.vpc, // Tek yönlü bağımlılık    });
    // Parent nested stack'ler arası bağlantıları yönetiyor    compute.lambda.connections.allowFrom(network.vpc);  }}

Tuzak 4: Rollback Davranışını Test Etmemek

Development'ta kasıtlı failure'lar yaratarak rollback'i test et:

typescript
// Test için kasıtlı failure kaynağı oluşturconst testFailure = process.env.TEST_ROLLBACK === 'true';
if (testFailure) {  new lambda.Function(this, 'FailureTest', {    runtime: lambda.Runtime.NODEJS_22_X,    handler: 'index.handler',    code: lambda.Code.fromInline('INVALID CODE'), // Deployment failure'a neden oluyor  });}
// Rollback'i test etmek için TEST_ROLLBACK=true ile deploy et// TEST_ROLLBACK=true cdk deploy

Önemli Çıkarımlar

  1. 500 Kaynak Limiti Sabit: Service quota artırımı mümkün değil. Baştan mimariyi buna göre planla.

  2. Konsolidasyonla Başla: Stack'leri ayırmadan önce kaynak sayısını genellikle %50-70 azalt. Shared IAM role'ler, security group'lar ve aggregate alarm'lar kaynak sayısını önemli ölçüde azaltıyor.

  3. Nested Stack'ler Operasyonel Karmaşıklığı Basitlikle Değiştiriyor: Tek deployment operasyonu, ama changeset'ler opak oluyor ve drift detection custom tooling gerektiriyor.

  4. Cross-Stack Referanslar Export Lock'ları Yaratıyor: Consuming stack'leri silmeden exported değerler güncellenemiyor. Gerçekten sabit kaynaklar için ayır.

  5. SSM Parameter Store Maksimum Esneklik Sağlıyor: Gevşek coupling bağımsız deployment ve güncellemelere izin veriyor. Değişebilecek değerler için en iyi seçenek.

  6. Birden Fazla Bağımsız Stack Microservice'ler için En İyi: Her service 500 kaynağın altında, bağımsız deploy ediliyor. Event-driven iletişim patternleri gerektiriyor.

  7. Kaynak Sayısını Proaktif İzle: CI/CD check'leri 450 kaynağa yaklaşırsa fail olmalı. Production deployment failure'ını bekleme.

  8. Hybrid Yaklaşım En Yaygın: Altyapı istikrarı ve değişim sıklığına göre stratejileri birleştir. Sabit foundation'lar cross-stack export kullanır; değişken application'lar SSM kullanır.

  9. Rollback Davranışını Test Et: Production sorunları olmadan önce development'ta kasıtlı failure'lar yaratarak rollback davranışını anla.

  10. Deployment Sıklığına Göre Strateji Seç: Düşük sıklık → Nested Stack'ler; Orta → Cross-Stack; Yüksek → SSM; Çok Yüksek → Birden Fazla Bağımsız Stack.

CloudFormation'un kaynak limiti ile çalışmak bana gösterdi ki, doğru strateji takımının deployment patternlerine ve operasyonel tercihlerine bağlı. Konsolidasyonla başla, proaktif izle ve altyapının istikrarı ile değişim sıklığına uyan yaklaşımı seç.

İlgili Yazılar