Skip to content
~/sph.sh

AWS CDK Kod Organizasyonu: Service-Based ve Domain-Based Mimari Patternleri

AWS CDK projelerinde service-based, domain-based, feature-based veya layer-based organizasyon patternlerini ne zaman kullanacağını öğren. Karar çerçeveleri, çalışan örnekler ve sürdürülebilir infrastructure code için migration stratejileri.

Özet

AWS CDK projeleri genellikle belirsiz organizasyon stratejileriyle başlar ve ölçeklendikçe tight coupling, circular dependency'ler ve deployment darboğazlarıyla karşılaşır. Bu rehber, beş organizasyon patternini - service-based, domain-based, feature-based, layer-based ve hybrid - çalışan TypeScript örnekleri ve karar çerçeveleriyle inceliyor. Ekiplerin business ihtiyaçlarına, team yapısına ve deployment gereksinimlerine uygun CDK projeleri oluşturmasına yardımcı oluyor.

Problem Bağlamı

Farklı ekiplerle CDK projeleriyle çalışırken tekrar eden bir pattern fark ettim: projeler iyi niyetlerle başlıyor ama zamanla maintain edilmesi zorlaşıyor. Infrastructure code organik olarak büyüyor, dosyalar uygun olan yere konuluyor ve kısa sürede ekipler merge conflict'ler, belirsiz ownership ve çözemedikleri deployment dependency'lerle uğraşıyor.

Teknik sorunlar birkaç şekilde ortaya çıkıyor. AWS service'lerine göre organize olan ekipler (ayrı lambda/, dynamodb/, api-gateway/ klasörleri oluşturma) tek bir business özelliğindeki değişikliğin birden fazla directory'ye dokunmasını görüyor. Cross-stack referanslar, bir şeyler bozulana kadar belli olmayan deployment dependency'ler yaratıyor. Birden fazla ekip shared infrastructure üzerinde çalıştığında, belirsiz domain sınırları merge conflict'lere ve koordinasyon yüküne yol açıyor.

Temel soru sadece dosya organizasyonu değil - business mimarisini, deployment gereksinimlerini ve team yapısını yansıtan, aynı zamanda sistemleri kırılgan yapan coupling problemlerinden kaçınan infrastructure'ı nasıl modelliyorsun.

Teknik Gereksinimler

İyi organize edilmiş bir CDK projesinin çözmesi gereken birkaç teknik gereksinim var:

Deployment Bağımsızlığı: Ekipler, infrastructure değişikliklerini diğer ekiplerle koordine etmeden veya ilgisiz sistemleri kırma endişesi olmadan deploy edebilmeli.

Açık Ownership: Her stack ve construct'ın belirgin bir sahibi olmalı, sorun çıktığında veya değişiklik gerektiğinde kime soracağın açık olmalı.

Sürdürülebilirlik: Infrastructure code'u gezinmek kolay olmalı, ilişkili kaynaklar mantıksal olarak gruplanmalı ki developer'lar ihtiyaç duydukları şeyi hızlıca bulabilsin.

Ölçeklenebilirlik: Organizasyon pattern'i, proje 10 kaynaktan 500'e, 1 ekipten 10 ekibe büyüdükçe büyük bir yeniden yapılandırma gerektirmeden çalışmalı.

Cross-Cutting Concern'ler: VPC'ler, security policy'ler ve monitoring gibi shared infrastructure, tekrarlama veya garip dependency'ler olmadan ele alınmalı.

Organizasyon Patternleri

Service-Based Organizasyon

Service-based pattern, code'u AWS service türüne göre organize eder - tüm Lambda fonksiyonları bir arada, tüm DynamoDB tabloları bir arada, tüm API Gateway'ler bir arada.

typescript
// Directory yapısı// cdk-project/// ├── lib/// │   ├── lambda/// │   │   ├── user-handler.ts// │   │   ├── order-handler.ts// │   │   └── payment-handler.ts// │   ├── dynamodb/// │   │   ├── user-table.ts// │   │   ├── order-table.ts// │   │   └── payment-table.ts// │   ├── api-gateway/// │   │   ├── user-api.ts// │   │   ├── order-api.ts// │   │   └── payment-api.ts// │   └── stacks/// │       ├── lambda-stack.ts// │       ├── dynamodb-stack.ts// │       └── api-gateway-stack.ts
// lib/stacks/dynamodb-stack.tsimport { Stack, StackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';import { Table, BillingMode, AttributeType } from 'aws-cdk-lib/aws-dynamodb';
export class DynamoDBStack extends Stack {  public readonly userTable: Table;  public readonly orderTable: Table;  public readonly paymentTable: Table;
  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    this.userTable = new Table(this, 'UserTable', {      partitionKey: { name: 'userId', type: AttributeType.STRING },      billingMode: BillingMode.PAY_PER_REQUEST    });
    this.orderTable = new Table(this, 'OrderTable', {      partitionKey: { name: 'orderId', type: AttributeType.STRING },      billingMode: BillingMode.PAY_PER_REQUEST    });
    this.paymentTable = new Table(this, 'PaymentTable', {      partitionKey: { name: 'paymentId', type: AttributeType.STRING },      billingMode: BillingMode.PAY_PER_REQUEST    });  }}
