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ı:
-
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.
-
Drift Detection Karmaşıklığı: Her nested stack’i ayrı ayrı kontrol etmek gerekiyor, sonuçları aggregate etmek için custom script lazım.
-
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.
-
2500 Kaynak Operasyon Limiti: Nested stack’lerle bile, tek deployment operasyonu toplamda 2500 kaynakla sınırlı.
-
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ı:
valueFromLookupcdk.context.json’da cache’leniyor - eski kalabilirvalueForStringParameterdeploy 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ç:
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
-
500 Kaynak Limiti Sabit: Service quota artırımı mümkün değil. Baştan mimariyi buna göre planla.
-
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.
-
Nested Stack’ler Operasyonel Karmaşıklığı Basitlikle Değiştiriyor: Tek deployment operasyonu, ama changeset’ler opak oluyor ve drift detection custom tooling gerektiriyor.
-
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.
-
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.
-
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.
-
Kaynak Sayısını Proaktif İzle: CI/CD check’leri 450 kaynağa yaklaşırsa fail olmalı. Production deployment failure’ını bekleme.
-
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.
-
Rollback Davranışını Test Et: Production sorunları olmadan önce development’ta kasıtlı failure’lar yaratarak rollback davranışını anla.
-
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
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ı.
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.
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.
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.
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.