İçeriğe atla

2025-11-18

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:

// Single Lambda function creates multiple resources
const 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:

// Development: 178 kaynak
const 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:

# CDK - Deployment öncesi kaynak sayısını say
cdk synth -j | jq '.Resources | length'

# Multi-stack app'te belirli stack için
cdk synth YourStackName -j | jq '.Resources | length'

# CLI - Mevcut stack kaynaklarını say
aws 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

// ÖNCE: Her Lambda kendi role'ünü alıyor
// 10 Lambda = 10 function + 10 role + 10+ policy = 30+ kaynak
const 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 ekle
sharedLambdaRole.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 kullan
const 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

// ÖNCE: VPC'deki her Lambda kendi security group'unu alıyor
// 20 Lambda = 20 security group
const 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 kaynak
const 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

// Ö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 kaynak
Toplam: 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 kaynak
Toplam: 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

// YÜKSEK KONSOLİDASYON: Development/staging environment'lar
const 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'lar
const 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'lar
const 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

// lib/stacks/nested/networking-stack.ts
import { 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.ts
import { 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.ts
import { 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.ts
import { 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

# Her şeyi tek operasyonda deploy et
cdk 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

// lib/stacks/network-stack.ts
import { 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.ts
import { 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.ts
import { 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.ts
import * 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

# Stack'leri bağımsız deploy et
cdk deploy NetworkStack
cdk deploy StorageStack
cdk deploy ComputeStack

# Network/storage'a dokunmadan compute stack'i güncelle
cdk deploy ComputeStack

# Tüm stack'leri listele
cdk 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.

# 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

// lib/stacks/network-stack-ssm.ts
import { 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.ts
import { 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.ts
import { 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

# Herhangi bir sırada deploy et (mantıksal sıra önerilse de)
cdk deploy NetworkStackSSM
cdk deploy StorageStackSSM
cdk deploy ComputeStackSSM

# ComputeStack'i etkilemeden NetworkStack VPC'sini güncelle
cdk 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

// lib/constructs/service-stack.ts
import { 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.ts
import * 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

# Tüm stack'leri listele
cdk list
# Çıktı:
# user-service-prod
# order-service-prod
# payment-service-prod
# inventory-service-prod
# notification-service-prod

# Tüm service'leri deploy et
cdk deploy --all

# Belirli service'i deploy et
cdk deploy user-service-prod

# Birden fazla belirli service'i deploy et
cdk 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ç:

Evet

Hayır

Nadir, senkronize

Sık, bağımsız

Evet

Hayır

Evet

Hayır

Evet

Hayır

Başlangıç: 500 Kaynağa yaklaşıyorsun

Kaynakları konsolide edebilir misin?

Strateji 0: Konsolidasyon

Takımlar ne sıklıkla deploy ediyor?

Strateji 1: Nested Stackler

Microservice mimarisi?

Strateji 4: Birden Fazla Bağımsız Stack

Export esnekliği gerekiyor mu?

Strateji 3: SSM Parameter Store

Strateji 2: Cross-Stack Referanslar

Hala 500 üstünde mi?

Bitti

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:

#!/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
  fi
done

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:

// 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.

// Parent stack bağımlılıkları yönetiyor
class 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:

// Test için kasıtlı failure kaynağı oluştur
const 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

AWS Bedrock AgentCore'u CDK ile deploy etmek: hızlı başlangıç

AgentCore Runtime üzerinde minimal bir Strands agent'ı CDK ile deploy etme rehberi — parametrize stack, arm64 build, deploy ve invoke akışı, ve ilk çağrıdan önce gereken IAM ve Marketplace ön koşulları.

aws-bedrockai-agentsaws-cdk+3
AWS ile Edge Computing: CloudFront Functions vs Lambda@Edge

Global uygulamalar için AWS edge computing çözümlerini seçme ve uygulama üzerine pratik örnekler ve maliyet optimizasyonu stratejileri içeren kapsamlı teknik rehber.

awscloudfrontlambda+6
AWS Secrets Manager & Parameter Store: Güvenlik Best Practices

AWS Secrets Manager ve Systems Manager Parameter Store'u karşılaştıran kapsamlı teknik rehber - hangi servisi ne zaman kullanmalı ve gerçek dünya implementation pattern'leri.

awssecrets-managerparameter-store+8
Feature Flags at Scale: Implementation Pattern'leri ve Platform Karşılaştırması

Distributed sistemlerde feature flag implementasyonu için production odaklı bir rehber. LaunchDarkly, Unleash ve AWS AppConfig karşılaştırması ile gradual rollout, A/B testing ve technical debt yönetimi için çalışan örnekler.

feature-flagsdevopscontinuous-delivery+7
AWS AppSync & GraphQL: Production-Ready Real-time API'ler Geliştirmek

AWS AppSync ile ölçeklenebilir real-time API'ler geliştirmek için kapsamlı bir rehber: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.

awsappsyncgraphql+5