// lib/stacks/lambda-stack.tsimport { Stack, StackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';import { Table } from 'aws-cdk-lib/aws-dynamodb';
export interface LambdaStackProps extends StackProps {  readonly userTable: Table;  readonly orderTable: Table;  readonly paymentTable: Table;}
export class LambdaStack extends Stack {  constructor(scope: Construct, id: string, props: LambdaStackProps) {    super(scope, id, props);
    const userHandler = new NodejsFunction(this, 'UserHandler', {      entry: 'src/handlers/user.ts',      environment: {        TABLE_NAME: props.userTable.tableName      }    });
    props.userTable.grantReadWriteData(userHandler);
    // Order ve payment handler'lar için benzer...  }}

Bu yaklaşım başta mantıklı görünüyor - tüm Lambda fonksiyonları bir yerde, tutarlı konfigürasyonlar uygulamak kolay. Ancak problemler hızla belirgin hale geliyor:

  • User fonksiyonalitesinde değişiklik yapmak birden fazla directory ve stack'e dokunmayı gerektiriyor
  • Cross-stack referanslar deployment dependency'leri yaratıyor (DynamoDB stack, Lambda stack'ten önce deploy edilmeli)
  • Team ownership belirsiz - "Lambda stack"in sahibi kim?
  • Infrastructure'ın bir alt kümesini (sadece user ile ilgili kaynaklar) deploy etmek zor

Service-based ne zaman işe yarar: 10'dan az kaynağa sahip küçük projeler, öğrenme/deney aşaması veya organizasyon pattern'inin henüz çok önemli olmadığı single-service uygulamalar.

Domain-Based Organizasyon

Domain-based pattern, code'u business domain'ine veya bounded context'e göre organize eder, bir domain için tüm infrastructure'ı bir arada gruplar.

typescript
// Directory yapısı// cdk-project/// ├── lib/// │   ├── domains/// │   │   ├── user/// │   │   │   ├── user-stack.ts// │   │   │   ├── user-handler.ts// │   │   │   ├── user-table.ts// │   │   │   ├── user-api.ts// │   │   │   └── constructs/// │   │   │       └── user-service.ts// │   │   ├── order/// │   │   │   ├── order-stack.ts// │   │   │   ├── order-handler.ts// │   │   │   ├── order-table.ts// │   │   │   └── order-api.ts// │   │   └── payment/// │   │       ├── payment-stack.ts// │   │       ├── payment-handler.ts// │   │       └── payment-table.ts// │   └── shared/// │       ├── networking-stack.ts// │       ├── security-stack.ts// │       └── monitoring-stack.ts
// lib/domains/user/user-stack.tsimport { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';import { Construct } from 'constructs';import { Table, BillingMode, AttributeType, TableEncryption } from 'aws-cdk-lib/aws-dynamodb';import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';import { RestApi, LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
export class UserStack extends Stack {  public readonly api: RestApi;  public readonly apiEndpoint: string;
  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    // User ile ilgili tüm infrastructure bir arada    const table = new Table(this, 'UserTable', {      partitionKey: { name: 'userId', type: AttributeType.STRING },      billingMode: BillingMode.PAY_PER_REQUEST,      encryption: TableEncryption.AWS_MANAGED,      pointInTimeRecovery: true,      removalPolicy: RemovalPolicy.RETAIN    });
    const handler = new NodejsFunction(this, 'UserHandler', {      entry: 'src/handlers/user.ts',      environment: {        TABLE_NAME: table.tableName      }    });
    table.grantReadWriteData(handler);
    this.api = new RestApi(this, 'UserApi', {      restApiName: 'user-service',      deployOptions: {        stageName: 'v1',        tracingEnabled: true      }    });
    const users = this.api.root.addResource('users');    users.addMethod('GET', new LambdaIntegration(handler));    users.addMethod('POST', new LambdaIntegration(handler));
    const user = users.addResource('{userId}');    user.addMethod('GET', new LambdaIntegration(handler));    user.addMethod('PUT', new LambdaIntegration(handler));    user.addMethod('DELETE', new LambdaIntegration(handler));
    this.apiEndpoint = this.api.url;  }}
// bin/app.tsimport { App } from 'aws-cdk-lib';import { UserStack } from '../lib/domains/user/user-stack';import { OrderStack } from '../lib/domains/order/order-stack';import { PaymentStack } from '../lib/domains/payment/payment-stack';
const app = new App();
const userStack = new UserStack(app, 'UserStack', {  env: { account: '123456789012', region: 'us-east-1' }});
const orderStack = new OrderStack(app, 'OrderStack', {  env: { account: '123456789012', region: 'us-east-1' },  userApiEndpoint: userStack.apiEndpoint});
// Order stack, user stack'e bağımlıorderStack.addDependency(userStack);
const paymentStack = new PaymentStack(app, 'PaymentStack', {  env: { account: '123456789012', region: 'us-east-1' }});

Bu organizasyon, infrastructure'ı business yetenekleriyle hizalıyor. User fonksiyonalitesindeki değişiklikler sadece user/ directory'sine dokunuyor. Team ownership açık - user ekibi user stack'e sahip. Domain'ler (shared infrastructure'dan sonra) bağımsız deploy edilebiliyor ve gerekirse bir domain'i ayrı bir repository'ye çıkarmak basit.

Single-Table Design ile uyum: Domain-based organizasyon, DynamoDB Single-Table Design pattern'i ile mükemmel uyumludur. Single table birden fazla entity type'ı (User, Order, Payment) içerdiğinde, her domain kendi access pattern'lerini ve repository logic'ini kendi klasöründe tutabilir. Örneğin user/ klasöründe user-repository.ts dosyası User entity'sine özel query'leri içerir, order/ klasöründe order-repository.ts Order access pattern'lerini yönetir - hepsi aynı physical table'ı kullanırken. Tablo definition'ı shared/ klasöründe olur, domain'ler sadece access pattern'lerini own eder.

Domain-based ne zaman işe yarar: Microservices mimarileri, multi-team organizasyonlar, business'a hizalanmış infrastructure, Single-Table Design kullanımı veya açık bounded context'lerin olduğu durumlar.

Feature-Based Organizasyon

Product odaklı ekipler için, user'a yönelik özelliklere göre organize olmak teknik domain'lerden daha mantıklı.

typescript
// Directory yapısı// cdk-project/// ├── lib/// │   ├── features/// │   │   ├── authentication/// │   │   │   ├── auth-stack.ts// │   │   │   ├── cognito-pool.ts// │   │   │   ├── auth-lambda.ts// │   │   │   └── auth-api.ts// │   │   ├── user-profile/// │   │   │   ├── profile-stack.ts// │   │   │   ├── profile-handler.ts// │   │   │   └── profile-table.ts// │   │   ├── notifications/// │   │   │   ├── notification-stack.ts// │   │   │   ├── sns-topics.ts// │   │   │   ├── email-handler.ts// │   │   │   └── sms-handler.ts// │   │   └── search/// │   │       ├── search-stack.ts// │   │       ├── opensearch-domain.ts// │   │       └── indexer-lambda.ts
// lib/features/notifications/notification-stack.tsimport { Stack, StackProps, Duration } from 'aws-cdk-lib';import { Construct } from 'constructs';import { Topic } from 'aws-cdk-lib/aws-sns';import { EmailSubscription, SmsSubscription, SqsSubscription, LambdaSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';import { Queue } from 'aws-cdk-lib/aws-sqs';
export class NotificationStack extends Stack {  public readonly emailTopic: Topic;  public readonly smsTopic: Topic;
  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    // Email bildirimleri    this.emailTopic = new Topic(this, 'EmailTopic', {      displayName: 'Email Notifications'    });
    const emailQueue = new Queue(this, 'EmailQueue', {      visibilityTimeout: Duration.seconds(300)    });
    const emailHandler = new NodejsFunction(this, 'EmailHandler', {      entry: 'src/handlers/send-email.ts',      environment: {        SMTP_HOST: 'smtp.example.com'      }    });
    emailHandler.addEventSource(new SqsEventSource(emailQueue));    this.emailTopic.addSubscription(new SqsSubscription(emailQueue));
    // SMS bildirimleri    this.smsTopic = new Topic(this, 'SmsTopic', {      displayName: 'SMS Notifications'    });
    const smsHandler = new NodejsFunction(this, 'SmsHandler', {      entry: 'src/handlers/send-sms.ts'    });
    this.smsTopic.addSubscription(new LambdaSubscription(smsHandler));  }}

Feature-based organizasyon, özellikler birden fazla AWS service'ine ve domain'e yayıldığında, ekipler teknik katmanlardan ziyade product özellikleri etrafında organize olduğunda ve product hızlı özellik geliştirme ve deployment gerektirdiğinde iyi çalışır.

Feature-based ne zaman işe yarar: Feature ekipleri olan product şirketleri, hızlı geliştirme ortamları, bağımsız deploy edilebilecek özellikler.

Layer-Based Organizasyon

Layer-based organizasyon, infrastructure'ı teknik katmana göre ayırır, kaynakları lifecycle ve değişim sıklığına göre gruplar.

typescript
// Directory yapısı// cdk-project/// ├── lib/// │   ├── layers/// │   │   ├── foundation/// │   │   │   ├── vpc-stack.ts// │   │   │   ├── security-groups-stack.ts// │   │   │   └── base-stack.ts// │   │   ├── data/// │   │   │   ├── rds-stack.ts// │   │   │   ├── dynamodb-stack.ts// │   │   │   └── s3-stack.ts// │   │   ├── compute/// │   │   │   ├── lambda-stack.ts// │   │   │   ├── ecs-stack.ts// │   │   │   └── batch-stack.ts// │   │   └── api/// │   │       ├── apigw-stack.ts// │   │       └── alb-stack.ts
// bin/app.tsimport { App } from 'aws-cdk-lib';import { FoundationStack } from '../lib/layers/foundation/vpc-stack';import { DataStack } from '../lib/layers/data/dynamodb-stack';import { ComputeStack } from '../lib/layers/compute/lambda-stack';import { ApiStack } from '../lib/layers/api/apigw-stack';
const app = new App();
// Foundation katmanı - bir kez deploy edilir, nadiren güncellenirconst foundationStack = new FoundationStack(app, 'Foundation', {  env: { account: '123456789012', region: 'us-east-1' }});
// Data katmanı - stateful, RETAIN removal policy gerektirirconst dataStack = new DataStack(app, 'Data', {  env: { account: '123456789012', region: 'us-east-1' },  vpc: foundationStack.vpc});
// Compute katmanı - stateless, sık güncellenirconst computeStack = new ComputeStack(app, 'Compute', {  env: { account: '123456789012', region: 'us-east-1' },  vpc: foundationStack.vpc,  tables: dataStack.tables});
// API katmanı - deployment artifact, çok sık güncellemelerconst apiStack = new ApiStack(app, 'Api', {  env: { account: '123456789012', region: 'us-east-1' },  handlers: computeStack.handlers});
// Açık dependency'ler doğru deployment sırasını garantilerdataStack.addDependency(foundationStack);computeStack.addDependency(dataStack);apiStack.addDependency(computeStack);

Layer-based organizasyon stateful ve stateless kaynakların net ayrımını, farklı güncelleme sıklıklarının ayrı ele alınmasını (foundation nadiren değişir, API'ler sık değişir), data katmanı nadiren değiştiği için azalan deployment riskini ve katman başına farklı removal policy'lerin daha kolay uygulanmasını sağlar.

Dezavantajlar ise business logic'in katmanlara dağılması, team ownership'in daha az net olması ve complete özellikleri deploy etmenin birden fazla katman arasında koordinasyon gerektirmesi.

Layer-based ne zaman işe yarar: Net infrastructure katmanları, ayrı infrastructure ve application ekipleri, stateful kaynaklardan stateless kaynakları ayırma ihtiyacı.

Hybrid Yaklaşım

Gerçek dünya projeleri genellikle pattern'leri birleştirmekten fayda görür - cross-cutting concern'ler için katmanlar kullanırken business logic'i domain'e göre organize etme.

typescript
// Directory yapısı// cdk-project/// ├── lib/// │   ├── foundation/// │   │   ├── network-stack.ts      // Shared VPC, subnet'ler// │   │   ├── security-stack.ts     // Security group'lar, IAM role'ler// │   │   └── monitoring-stack.ts   // CloudWatch, X-Ray// │   ├── domains/// │   │   ├── user/// │   │   │   ├── user-service-stack.ts// │   │   │   └── constructs/// │   │   │       ├── user-api.ts// │   │   │       ├── user-storage.ts// │   │   │       └── user-lambda.ts// │   │   ├── order/// │   │   │   ├── order-service-stack.ts// │   │   │   └── constructs/// │   │   └── payment/// │   │       ├── payment-service-stack.ts// │   │       └── constructs/// │   ├── shared/// │   │   ├── constructs/// │   │   │   ├── api-endpoint.ts   // Tekrar kullanılabilir L3 construct// │   │   │   ├── lambda-factory.ts // Factory fonksiyonları// │   │   │   └── monitored-table.ts// │   │   └── aspects/// │   │       ├── tagging-aspect.ts// │   │       ├── security-aspect.ts// │   │       └── removal-policy-aspect.ts// │   └── config/// │       ├── environment.ts        // Type-safe config// │       ├── dev.ts// │       ├── staging.ts// │       └── prod.ts
// lib/foundation/network-stack.tsimport { Stack, StackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';import { Vpc, SubnetType, GatewayVpcEndpointAwsService } from 'aws-cdk-lib/aws-ec2';
export class NetworkStack extends Stack {  public readonly vpc: Vpc;
  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    this.vpc = new Vpc(this, 'Vpc', {      maxAzs: 3,      natGateways: 1,      subnetConfiguration: [        {          cidrMask: 24,          name: 'public',          subnetType: SubnetType.PUBLIC        },        {          cidrMask: 24,          name: 'private',          subnetType: SubnetType.PRIVATE_WITH_EGRESS        },        {          cidrMask: 28,          name: 'isolated',          subnetType: SubnetType.PRIVATE_ISOLATED        }      ]    });
    // VPC endpoint'ler data transfer maliyetlerini azaltır    this.vpc.addGatewayEndpoint('S3Endpoint', {      service: GatewayVpcEndpointAwsService.S3    });
    this.vpc.addGatewayEndpoint('DynamoDBEndpoint', {      service: GatewayVpcEndpointAwsService.DYNAMODB    });  }}
// bin/app.tsimport { App } from 'aws-cdk-lib';import { NetworkStack } from '../lib/foundation/network-stack';import { UserServiceStack } from '../lib/domains/user/user-service-stack';import { OrderServiceStack } from '../lib/domains/order/order-service-stack';import { getConfig } from '../lib/config/environment';
const app = new App();const config = getConfig(process.env.STAGE || 'dev');
// Foundation katmanı - ilk deploy edilir, nadiren değişirconst network = new NetworkStack(app, 'Network', {  env: config.env});
// Domain stack'leri - business logic, bağımsız deploymentconst userStack = new UserServiceStack(app, 'UserService', {  env: config.env,  vpc: network.vpc});
const orderStack = new OrderServiceStack(app, 'OrderService', {  env: config.env,  vpc: network.vpc,  userApiEndpoint: userStack.apiEndpoint});
orderStack.addDependency(userStack);

Hybrid yaklaşım her iki dünyanın en iyisini veriyor: foundation katmanı cross-cutting concern'lerle ilgileniyor, domain stack'leri business logic bütünlüğünü koruyor ve net dependency graph'leri foundation kurulduktan sonra bağımsız deployment'ı mümkün kılıyor.

Karar Çerçevesi

Stack'leri Ne Zaman Ayırmalı vs Construct Kullanmalı

Karşılaştığım en yaygın sorulardan biri: "Ne zaman yeni bir stack oluşturmalıyım, ne zaman sadece construct kullanmalıyım?" Cevap deployment bağımsızlığına dayanıyor.

Birden fazla stack kullan:

  • Farklı ekipler farklı parçaları maintain edecekse
  • Farklı deployment schedule'ları varsa (data layer ayda bir deploy edilir, compute layer günlük)
  • Farklı environment'lar veya account'lar varsa (dev bir account'ta, prod başka bir account'ta)
  • CI/CD için bağımsız deployment kritikse
  • CloudFormation'ın stack başına 500 kaynak limitine yaklaşılıyorsa
  • Açık sınırları olan farklı business domain'leri varsa
  • Stateful kaynaklar stateless kaynaklardan farklı removal policy'lere ihtiyaç duyuyorsa

Construct kullan (stack değil):

  • Sadece aynı ekip içinde code ownership ayırıyorsan
  • Kaynaklar birlikte değişiyor ve birlikte deploy edilmeliyse
  • Bağımsız deployment'a ihtiyaç yoksa
  • Kaynaklar tight coupling ve çok sayıda referansa sahipse
  • Sadece code sürdürülebilirliği için organize ediyorsan

İşte bir karar ağacı örneği:

typescript
// Soru 1: Kaynaklar birlikte mi değişiyor?// EVET → Construct'larla tek stack// HAYIR → Devam
// Soru 2: Farklı ekipler mi maintain ediyor?// EVET → Ayrı stack'ler// HAYIR → Devam
// Soru 3: Farklı deployment schedule'ları mı var?// EVET → Ayrı stack'ler// HAYIR → Devam
// Soru 4: 500 kaynağa yaklaşıyor musun?// EVET → Ayrı stack'ler (veya nested stack'ler)// HAYIR → Construct'larla tek stack
// Örnek: API, Lambda, DynamoDB ile user service// Hepsi birlikte değişiyor, aynı ekip, birlikte deploy → Tek stackexport class UserServiceStack extends Stack {  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    // Ayrı stack'ler değil, construct'larla organize et    const storage = new UserStorageConstruct(this, 'Storage');    const compute = new UserComputeConstruct(this, 'Compute', {      table: storage.table    });    const api = new UserApiConstruct(this, 'Api', {      handler: compute.handler    });  }}
// Örnek: Foundation (VPC) vs Application (Lambda/API)// Farklı değişim sıklığı, farklı lifecycle → Ayrı stack'lerconst foundationStack = new FoundationStack(app, 'Foundation');const appStack = new AppStack(app, 'App', {  vpc: foundationStack.vpc});appStack.addDependency(foundationStack);

Monorepo vs Multi-Repo Stratejisi

Monorepo versus multi-repo kararı, ekiplerin nasıl işbirliği yaptığını ve infrastructure'ı nasıl deploy ettiğini etkiler.

Monorepo pattern:

typescript
// Directory yapısı// company-infrastructure/        # Tek repository// ├── packages/// │   ├── foundation/// │   │   ├── src/// │   │   │   └── network-stack.ts// │   │   ├── package.json// │   │   └── cdk.json// │   ├── user-service/// │   │   ├── src/// │   │   │   └── user-stack.ts// │   │   ├── package.json// │   │   └── cdk.json// │   ├── order-service/// │   │   ├── src/// │   │   │   └── order-stack.ts// │   │   ├── package.json// │   │   └── cdk.json// │   └── shared-constructs/// │       ├── src/// │       │   ├── api-endpoint.ts// │       │   └── lambda-factory.ts// │       ├── package.json// │       └── tsconfig.json// ├── package.json              # Root package.json// ├── nx.json                   # NX workspace config// └── tsconfig.base.json
// package.json (root){  "name": "company-infrastructure",  "private": true,  "workspaces": ["packages/*"],  "scripts": {    "build": "nx run-many --target=build --all",    "deploy:dev": "nx run-many --target=deploy --all --args='--context stage=dev'",    "deploy:user": "nx run user-service:deploy"  },  "devDependencies": {    "nx": "^17.0.0",    "aws-cdk": "^2.100.0"  }}

Monorepo faydaları: tek source of truth, service'ler arasında atomic değişiklikler, boundary'ler arasında daha kolay refactoring, npm'e publish etmeden shared construct'lar, basitleştirilmiş dependency yönetimi ve tek CI/CD pipeline konfigürasyonu.

Dezavantajlar: daha büyük repository clone süresi, service'ler arasında tight coupling riski, hangi service'lerin değiştiğini algılayan CI/CD karmaşıklığı, daha az ayrıntılı team permission'ları ve potansiyel olarak daha büyük deployment blast radius.

Multi-repo pattern her service için ayrı repository'ler kullanır, shared construct'lar private npm registry'ye publish edilir.

Multi-repo faydaları: net service boundary'leri, bağımsız versioning ve deployment, açık team ownership ve permission'ları, daha küçük repository boyutu, language/framework çeşitliliği ve azalmış merge conflict'ler.

Dezavantajlar: cross-service değişikliklerin birden fazla PR gerektirmesi, shared construct'ların publish ve versioning'e ihtiyaç duyması, dependency version yönetimi karmaşıklığı, consistency'i garanti etmenin zorluğu ve daha fazla CI/CD pipeline overhead'i.

Tavsiyem: 10'dan az kişilik ekipler için monorepo ile başla. Atomic değişikliklerin ve shared construct'ların basitliği, küçük ölçekte dezavantajlardan daha ağır basıyor. Team boyutu veya deployment bağımsızlığı gerektirdiğinde multi-repo'ya geç - genellikle 50+ mühendis civarında veya farklı lifecycle'lara sahip gerçekten bağımsız service'lerin olduğu durumda.

Cross-Cutting Concern'leri Ele Alma

Networking, security, monitoring ve diğer cross-cutting concern'ler domain boundary'lerine temiz bir şekilde uymaz. İyi çalışan üç pattern var.

Foundation Stack Pattern

typescript
// lib/foundation/foundation-stack.tsimport { Stack, StackProps, Tags } from 'aws-cdk-lib';import { Construct } from 'constructs';import { Vpc, IVpc, SubnetType, SecurityGroup } from 'aws-cdk-lib/aws-ec2';import { Dashboard } from 'aws-cdk-lib/aws-cloudwatch';
export interface FoundationStackProps extends StackProps {  readonly stage: string;}
export class FoundationStack extends Stack {  public readonly vpc: IVpc;  public readonly monitoring: Dashboard;  public readonly lambdaSecurityGroup: SecurityGroup;
  constructor(scope: Construct, id: string, props: FoundationStackProps) {    super(scope, id, props);
    // Standart subnet'lerle VPC    this.vpc = new Vpc(this, 'Vpc', {      maxAzs: 3,      natGateways: props.stage === 'prod' ? 3 : 1,      subnetConfiguration: [        {          cidrMask: 24,          name: 'public',          subnetType: SubnetType.PUBLIC        },        {          cidrMask: 24,          name: 'private',          subnetType: SubnetType.PRIVATE_WITH_EGRESS        },        {          cidrMask: 28,          name: 'isolated',          subnetType: SubnetType.PRIVATE_ISOLATED        }      ]    });
    // Merkezi monitoring dashboard    this.monitoring = new Dashboard(this, 'Monitoring', {      dashboardName: `${props.stage}-metrics`    });
    // Lambda fonksiyonları için standart security group    this.lambdaSecurityGroup = new SecurityGroup(this, 'LambdaSG', {      vpc: this.vpc,      description: 'Security group for Lambda functions',      allowAllOutbound: true    });
    // Bu stack'teki her şeye tag uygula    Tags.of(this).add('Environment', props.stage);    Tags.of(this).add('ManagedBy', 'CDK');    Tags.of(this).add('CostCenter', 'Engineering');  }}
// Domain stack'leri foundation'ı consume ederexport class UserServiceStack extends Stack {  constructor(    scope: Construct,    id: string,    props: StackProps & { foundation: FoundationStack }  ) {    super(scope, id, props);
    const handler = new NodejsFunction(this, 'Handler', {      vpc: props.foundation.vpc,      securityGroups: [props.foundation.lambdaSecurityGroup],      entry: 'src/handlers/user.ts'    });
    // Lambda metric'lerini merkezi dashboard'a ekle    props.foundation.monitoring.addWidgets(      new GraphWidget({        title: 'User Service Invocations',        left: [handler.metricInvocations()]      })    );  }}

Cross-Cutting Policy'ler için CDK Aspect'ler

CDK Aspect'ler, application'daki tüm kaynaklara otomatik olarak policy uygulamanı sağlar.

typescript
// lib/shared/aspects/security-compliance-aspect.tsimport { IAspect } from 'aws-cdk-lib';import { IConstruct } from 'constructs';import { CfnBucket } from 'aws-cdk-lib/aws-s3';import { Table } from 'aws-cdk-lib/aws-dynamodb';import { NodejsFunction, Function } from 'aws-cdk-lib/aws-lambda-nodejs';import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';import { RemovalPolicy } from 'aws-cdk-lib';
export class SecurityComplianceAspect implements IAspect {  visit(node: IConstruct): void {    // Tüm S3 bucket'larında encryption'ı zorla    if (node instanceof CfnBucket) {      if (!node.bucketEncryption) {        node.bucketEncryption = {          serverSideEncryptionConfiguration: [{            serverSideEncryptionByDefault: {              sseAlgorithm: 'AES256'            }          }]        };      }    }
    // DynamoDB table'larında encryption ve retention'ı zorla    if (node instanceof Table) {      // Açık encryption olmayan table'lar AWS managed alır      node.applyRemovalPolicy(RemovalPolicy.RETAIN);    }
    // Tüm Lambda fonksiyonlarının log retention'ına sahip olduğunu garantile    if (node instanceof NodejsFunction || node instanceof Function) {      const logGroupName = `/aws/lambda/${node.functionName}`;      new LogGroup(node, 'LogGroup', {        logGroupName,        retention: RetentionDays.ONE_WEEK,        removalPolicy: RemovalPolicy.DESTROY      });    }  }}
// Tüm app'e uygulaconst app = new App();Aspects.of(app).add(new SecurityComplianceAspect());

Shared Construct Library

Best practice'leri kapsayan tekrar kullanılabilir L3 construct'lar oluştur. Bu construct'ları factory pattern'leri ve functional programming yaklaşımlarıyla daha da güçlendirebilirsin - detaylar için AWS CDK Functional Patterns yazısına göz at.

typescript
// lib/shared/constructs/monitored-lambda.tsimport { Construct } from 'constructs';import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';import { Alarm, ComparisonOperator } from 'aws-cdk-lib/aws-cloudwatch';import { Topic } from 'aws-cdk-lib/aws-sns';import { EmailSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions';import { Tracing } from 'aws-cdk-lib/aws-lambda';
export interface MonitoredLambdaProps extends NodejsFunctionProps {  readonly alarmEmail?: string;  readonly errorThreshold?: number;}
export class MonitoredLambda extends Construct {  public readonly function: NodejsFunction;  public readonly errorAlarm: Alarm;  public readonly throttleAlarm: Alarm;
  constructor(scope: Construct, id: string, props: MonitoredLambdaProps) {    super(scope, id);
    this.function = new NodejsFunction(this, 'Function', {      ...props,      tracing: Tracing.ACTIVE // Default olarak X-Ray aktif    });
    // Otomatik error alarm    this.errorAlarm = new Alarm(this, 'ErrorAlarm', {      metric: this.function.metricErrors(),      threshold: props.errorThreshold ?? 5,      evaluationPeriods: 2,      comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,      alarmDescription: `Errors on ${this.function.functionName}`    });
    // Otomatik throttle alarm    this.throttleAlarm = new Alarm(this, 'ThrottleAlarm', {      metric: this.function.metricThrottles(),      threshold: 1,      evaluationPeriods: 1,      comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,      alarmDescription: `Throttles on ${this.function.functionName}`    });
    // İsteğe bağlı SNS notification    if (props.alarmEmail) {      const topic = new Topic(this, 'AlarmTopic');      topic.addSubscription(new EmailSubscription(props.alarmEmail));      this.errorAlarm.addAlarmAction(new SnsAction(topic));      this.throttleAlarm.addAlarmAction(new SnsAction(topic));    }  }}
// Herhangi bir domain stack'te kullanımconst handler = new MonitoredLambda(this, 'Handler', {  entry: 'src/handlers/user.ts',  alarmEmail: '[email protected]',  errorThreshold: 10});

Multi-Environment Yönetimi

Birden fazla environment'ı (dev, staging, prod) yönetmek dikkatli configuration stratejisi gerektiriyor.

Static Stack Creation (Tavsiye Edilen)

Consistency'i garantilemek için synthesis sırasında tüm environment'ları oluştur.

typescript
// config/environment.tsexport interface EnvironmentConfig {  readonly stage: 'dev' | 'staging' | 'prod';  readonly account: string;  readonly region: string;  readonly vpcCidr: string;  readonly lambdaDefaults: {    readonly timeout: number;    readonly memorySize: number;  };  readonly enableDetailedMonitoring: boolean;}
// config/dev.tsexport const devConfig: EnvironmentConfig = {  stage: 'dev',  account: '111111111111',  region: 'us-east-1',  vpcCidr: '10.0.0.0/16',  lambdaDefaults: {    timeout: 30,    memorySize: 512  },  enableDetailedMonitoring: false};
// config/prod.tsexport const prodConfig: EnvironmentConfig = {  stage: 'prod',  account: '222222222222',  region: 'us-east-1',  vpcCidr: '10.1.0.0/16',  lambdaDefaults: {    timeout: 60,    memorySize: 1024  },  enableDetailedMonitoring: true};
// bin/app.tsimport { App } from 'aws-cdk-lib';import { devConfig, stagingConfig, prodConfig } from '../config';import { UserServiceStack } from '../lib/domains/user/user-service-stack';
const app = new App();
// Synthesis sırasında tüm environment'ları oluştur[devConfig, stagingConfig, prodConfig].forEach(config => {  new UserServiceStack(app, `UserService-${config.stage}`, {    env: { account: config.account, region: config.region },    config  });});
// Faydalar:// - Bir kez synthesize et, birden fazla environment'a deploy et// - Dev'de çalışan kodun prod'da da çalışacağını garantiler// - TypeScript geliştirme sırasında TÜM environment'ları validate eder// - `cdk list` tüm environment'lardaki tüm stack'leri gösterir

Bu yaklaşım production configuration'ın geliştirme sırasında validate edilmesini garantiler. Production config'te bir yazım hatası, production deployment sırasında değil, local'de cdk synth çalıştırıldığında hemen yakalanır.

Yaygın Hatalar

Erken Stack Bölme

Çok fazla küçük stack oluşturmak aşırı cross-stack referanslarına ve deployment karmaşıklığına yol açar.

typescript
// Aşırı bölme - tek service için 6 stack (bunu yapma)const vpcStack = new VpcStack(app, 'Vpc');const sgStack = new SecurityGroupStack(app, 'SG', { vpc: vpcStack.vpc });const tableStack = new TableStack(app, 'Table');const lambdaStack = new LambdaStack(app, 'Lambda', {  table: tableStack.table,  sg: sgStack.lambdaSG});const apiStack = new ApiStack(app, 'Api', { handler: lambdaStack.handler });const alarmStack = new AlarmStack(app, 'Alarm', {  lambda: lambdaStack.handler,  api: apiStack.api});
// Her değişiklik doğru sırada 6 stack deploy etmeyi gerektirir// Cross-stack referanslar kırılgan dependency'ler yaratır
// Daha iyi: Organizasyon için construct'larla tek stackexport class UserServiceStack extends Stack {  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    const network = new NetworkConstruct(this, 'Network');    const storage = new StorageConstruct(this, 'Storage');    const compute = new ComputeConstruct(this, 'Compute', {      table: storage.table,      vpc: network.vpc    });    const api = new ApiConstruct(this, 'Api', { handler: compute.handler });    const monitoring = new MonitoringConstruct(this, 'Monitoring', {      lambda: compute.handler,      api: api.restApi    });  }}
// Bir kez deploy et, tüm kaynaklar birlikte güncellenir

Büyük stack'lerle başla ve sadece net bir sebep olduğunda böl: farklı ekipler, farklı lifecycle'lar veya kaynak limitine yaklaşma.

Circular Dependency'ler

Stack'ler arasındaki circular dependency'ler kaynak ilişkilerini tasarlarken yaygın bir sorun.

typescript
// Circular dependency problemi (bunu yapma)export class UserStack extends Stack {  public readonly apiUrl: string;
  constructor(scope: Construct, id: string, props: { orderTable: ITable }) {    super(scope, id);    // User service order table'a ihtiyaç duyuyor ← OrderStack'e dependency    const handler = new NodejsFunction(this, 'Handler', {      environment: { ORDER_TABLE: props.orderTable.tableName }    });
    this.apiUrl = api.url;  }}
export class OrderStack extends Stack {  public readonly table: Table;
  constructor(scope: Construct, id: string, props: { userApiUrl: string }) {    super(scope, id);    // Order service user API'ye ihtiyaç duyuyor ← UserStack'e dependency    const handler = new NodejsFunction(this, 'Handler', {      environment: { USER_API_URL: props.userApiUrl }    });
    this.table = new Table(/*...*/);  }}
// İkisinden birini ilk oluşturamıyoruz!
// Çözüm 1: Shared resource'larla foundation stackconst foundationStack = new FoundationStack(app, 'Foundation');
const userStack = new UserStack(app, 'User', {  sharedTable: foundationStack.sharedTable});
const orderStack = new OrderStack(app, 'Order', {  sharedTable: foundationStack.sharedTable,  userApiUrl: userStack.apiUrl});
// Çözüm 2: Event-driven (direkt dependency yok)const userStack = new UserStack(app, 'User');const orderStack = new OrderStack(app, 'Order');
// Service'ler EventBridge/SQS üzerinden iletişim kurar, cross-reference yok

Tek yönlü dependency akışı tasarla. Döngüleri kırmak için shared resource stack'leri veya event-driven mimari kullan.

Cross-Account/Region Referansları

Farklı AWS hesapları veya bölgeleri arasında kaynak referansı açık işleme gerektirir. SSM Parameter Store üzerinden export/import veya manuel ARN geçişi kullan. Doğrudan cross-account referansı çalışmaz; çözüm olarak StringParameter.valueFromLookup ile ARN alıp Table.fromTableArn ile import et.

CDK Refactor Tool Kullanmama

Logical ID'leri korumadan refactor yapmak kaynak değiştirme ve veri kaybına yol açar. Kod reorganizasyonunda her zaman logical ID'leri koru; overrideLogicalId veya construct ismini sabit tut.

Sonuçlar

Bir projede foundation katmanı ile domain-based organizasyon uyguladıktan sonra, ekip ölçülebilir iyileştirmeler gördü. Bireysel service'ler için deployment süresi 15 dakikadan (tüm service'leri deploy etme) 3-5 dakikaya (sadece değişen service'i deploy etme) düştü. Merge conflict'ler önemli ölçüde azaldı - birden fazla ekibin shared stack'lere dokunmasından kaynaklanan sprint başına 3-4 conflict yerine, her ekip kendi domain stack'ine sahip olduğu için conflict'ler nadir hale geldi.

Organizasyon pattern'i onboarding'i de iyileştirdi. Yeni developer'lar user service'i service-type klasörlerine dağılmış infrastructure'ı bir araya getirmek yerine sadece domains/user/ directory'sini okuyarak anlayabiliyordu. Code review süresi iyileşti çünkü reviewer'lar cross-stack dependency'leri anlamadan tek bir domain'e odaklanabiliyordu.

Önemli Çıkarımlar

Organizasyon pattern'i business yapısıyla eşleşir: Business'a hizalanmış ekipler için domain-based, infrastructure ekipleri için layer-based, product ekipleri için feature-based seç. Pattern, organizasyonun sistemi nasıl düşündüğünü yansıtmalı.

Stack'lerden önce construct'lar: Çoğu code organizasyon problemi birden fazla stack değil, construct'larla çözülür. Stack'leri sadece deployment bağımsızlığı, team boundary'leri veya kaynak limitine ulaşma için böl.

Foundation pattern esastır: Shared infrastructure'ı (VPC, networking, security) domain-specific kaynaklardan ayır, böylece circular dependency'lerden kaçın ve domain bağımsızlığını sağla.

Küçük ekipler için monorepo, büyükler için multi-repo: Monorepo'lar ~50 developer'a kadar iyi çalışır. Sonrasında, net service boundary'leri ve team özerkliği için multi-repo'yu düşün.

Static stack creation consistency garantiler: Synthesis sırasında tüm environment'ları (dev, staging, prod) oluşturmak dev'de test edilen kodun tam olarak prod'a deploy edileceğini garantiler.

Basit başla, yavaşça geliştir: Büyük stack'ler ve construct'larla başla, sadece net sebepler ortaya çıktığında ayrı stack'lere böl (farklı ekipler, farklı lifecycle'lar, kaynak limitleri).

Doğru organizasyon pattern'i senin spesifik bağlamına bağlı - ekip boyutu, deployment gereksinimleri ve business yapısı. Basit başla ve gerçek ihtiyaçlar netleştikçe organizasyonun ortaya çıkmasına izin ver.

İlgili Yazılar